added custom marshalling, verbosity and cleaned up

This commit is contained in:
Jan Halfar 2016-02-02 18:20:31 +01:00
parent f2772cc8a7
commit 6f5b9d46a0
7 changed files with 439 additions and 89 deletions

View File

@ -4,29 +4,45 @@ First of all do not write SOAP services if you can avoid it! It is over.
If you can not avoid it this package might help. If you can not avoid it this package might help.
## Service
```go ```go
package main package main
import "github.com/foomo/soap" import (
"encoding/xml"
"fmt"
"net/http"
"github.com/foomo/soap"
)
// FooRequest a simple request
type FooRequest struct { type FooRequest struct {
Foo string XMLName xml.Name `xml:"fooRequest"`
Foo string
} }
// FooResponse a simple response
type FooResponse struct { type FooResponse struct {
Bar string Bar string
} }
// RunServer run a little demo server
func RunServer() { func RunServer() {
soapServer := soap.NewServer("127.0.0.1:8080") soapServer := soap.NewServer()
soapServer.HandleOperation( soapServer.HandleOperation(
// SOAPAction
"operationFoo", "operationFoo",
// tagname of soap body content
"fooRequest",
// RequestFactoryFunc - give the server sth. to unmarshal the request into
func() interface{} { func() interface{} {
return &FooRequest{} return &FooRequest{}
}, },
func(request interface{}) (response interface{}, err error) { // OperationHandlerFunc - do something
fooRequest := request.(FooRequest) func(request interface{}, w http.ResponseWriter, httpRequest *http.Request) (response interface{}, err error) {
fooRequest := request.(*FooRequest)
fooResponse := &FooResponse{ fooResponse := &FooResponse{
Bar: "Hello " + fooRequest.Foo, Bar: "Hello " + fooRequest.Foo,
} }
@ -34,11 +50,47 @@ func RunServer() {
return return
}, },
) )
err := soapServer.ListenAndServe(":8080")
fmt.Println("exiting with error", err)
} }
func main() { func main() {
RunServer() RunServer()
} }
```
## Client
```go
package main
import (
"encoding/xml"
"log"
"github.com/foomo/soap"
)
// FooRequest a simple request
type FooRequest struct {
XMLName xml.Name `xml:"fooRequest"`
Foo string
}
// FooResponse a simple response
type FooResponse struct {
Bar string
}
func main() {
soap.Verbose = true
client := soap.NewClient("http://127.0.0.1:8080/", nil, nil)
response := &FooResponse{}
httpResponse, err := client.Call("operationFoo", &FooRequest{Foo: "hello i am foo"}, response)
if err != nil {
panic(err)
}
log.Println(response.Bar, httpResponse.Status)
}
``` ```

140
client.go Normal file
View File

@ -0,0 +1,140 @@
package soap
import (
"bytes"
"encoding/xml"
"io/ioutil"
"log"
"net"
"net/http"
"time"
)
// ClientDialTimeout default timeout 30s
var ClientDialTimeout = time.Duration(30 * time.Second)
// UserAgent is the default user agent
var UserAgent = "go-soap-0.1"
// Verbose be verbose
var Verbose = false
func l(m ...interface{}) {
if Verbose {
log.Println(m...)
}
}
// XMLMarshaller lets you inject your favourite custom xml implementation
type XMLMarshaller interface {
Marshal(v interface{}) ([]byte, error)
Unmarshal(xml []byte, v interface{}) error
}
type defaultMarshaller struct {
}
func (dm *defaultMarshaller) Marshal(v interface{}) (xmlBytes []byte, err error) {
return xml.Marshal(v)
}
func (dm *defaultMarshaller) Unmarshal(xmlBytes []byte, v interface{}) error {
return xml.Unmarshal(xmlBytes, v)
}
func newDefaultMarshaller() XMLMarshaller {
return &defaultMarshaller{}
}
func dialTimeout(network, addr string) (net.Conn, error) {
return net.DialTimeout(network, addr, ClientDialTimeout)
}
// BasicAuth credentials for the client
type BasicAuth struct {
Login string
Password string
}
// Client generic SOAP client
type Client struct {
url string
tls bool
auth *BasicAuth
tr *http.Transport
Marshaller XMLMarshaller
}
// NewClient constructor
func NewClient(url string, auth *BasicAuth, tr *http.Transport) *Client {
return &Client{
url: url,
auth: auth,
tr: tr,
Marshaller: newDefaultMarshaller(),
}
}
// Call make a SOAP call
func (s *Client) Call(soapAction string, request, response interface{}) (httpResponse *http.Response, err error) {
envelope := Envelope{}
envelope.Body.Content = request
xmlBytes, err := s.Marshaller.Marshal(envelope)
if err != nil {
return
}
req, err := http.NewRequest("POST", s.url, bytes.NewBuffer(xmlBytes))
if err != nil {
return
}
if s.auth != nil {
req.SetBasicAuth(s.auth.Login, s.auth.Password)
}
req.Header.Add("Content-Type", "text/xml; charset=\"utf-8\"")
req.Header.Set("User-Agent", UserAgent)
if soapAction != "" {
req.Header.Add("SOAPAction", soapAction)
}
req.Close = true
tr := s.tr
if tr == nil {
tr = http.DefaultTransport.(*http.Transport)
}
client := &http.Client{Transport: tr}
l("POST to", s.url, "with", string(xmlBytes))
httpResponse, err = client.Do(req)
if err != nil {
return
}
defer httpResponse.Body.Close()
rawbody, err := ioutil.ReadAll(httpResponse.Body)
if err != nil {
return
}
if len(rawbody) == 0 {
l("empty response")
return
}
l("response", string(rawbody))
respEnvelope := new(Envelope)
respEnvelope.Body = Body{Content: response}
err = xml.Unmarshal(rawbody, respEnvelope)
if err != nil {
return
}
fault := respEnvelope.Body.Fault
if fault != nil {
err = fault
return
}
return
}

30
examples/client.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"encoding/xml"
"log"
"github.com/foomo/soap"
)
// FooRequest a simple request
type FooRequest struct {
XMLName xml.Name `xml:"fooRequest"`
Foo string
}
// FooResponse a simple response
type FooResponse struct {
Bar string
}
func main() {
soap.Verbose = true
client := soap.NewClient("http://127.0.0.1:8080/", nil, nil)
response := &FooResponse{}
httpResponse, err := client.Call("operationFoo", &FooRequest{Foo: "hello i am foo"}, response)
if err != nil {
panic(err)
}
log.Println(response.Bar, httpResponse.Status)
}

