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.
## Service
```go
package main
import "github.com/foomo/soap"
import (
"encoding/xml"
"fmt"
"net/http"
"github.com/foomo/soap"
)
// FooRequest a simple request
type FooRequest struct {
Foo string
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("127.0.0.1:8080")
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{}
},
func(request interface{}) (response interface{}, err error) {
fooRequest := request.(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,
}
@ -34,11 +50,47 @@ func RunServer() {
return
},
)
err := soapServer.ListenAndServe(":8080")
fmt.Println("exiting with error", err)
}
func main() {
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
import (
"encoding/json"
"encoding/xml"
"errors"
"io/ioutil"
"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 dummyContent struct{}
type operationHander struct {
requestFactory RequestFactoryFunc
handler OperationHandlerFunc
}
type Server struct {
handlers map[string]map[string]*operationHander
type responseWriter struct {
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 {
s := &Server{
handlers: make(map[string]map[string]*operationHander),
handlers: make(map[string]map[string]*operationHander),
Marshaller: newDefaultMarshaller(),
}
return s
}
// HandleOperation register to handle an operation
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{
handler: operationHandlerFunc,
requestFactory: requestFactory,
}
}
func (s *Server) serveSOAP(requestEnvelopeBytes []byte, soapAction string) (responseEnvelopeBytes []byte, err error) {
messageType := "find me as the element name in the soap body"
actionHandlers, ok := s.handlers[soapAction]
if !ok {
err = errors.New("could not find handlers for action: \"" + soapAction + "\"")
return
func (s *Server) handleError(err error, w http.ResponseWriter) {
// has to write a soap fault
l("handling error:", err)
responseEnvelope := &Envelope{
Body: Body{
Content: &Fault{
String: err.Error(),
},
},
}
handler, ok := actionHandlers[messageType]
if !ok {
err = errors.New("no handler for message type: " + messageType)
return
}
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(),
}
xmlBytes, xmlErr := s.Marshaller.Marshal(responseEnvelope)
if xmlErr != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("could not marshal soap fault for: " + err.Error() + " xmlError: " + xmlErr.Error()))
} else {
responseEnvelope.Body.Content = response
w.Write(xmlBytes)
}
// marshal responseEnvelope
return
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
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:
// this will be a soap fault !?
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 {
return http.ListenAndServe(addr, s)
}

27
soap.go
View File

@ -2,26 +2,31 @@ package soap
import "encoding/xml"
type SOAPEnvelope struct {
// Envelope type
type Envelope struct {
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"`
Header interface{}
}
type SOAPBody struct {
// Body type
type Body struct {
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
Fault *SOAPFault `xml:",omitempty"`
Content interface{} `xml:",omitempty"`
Fault *Fault `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"`
Code string `xml:"faultcode,omitempty"`
@ -30,7 +35,8 @@ type SOAPFault struct {
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 {
return xml.UnmarshalError("Content must be a pointer to a struct")
}
@ -56,7 +62,7 @@ Loop:
if consumed {
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" {
b.Fault = &SOAPFault{}
b.Fault = &Fault{}
b.Content = nil
err = d.DecodeElement(b.Fault, &se)
@ -66,6 +72,7 @@ Loop:
consumed = true
} else {
b.SOAPBodyContentType = se.Name.Local
if err = d.DecodeElement(b.Content, &se); err != nil {
return err
}
@ -80,6 +87,6 @@ Loop:
return nil
}
func (f *SOAPFault) Error() string {
func (f *Fault) Error() 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()
}