From 7fa614a880f32a83915f07926d56f2830d455bd0 Mon Sep 17 00:00:00 2001 From: Peter Haworth Date: Wed, 12 Jan 2022 14:03:42 +0000 Subject: [PATCH 1/4] Start on making it possible to use standard encoding/xml for request marshaling Currently requires an explicit Request.UseXMLEncoder flag, but can probably automatically detect an XMLName field on the request object without breaking compatibility with requests which might trigger the Struct branch in recursiveEncode It's also a slightly uncomfortable mix of creating tokens for later encoding alongside encoding things directly, but the current interfaces make that hard to avoid Tests also still need a bit of work --- encode.go | 91 ++++++++++++++++++++++++++++++++++---------------- encode_test.go | 52 ++++++++++++++++++++++++++++- request.go | 5 +-- soap_test.go | 4 +-- 4 files changed, 119 insertions(+), 33 deletions(-) diff --git a/encode.go b/encode.go index 8487ea0..8080ae8 100644 --- a/encode.go +++ b/encode.go @@ -7,7 +7,7 @@ import ( ) var ( - soapPrefix = "soap" + soapPrefix = "soap" customEnvelopeAttrs map[string]string = nil ) @@ -40,7 +40,9 @@ func (c process) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { tokens.startEnvelope() if c.Client.HeaderParams != nil { tokens.startHeader(c.Client.HeaderName, namespace) - tokens.recursiveEncode(c.Client.HeaderParams) + if err := tokens.recursiveEncode(c.Client.HeaderParams); err != nil { + return err + } tokens.endHeader(c.Client.HeaderName) } @@ -49,27 +51,52 @@ func (c process) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { return err } - tokens.recursiveEncode(c.Request.Params) + if c.Request.UseXMLEncoder { + if err := tokens.flush(e); err != nil { + return err + } + if err := e.Encode(c.Request.Params); err != nil { + return err + } + if err := tokens.flush(e); err != nil { + return err + } + } else { + tokens.startBodyPayload(c.Request.Method, namespace) + if err := tokens.recursiveEncode(c.Request.Params); err != nil { + return err + } + tokens.endBodyPayload(c.Request.Method) + } //end envelope - tokens.endBody(c.Request.Method) + tokens.endBody() tokens.endEnvelope() + if err := tokens.flush(e); err != nil { + return err + } + + return e.Flush() +} + +func (tokens *tokenData) flush(e *xml.Encoder) error { for _, t := range tokens.data { err := e.EncodeToken(t) if err != nil { return err } } - - return e.Flush() + tokens.data = []xml.Token{} + return nil } type tokenData struct { data []xml.Token + // encoder *xml.Encoder } -func (tokens *tokenData) recursiveEncode(hm interface{}) { +func (tokens *tokenData) recursiveEncode(hm interface{}) error { v := reflect.ValueOf(hm) switch v.Kind() { @@ -83,12 +110,16 @@ func (tokens *tokenData) recursiveEncode(hm interface{}) { } tokens.data = append(tokens.data, t) - tokens.recursiveEncode(v.MapIndex(key).Interface()) + if err := tokens.recursiveEncode(v.MapIndex(key).Interface()); err != nil { + return err + } tokens.data = append(tokens.data, xml.EndElement{Name: t.Name}) } case reflect.Slice: for i := 0; i < v.Len(); i++ { - tokens.recursiveEncode(v.Index(i).Interface()) + if err := tokens.recursiveEncode(v.Index(i).Interface()); err != nil { + return err + } } case reflect.Array: if v.Len() == 2 { @@ -101,7 +132,9 @@ func (tokens *tokenData) recursiveEncode(hm interface{}) { } tokens.data = append(tokens.data, t) - tokens.recursiveEncode(v.Index(1).Interface()) + if err := tokens.recursiveEncode(v.Index(1).Interface()); err != nil { + return err + } tokens.data = append(tokens.data, xml.EndElement{Name: t.Name}) } case reflect.String: @@ -110,6 +143,7 @@ func (tokens *tokenData) recursiveEncode(hm interface{}) { case reflect.Struct: tokens.data = append(tokens.data, v.Interface()) } + return nil } func (tokens *tokenData) startEnvelope() { @@ -130,7 +164,7 @@ func (tokens *tokenData) startEnvelope() { e.Attr = make([]xml.Attr, 0) for local, value := range customEnvelopeAttrs { e.Attr = append(e.Attr, xml.Attr{ - Name: xml.Name{Space: "", Local: local}, + Name: xml.Name{Space: "", Local: local}, Value: value, }) } @@ -174,8 +208,6 @@ func (tokens *tokenData) startHeader(m, n string) { } tokens.data = append(tokens.data, h, r) - - return } func (tokens *tokenData) endHeader(m string) { @@ -202,17 +234,21 @@ func (tokens *tokenData) endHeader(m string) { } func (tokens *tokenData) startBody(m, n string) error { + if m == "" || n == "" { + return fmt.Errorf("method or namespace is empty") + } + b := xml.StartElement{ Name: xml.Name{ Space: "", Local: fmt.Sprintf("%s:Body", soapPrefix), }, } + tokens.data = append(tokens.data, b) + return nil +} - if m == "" || n == "" { - return fmt.Errorf("method or namespace is empty") - } - +func (tokens *tokenData) startBodyPayload(m, n string) { r := xml.StartElement{ Name: xml.Name{ Space: "", @@ -222,27 +258,26 @@ func (tokens *tokenData) startBody(m, n string) error { {Name: xml.Name{Space: "", Local: "xmlns"}, Value: n}, }, } - - tokens.data = append(tokens.data, b, r) - - return nil + tokens.data = append(tokens.data, r) } // endToken close body of the envelope -func (tokens *tokenData) endBody(m string) { - b := xml.EndElement{ +func (tokens *tokenData) endBodyPayload(m string) { + r := xml.EndElement{ Name: xml.Name{ Space: "", - Local: fmt.Sprintf("%s:Body", soapPrefix), + Local: m, }, } + tokens.data = append(tokens.data, r) +} - r := xml.EndElement{ +func (tokens *tokenData) endBody() { + b := xml.EndElement{ Name: xml.Name{ Space: "", - Local: m, + Local: fmt.Sprintf("%s:Body", soapPrefix), }, } - - tokens.data = append(tokens.data, r, b) + tokens.data = append(tokens.data, b) } diff --git a/encode_test.go b/encode_test.go index a370e1c..414aa4c 100644 --- a/encode_test.go +++ b/encode_test.go @@ -2,6 +2,7 @@ package gosoap import ( "encoding/xml" + "fmt" "testing" ) @@ -96,7 +97,7 @@ func TestClient_MarshalXML4(t *testing.T) { func TestSetCustomEnvelope(t *testing.T) { SetCustomEnvelope("soapenv", map[string]string{ "xmlns:soapenv": "http://schemas.xmlsoap.org/soap/envelope/", - "xmlns:tem": "http://tempuri.org/", + "xmlns:tem": "http://tempuri.org/", }) soap, err := SoapClient("http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl", nil) @@ -111,3 +112,52 @@ func TestSetCustomEnvelope(t *testing.T) { } } } + +type checkVatApprox struct { + XMLName xml.Name `xml:"urn:ec.europa.eu:taxud:vies:services:checkVat:types checkVatApprox"` + CountryCode string `xml:"countryCode"` + VatNumber string `xml:"vatNumber"` + TraderName string `xml:"traderName,omitempty"` +} +type checkVatApproxResponse struct { + CountryCode string `xml:"countryCode"` + VatNumber string `xml:"vatNumber"` + Valid bool `xml:"valid"` + TraderName string `xml:"traderName,omitempty"` +} + +func (cva *checkVatApprox) SoapBuildRequest() *Request { + r := NewRequest("checkVatApprox", cva) + // if err!=nil{ + // t.Errorf("error not expected: %s", err) + // } + r.UseXMLEncoder = true + return r +} +func TestClient_MarshalWithEncoder(t *testing.T) { + soap, err := SoapClient("http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl", nil) + if err != nil { + t.Errorf("error not expected: %s", err) + } + + resp, err := soap.CallByStruct(&checkVatApprox{CountryCode: "fr", VatNumber: "67586586"}) + if err != nil { + t.Errorf("error not expected: %s", err) + } + + var cvaResp checkVatApproxResponse + err = resp.Unmarshal(&cvaResp) + if err != nil { + t.Errorf("unmarshal error not expected: %s", err) + } + + fmt.Printf("\n resp: %#v\n", resp) + fmt.Printf(" payload: %s\n", resp.Payload) + fmt.Printf(" body: %s\n", resp.Body) + fmt.Printf(" err: %s\n", err) + fmt.Printf(" unmarshaled: %#v\n", cvaResp) + expectResp:=checkVatApproxResponse{CountryCode:"FR",VatNumber:"67586586",Valid:false,TraderName:"---"} + if cvaResp!= expectResp{ + t.Errorf("got unexpected response: %#v",cvaResp) + } +} diff --git a/request.go b/request.go index e549333..78ae5e0 100644 --- a/request.go +++ b/request.go @@ -6,8 +6,9 @@ import ( // Request Soap Request type Request struct { - Method string - Params SoapParams + Method string + Params SoapParams + UseXMLEncoder bool } func NewRequest(m string, p SoapParams) *Request { diff --git a/soap_test.go b/soap_test.go index 90481b2..fdcde35 100644 --- a/soap_test.go +++ b/soap_test.go @@ -185,14 +185,14 @@ func TestClient_Call(t *testing.T) { } c := &Client{} - res, err = c.Call("", Params{}) + _, err = c.Call("", Params{}) if err == nil { t.Errorf("error expected but nothing got.") } c.SetWSDL("://test.") - res, err = c.Call("checkVat", params) + _, err = c.Call("checkVat", params) if err == nil { t.Errorf("invalid WSDL") } From dd04f9154cad0332c8fc50ef2ceda79ef39595d3 Mon Sep 17 00:00:00 2001 From: Peter Haworth Date: Thu, 13 Jan 2022 16:41:50 +0000 Subject: [PATCH 2/4] Factor bodyContents() out of MarshalXML, ready for automatic detection Tidy up tests --- encode.go | 28 ++++++++++------ encode_test.go | 87 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 79 insertions(+), 36 deletions(-) diff --git a/encode.go b/encode.go index 8080ae8..00982a1 100644 --- a/encode.go +++ b/encode.go @@ -51,6 +51,23 @@ func (c process) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { return err } + err = tokens.bodyContents(c, namespace, e) + if err != nil { + return err + } + + //end envelope + tokens.endBody() + tokens.endEnvelope() + + if err := tokens.flush(e); err != nil { + return err + } + + return e.Flush() +} + +func (tokens *tokenData) bodyContents(c process, namespace string, e *xml.Encoder) error { if c.Request.UseXMLEncoder { if err := tokens.flush(e); err != nil { return err @@ -68,16 +85,7 @@ func (c process) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { } tokens.endBodyPayload(c.Request.Method) } - - //end envelope - tokens.endBody() - tokens.endEnvelope() - - if err := tokens.flush(e); err != nil { - return err - } - - return e.Flush() + return nil } func (tokens *tokenData) flush(e *xml.Encoder) error { diff --git a/encode_test.go b/encode_test.go index 414aa4c..f3b7720 100644 --- a/encode_test.go +++ b/encode_test.go @@ -2,7 +2,6 @@ package gosoap import ( "encoding/xml" - "fmt" "testing" ) @@ -115,7 +114,7 @@ func TestSetCustomEnvelope(t *testing.T) { type checkVatApprox struct { XMLName xml.Name `xml:"urn:ec.europa.eu:taxud:vies:services:checkVat:types checkVatApprox"` - CountryCode string `xml:"countryCode"` + CountryCode string `xml:"countryCode,omitempty"` VatNumber string `xml:"vatNumber"` TraderName string `xml:"traderName,omitempty"` } @@ -126,38 +125,74 @@ type checkVatApproxResponse struct { TraderName string `xml:"traderName,omitempty"` } +var encoderParamsTests = []struct { + Desc string + WSDL string + Params *checkVatApprox + Response *checkVatApproxResponse + Err string +}{ + { + Desc: "Fetch a non-existent VAT number", + WSDL: "http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl", + Params: &checkVatApprox{CountryCode: "fr", VatNumber: "invalid"}, + Response: &checkVatApproxResponse{ + CountryCode: "FR", VatNumber: "invalid", Valid: false, TraderName: "---", + }, + }, + { + Desc: "Fetch a valid VAT number", + WSDL: "http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl", + Params: &checkVatApprox{CountryCode: "fr", VatNumber: "45327920054"}, + Response: &checkVatApproxResponse{ + CountryCode: "FR", VatNumber: "45327920054", Valid: true, TraderName: "SAS EUROMEDIA", + }, + }, + { + Desc: "Fetch with empty params", + WSDL: "http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl", + Params: &checkVatApprox{}, + Err: `[soap:Server]: Invalid_input | Detail: `, + }, +} + func (cva *checkVatApprox) SoapBuildRequest() *Request { r := NewRequest("checkVatApprox", cva) - // if err!=nil{ - // t.Errorf("error not expected: %s", err) - // } r.UseXMLEncoder = true return r } func TestClient_MarshalWithEncoder(t *testing.T) { - soap, err := SoapClient("http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl", nil) - if err != nil { - t.Errorf("error not expected: %s", err) - } + for _, test := range encoderParamsTests { + soap, err := SoapClient(test.WSDL, nil) + if err != nil { + t.Errorf("%s: error not expected creating client: %s", test.Desc, err) + continue + } - resp, err := soap.CallByStruct(&checkVatApprox{CountryCode: "fr", VatNumber: "67586586"}) - if err != nil { - t.Errorf("error not expected: %s", err) - } + resp, err := soap.CallByStruct(test.Params) + if err != nil { + t.Errorf("%s: error not expected calling API: %s", test.Desc, err) + continue + } - var cvaResp checkVatApproxResponse - err = resp.Unmarshal(&cvaResp) - if err != nil { - t.Errorf("unmarshal error not expected: %s", err) - } + var actualResponse checkVatApproxResponse + err = resp.Unmarshal(&actualResponse) + if test.Err != "" { + if err == nil { + t.Errorf("%s: expected error, but got response: %#v", test.Desc, actualResponse) + continue + } else if err.Error() != test.Err { + t.Errorf("%s: error doesn't match expectation: %s", test.Desc, err) + } + } else { + if err != nil { + t.Errorf("%s: unmarshal error not expected: %s", test.Desc, err) + continue + } else if actualResponse != *test.Response { + t.Errorf("%s: response doesn't match expectation: %#v", test.Desc, actualResponse) + continue + } - fmt.Printf("\n resp: %#v\n", resp) - fmt.Printf(" payload: %s\n", resp.Payload) - fmt.Printf(" body: %s\n", resp.Body) - fmt.Printf(" err: %s\n", err) - fmt.Printf(" unmarshaled: %#v\n", cvaResp) - expectResp:=checkVatApproxResponse{CountryCode:"FR",VatNumber:"67586586",Valid:false,TraderName:"---"} - if cvaResp!= expectResp{ - t.Errorf("got unexpected response: %#v",cvaResp) + } } } From 4d5c62b33e438139410579bca402e8e2403c6fd7 Mon Sep 17 00:00:00 2001 From: Peter Haworth Date: Thu, 13 Jan 2022 17:33:18 +0000 Subject: [PATCH 3/4] Remove the need for UseXMLEncoder; just use it for any top-level struct --- encode.go | 10 +++++++++- encode_test.go | 10 +++++----- request.go | 5 ++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/encode.go b/encode.go index 00982a1..2b6d55d 100644 --- a/encode.go +++ b/encode.go @@ -68,7 +68,15 @@ func (c process) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { } func (tokens *tokenData) bodyContents(c process, namespace string, e *xml.Encoder) error { - if c.Request.UseXMLEncoder { + useEncodingXml := false + t := reflect.TypeOf(c.Request.Params) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() == reflect.Struct { + useEncodingXml = true + } + if useEncodingXml { if err := tokens.flush(e); err != nil { return err } diff --git a/encode_test.go b/encode_test.go index f3b7720..f775f33 100644 --- a/encode_test.go +++ b/encode_test.go @@ -125,6 +125,11 @@ type checkVatApproxResponse struct { TraderName string `xml:"traderName,omitempty"` } +func (cva *checkVatApprox) SoapBuildRequest() *Request { + r := NewRequest("checkVatApprox", cva) + return r +} + var encoderParamsTests = []struct { Desc string WSDL string @@ -156,11 +161,6 @@ var encoderParamsTests = []struct { }, } -func (cva *checkVatApprox) SoapBuildRequest() *Request { - r := NewRequest("checkVatApprox", cva) - r.UseXMLEncoder = true - return r -} func TestClient_MarshalWithEncoder(t *testing.T) { for _, test := range encoderParamsTests { soap, err := SoapClient(test.WSDL, nil) diff --git a/request.go b/request.go index 78ae5e0..e549333 100644 --- a/request.go +++ b/request.go @@ -6,9 +6,8 @@ import ( // Request Soap Request type Request struct { - Method string - Params SoapParams - UseXMLEncoder bool + Method string + Params SoapParams } func NewRequest(m string, p SoapParams) *Request { From c1e5974723a4a4c4ed444af78438103aea462915 Mon Sep 17 00:00:00 2001 From: Peter Haworth Date: Thu, 13 Jan 2022 17:58:13 +0000 Subject: [PATCH 4/4] tidy up a bit --- encode.go | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/encode.go b/encode.go index 2b6d55d..fcf712a 100644 --- a/encode.go +++ b/encode.go @@ -46,7 +46,7 @@ func (c process) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { tokens.endHeader(c.Client.HeaderName) } - err := tokens.startBody(c.Request.Method, namespace) + err := tokens.startSoapBody(c.Request.Method, namespace) if err != nil { return err } @@ -57,7 +57,7 @@ func (c process) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { } //end envelope - tokens.endBody() + tokens.endSoapBody() tokens.endEnvelope() if err := tokens.flush(e); err != nil { @@ -68,30 +68,38 @@ func (c process) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { } func (tokens *tokenData) bodyContents(c process, namespace string, e *xml.Encoder) error { - useEncodingXml := false + isStruct := false t := reflect.TypeOf(c.Request.Params) if t.Kind() == reflect.Ptr { t = t.Elem() } if t.Kind() == reflect.Struct { - useEncodingXml = true + isStruct = true } - if useEncodingXml { + if isStruct { + // Just use encoding/xml directly for structs, which allows for much more + // sophisticated control over the XML structure. The top-level element + // is intrinsically part of the encoding, so we don't need to do that + // for ourselves + + // Flush any pending tokens before we send things directly to the encoder if err := tokens.flush(e); err != nil { return err } if err := e.Encode(c.Request.Params); err != nil { return err } - if err := tokens.flush(e); err != nil { - return err - } + // if err := tokens.flush(e); err != nil { + // return err + // } } else { - tokens.startBodyPayload(c.Request.Method, namespace) + // For non-structs, we have to explicitly wrap a top-level element around + // the actual data + tokens.startBodyContents(c.Request.Method, namespace) if err := tokens.recursiveEncode(c.Request.Params); err != nil { return err } - tokens.endBodyPayload(c.Request.Method) + tokens.endBodyContents(c.Request.Method) } return nil } @@ -109,7 +117,6 @@ func (tokens *tokenData) flush(e *xml.Encoder) error { type tokenData struct { data []xml.Token - // encoder *xml.Encoder } func (tokens *tokenData) recursiveEncode(hm interface{}) error { @@ -249,7 +256,7 @@ func (tokens *tokenData) endHeader(m string) { tokens.data = append(tokens.data, r, h) } -func (tokens *tokenData) startBody(m, n string) error { +func (tokens *tokenData) startSoapBody(m, n string) error { if m == "" || n == "" { return fmt.Errorf("method or namespace is empty") } @@ -264,7 +271,7 @@ func (tokens *tokenData) startBody(m, n string) error { return nil } -func (tokens *tokenData) startBodyPayload(m, n string) { +func (tokens *tokenData) startBodyContents(m, n string) { r := xml.StartElement{ Name: xml.Name{ Space: "", @@ -278,7 +285,7 @@ func (tokens *tokenData) startBodyPayload(m, n string) { } // endToken close body of the envelope -func (tokens *tokenData) endBodyPayload(m string) { +func (tokens *tokenData) endBodyContents(m string) { r := xml.EndElement{ Name: xml.Name{ Space: "", @@ -288,7 +295,7 @@ func (tokens *tokenData) endBodyPayload(m string) { tokens.data = append(tokens.data, r) } -func (tokens *tokenData) endBody() { +func (tokens *tokenData) endSoapBody() { b := xml.EndElement{ Name: xml.Name{ Space: "",