52
examples/simple-server.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"encoding/xml"
"fmt"
"net/http"
"github.com/foomo/soap"
)
// FooRequest a simple request
type FooRequest struct {
XMLName xml.Name `xml:"fooRequest"`
Foo string
}
// FooResponse a simple response
type FooResponse struct {
Bar string
}
// RunServer run a little demo server
func RunServer() {
soapServer := soap.NewServer()
soapServer.HandleOperation(
// SOAPAction
"operationFoo",
// tagname of soap body content
"fooRequest",
// RequestFactoryFunc - give the server sth. to unmarshal the request into
func() interface{} {
return &FooRequest{}
},
// OperationHandlerFunc - do something
func(request interface{}, w http.ResponseWriter, httpRequest *http.Request) (response interface{}, err error) {
fooRequest := request.(*FooRequest)
fooResponse := &FooResponse{
Bar: "Hello \"" + fooRequest.Foo + "\"",
}
response = fooResponse
return
},
)
err := soapServer.ListenAndServe(":8080")
fmt.Println("exiting with error", err)
}
func main() {
// see what is going on
soap.Verbose = true
RunServer()
}

171
server.go
View File

@ -1,71 +1,170 @@
package soap package soap
import ( import (
"encoding/json"
"encoding/xml"
"errors" "errors"
"io/ioutil"
"net/http" "net/http"
) )
type OperationHandlerFunc func(request interface{}) (response interface{}, err error) // OperationHandlerFunc runs the actual business logic - request is whatever you constructed in RequestFactoryFunc
type OperationHandlerFunc func(request interface{}, w http.ResponseWriter, httpRequest *http.Request) (response interface{}, err error)
// RequestFactoryFunc constructs a request object for OperationHandlerFunc
type RequestFactoryFunc func() interface{} type RequestFactoryFunc func() interface{}
type dummyContent struct{}
type operationHander struct { type operationHander struct {
requestFactory RequestFactoryFunc requestFactory RequestFactoryFunc
handler OperationHandlerFunc handler OperationHandlerFunc
} }
type Server struct { type responseWriter struct {
handlers map[string]map[string]*operationHander w http.ResponseWriter
outputStarted bool
} }
func (w *responseWriter) Header() http.Header {
return w.w.Header()
}
func (w *responseWriter) Write(b []byte) (int, error) {
w.outputStarted = true
return w.w.Write(b)
}
func (w *responseWriter) WriteHeader(code int) {
w.w.WriteHeader(code)
}
// Server a SOAP server, which can be run standalone or used as a http.HandlerFunc
type Server struct {
handlers map[string]map[string]*operationHander
Marshaller XMLMarshaller
}
// NewServer construct a new SOAP server
func NewServer() *Server { func NewServer() *Server {
s := &Server{ s := &Server{
handlers: make(map[string]map[string]*operationHander), handlers: make(map[string]map[string]*operationHander),
Marshaller: newDefaultMarshaller(),
} }
return s return s
} }
// HandleOperation register to handle an operation // HandleOperation register to handle an operation
func (s *Server) HandleOperation(action string, messageType string, requestFactory RequestFactoryFunc, operationHandlerFunc OperationHandlerFunc) { func (s *Server) HandleOperation(action string, messageType string, requestFactory RequestFactoryFunc, operationHandlerFunc OperationHandlerFunc) {
_, ok := s.handlers[action]
if !ok {
s.handlers[action] = make(map[string]*operationHander)
}
s.handlers[action][messageType] = &operationHander{ s.handlers[action][messageType] = &operationHander{
handler: operationHandlerFunc, handler: operationHandlerFunc,
requestFactory: requestFactory, requestFactory: requestFactory,
} }
} }
func (s *Server) serveSOAP(requestEnvelopeBytes []byte, soapAction string) (responseEnvelopeBytes []byte, err error) { func (s *Server) handleError(err error, w http.ResponseWriter) {
messageType := "find me as the element name in the soap body" // has to write a soap fault
actionHandlers, ok := s.handlers[soapAction] l("handling error:", err)
if !ok { responseEnvelope := &Envelope{
err = errors.New("could not find handlers for action: \"" + soapAction + "\"") Body: Body{
return Content: &Fault{
String: err.Error(),
},
},
} }
handler, ok := actionHandlers[messageType] xmlBytes, xmlErr := s.Marshaller.Marshal(responseEnvelope)
if !ok { if xmlErr != nil {
err = errors.New("no handler for message type: " + messageType) w.WriteHeader(http.StatusInternalServerError)
return w.Write([]byte("could not marshal soap fault for: " + err.Error() + " xmlError: " + xmlErr.Error()))
}
request := handler.requestFactory()
// parse from envelope.body.content into request
response, err := handler.handler(request)
responseEnvelope := &SOAPEnvelope{
Body: SOAPBody{},
}
if err != nil {
// soap fault party time
responseEnvelope.Body.Fault = &SOAPFault{
String: err.Error(),
}
} else { } else {
responseEnvelope.Body.Content = response w.Write(xmlBytes)
} }
// marshal responseEnvelope
return
} }
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case "POST": case "POST":
w.Write([]byte("that actually could be a soap request")) l("incoming POST")
soapRequestBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
s.handleError(errors.New("could not read POST:: "+err.Error()), w)
return
}
soapAction := r.Header.Get("SOAPAction")
l("SOAPAction", "\""+soapAction+"\"")
actionHandlers, ok := s.handlers[soapAction]
if !ok {
s.handleError(errors.New("unknown action \""+soapAction+"\""), w)
return
}
// we need to find out, what is in the body
probeEnvelope := &Envelope{
Body: Body{
Content: &dummyContent{},
},
}
err = xml.Unmarshal(soapRequestBytes, &probeEnvelope)
if err != nil {
s.handleError(errors.New("could not probe soap body content:: "+err.Error()), w)
return
}
t := probeEnvelope.Body.SOAPBodyContentType
l("found content type", t)
actionHandler, ok := actionHandlers[t]
if !ok {
s.handleError(errors.New("no action handler for content type: \""+t+"\""), w)
return
}
request := actionHandler.requestFactory()
envelope := &Envelope{
Body: Body{
Content: request,
},
}
err = xml.Unmarshal(soapRequestBytes, &envelope)
if err != nil {
s.handleError(errors.New("could not unmarshal request:: "+err.Error()), w)
return
}
l("request", jsonDump(envelope))
// we have a valid request time to call the handler
responseWriter := &responseWriter{
w: w,
outputStarted: false,
}
response, err := actionHandler.handler(request, responseWriter, r)
if err != nil {
l("action handler threw up")
s.handleError(err, w)
return
}
l("result", jsonDump(response))
if !responseWriter.outputStarted {
responseEnvelope := &Envelope{
Body: Body{
Content: response,
},
}
xmlBytes, err := s.Marshaller.Marshal(responseEnvelope)
if err != nil {
s.handleError(errors.New("could not marshal response:: "+err.Error()), w)
}
w.Write(xmlBytes)
} else {
l("action handler sent its own output")
}
default: default:
// this will be a soap fault !? // this will be a soap fault !?
w.Write([]byte("this is a soap service - you have to POST soap requests\n")) w.Write([]byte("this is a soap service - you have to POST soap requests\n"))
@ -73,6 +172,18 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
func jsonDump(v interface{}) string {
if !Verbose {
return "not dumping"
}
jsonBytes, err := json.MarshalIndent(v, "", " ")
if err != nil {
return "error in json dump :: " + err.Error()
}
return string(jsonBytes)
}
// ListenAndServe run standalone
func (s *Server) ListenAndServe(addr string) error { func (s *Server) ListenAndServe(addr string) error {
return http.ListenAndServe(addr, s) return http.ListenAndServe(addr, s)
} }

