mirror of
https://github.com/foomo/soap.git
synced 2025-10-16 12:45:36 +00:00
added custom marshalling, verbosity and cleaned up
This commit is contained in:
parent
f2772cc8a7
commit
6f5b9d46a0
66
README.md
66
README.md
@ -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
140
client.go
Normal 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
30
examples/client.go
Normal 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
52
examples/simple-server.go
Normal 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
171
server.go
@ -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
27
soap.go
@ -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
|
||||
}
|
||||
|
||||
42
test/test.go
42
test/test.go
@ -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()
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user