27
soap.go
View File

@ -2,26 +2,31 @@ package soap
import "encoding/xml" import "encoding/xml"
type SOAPEnvelope struct { // Envelope type
type Envelope struct {
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
Body SOAPBody Body Body
} }
type SOAPHeader struct { // Header type
type Header struct {
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Header"` XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Header"`
Header interface{} Header interface{}
} }
type SOAPBody struct { // Body type
type Body struct {
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
Fault *SOAPFault `xml:",omitempty"` Fault *Fault `xml:",omitempty"`
Content interface{} `xml:",omitempty"` Content interface{} `xml:",omitempty"`
SOAPBodyContentType string `xml:"-"`
} }
type SOAPFault struct { // Fault type
type Fault struct {
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"` XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"`
Code string `xml:"faultcode,omitempty"` Code string `xml:"faultcode,omitempty"`
@ -30,7 +35,8 @@ type SOAPFault struct {
Detail string `xml:"detail,omitempty"` Detail string `xml:"detail,omitempty"`
} }
func (b *SOAPBody) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { // UnmarshalXML implement xml.Unmarshaler
func (b *Body) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
if b.Content == nil { if b.Content == nil {
return xml.UnmarshalError("Content must be a pointer to a struct") return xml.UnmarshalError("Content must be a pointer to a struct")
} }
@ -56,7 +62,7 @@ Loop:
if consumed { if consumed {
return xml.UnmarshalError("Found multiple elements inside SOAP body; not wrapped-document/literal WS-I compliant") return xml.UnmarshalError("Found multiple elements inside SOAP body; not wrapped-document/literal WS-I compliant")
} else if se.Name.Space == "http://schemas.xmlsoap.org/soap/envelope/" && se.Name.Local == "Fault" { } else if se.Name.Space == "http://schemas.xmlsoap.org/soap/envelope/" && se.Name.Local == "Fault" {
b.Fault = &SOAPFault{} b.Fault = &Fault{}
b.Content = nil b.Content = nil
err = d.DecodeElement(b.Fault, &se) err = d.DecodeElement(b.Fault, &se)
@ -66,6 +72,7 @@ Loop:
consumed = true consumed = true
} else { } else {
b.SOAPBodyContentType = se.Name.Local
if err = d.DecodeElement(b.Content, &se); err != nil { if err = d.DecodeElement(b.Content, &se); err != nil {
return err return err
} }
@ -80,6 +87,6 @@ Loop:
return nil return nil
} }
func (f *SOAPFault) Error() string { func (f *Fault) Error() string {
return f.String return f.String
} }

View File

@ -1,42 +0,0 @@
package main
import (
"fmt"
"github.com/foomo/soap"
)
type FooRequest struct {
Foo string
}
type FooResponse struct {
Bar string
}
func RunServer() {
soapServer := soap.NewServer()
/*
soapServer.HandleOperation(
"operationFoo",
"FooRequest",
func() interface{} {
return &FooRequest{}
},
func(request interface{}) (response interface{}, err error) {
fooRequest := request.(*FooRequest)
fooResponse := &FooResponse{
Bar: "Hello " + fooRequest.Foo,
}
response = fooResponse
return
},
)
*/
err := soapServer.ListenAndServe(":8080")
fmt.Println(err)
}
func main() {
RunServer()
}