├── .github
└── workflows
│ └── go.yml
├── LICENSE
├── README.md
├── client.go
├── client_test.go
├── decoder.go
├── decoder_test.go
├── encoder.go
├── encoder_test.go
├── fixtures
└── cp1251.xml
├── go.mod
├── go.sum
├── request.go
├── response.go
├── response_test.go
└── test_server.rb
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 |
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v3
18 | with:
19 | go-version: 1.18
20 |
21 | - name: Build
22 | run: go build -v ./...
23 |
24 | - name: Test
25 | run: go test -v ./...
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2012 Dmitry Maksimov
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://godoc.org/github.com/kolo/xmlrpc)
2 |
3 | ## Overview
4 |
5 | xmlrpc is an implementation of client side part of XMLRPC protocol in Go language.
6 |
7 | ## Status
8 |
9 | This project is in minimal maintenance mode with no further development. Bug fixes
10 | are accepted, but it might take some time until they will be merged.
11 |
12 | ## Installation
13 |
14 | To install xmlrpc package run `go get github.com/kolo/xmlrpc`. To use
15 | it in application add `"github.com/kolo/xmlrpc"` string to `import`
16 | statement.
17 |
18 | ## Usage
19 |
20 | client, _ := xmlrpc.NewClient("https://bugzilla.mozilla.org/xmlrpc.cgi", nil)
21 | result := struct{
22 | Version string `xmlrpc:"version"`
23 | }{}
24 | client.Call("Bugzilla.version", nil, &result)
25 | fmt.Printf("Version: %s\n", result.Version) // Version: 4.2.7+
26 |
27 | Second argument of NewClient function is an object that implements
28 | [http.RoundTripper](http://golang.org/pkg/net/http/#RoundTripper)
29 | interface, it can be used to get more control over connection options.
30 | By default it initialized by http.DefaultTransport object.
31 |
32 | ### Arguments encoding
33 |
34 | xmlrpc package supports encoding of native Go data types to method
35 | arguments.
36 |
37 | Data types encoding rules:
38 |
39 | * int, int8, int16, int32, int64 encoded to int;
40 | * float32, float64 encoded to double;
41 | * bool encoded to boolean;
42 | * string encoded to string;
43 | * time.Time encoded to datetime.iso8601;
44 | * xmlrpc.Base64 encoded to base64;
45 | * slice encoded to array;
46 |
47 | Structs encoded to struct by following rules:
48 |
49 | * all public field become struct members;
50 | * field name become member name;
51 | * if field has xmlrpc tag, its value become member name.
52 | * for fields tagged with `",omitempty"`, empty values are omitted;
53 | * fields tagged with `"-"` are omitted.
54 |
55 | Server method can accept few arguments, to handle this case there is
56 | special approach to handle slice of empty interfaces (`[]interface{}`).
57 | Each value of such slice encoded as separate argument.
58 |
59 | ### Result decoding
60 |
61 | Result of remote function is decoded to native Go data type.
62 |
63 | Data types decoding rules:
64 |
65 | * int, i4 decoded to int, int8, int16, int32, int64;
66 | * double decoded to float32, float64;
67 | * boolean decoded to bool;
68 | * string decoded to string;
69 | * array decoded to slice;
70 | * structs decoded following the rules described in previous section;
71 | * datetime.iso8601 decoded as time.Time data type;
72 | * base64 decoded to string.
73 |
74 | ## Implementation details
75 |
76 | xmlrpc package contains clientCodec type, that implements [rpc.ClientCodec](http://golang.org/pkg/net/rpc/#ClientCodec)
77 | interface of [net/rpc](http://golang.org/pkg/net/rpc) package.
78 |
79 | xmlrpc package works over HTTP protocol, but some internal functions
80 | and data type were made public to make it easier to create another
81 | implementation of xmlrpc that works over another protocol. To encode
82 | request body there is EncodeMethodCall function. To decode server
83 | response Response data type can be used.
84 |
85 | ## Contribution
86 |
87 | See [project status](#status).
88 |
89 | ## Authors
90 |
91 | Dmitry Maksimov (dmtmax@gmail.com)
92 |
--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "net/http/cookiejar"
9 | "net/rpc"
10 | "net/url"
11 | "sync"
12 | )
13 |
14 | type Client struct {
15 | *rpc.Client
16 | }
17 |
18 | // clientCodec is rpc.ClientCodec interface implementation.
19 | type clientCodec struct {
20 | // url presents url of xmlrpc service
21 | url *url.URL
22 |
23 | // httpClient works with HTTP protocol
24 | httpClient *http.Client
25 |
26 | // cookies stores cookies received on last request
27 | cookies http.CookieJar
28 |
29 | // responses presents map of active requests. It is required to return request id, that
30 | // rpc.Client can mark them as done.
31 | responses map[uint64]*http.Response
32 | mutex sync.Mutex
33 |
34 | response Response
35 |
36 | // ready presents channel, that is used to link request and it`s response.
37 | ready chan uint64
38 |
39 | // close notifies codec is closed.
40 | close chan uint64
41 | }
42 |
43 | func (codec *clientCodec) WriteRequest(request *rpc.Request, args interface{}) (err error) {
44 | httpRequest, err := NewRequest(codec.url.String(), request.ServiceMethod, args)
45 |
46 | if err != nil {
47 | return err
48 | }
49 |
50 | if codec.cookies != nil {
51 | for _, cookie := range codec.cookies.Cookies(codec.url) {
52 | httpRequest.AddCookie(cookie)
53 | }
54 | }
55 |
56 | var httpResponse *http.Response
57 | httpResponse, err = codec.httpClient.Do(httpRequest)
58 |
59 | if err != nil {
60 | return err
61 | }
62 |
63 | if codec.cookies != nil {
64 | codec.cookies.SetCookies(codec.url, httpResponse.Cookies())
65 | }
66 |
67 | codec.mutex.Lock()
68 | codec.responses[request.Seq] = httpResponse
69 | codec.mutex.Unlock()
70 |
71 | codec.ready <- request.Seq
72 |
73 | return nil
74 | }
75 |
76 | func (codec *clientCodec) ReadResponseHeader(response *rpc.Response) (err error) {
77 | var seq uint64
78 | select {
79 | case seq = <-codec.ready:
80 | case <-codec.close:
81 | return errors.New("codec is closed")
82 | }
83 | response.Seq = seq
84 |
85 | codec.mutex.Lock()
86 | httpResponse := codec.responses[seq]
87 | delete(codec.responses, seq)
88 | codec.mutex.Unlock()
89 |
90 | defer httpResponse.Body.Close()
91 |
92 | if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
93 | response.Error = fmt.Sprintf("request error: bad status code - %d", httpResponse.StatusCode)
94 | return nil
95 | }
96 |
97 | body, err := ioutil.ReadAll(httpResponse.Body)
98 | if err != nil {
99 | response.Error = err.Error()
100 | return nil
101 | }
102 |
103 | resp := Response(body)
104 | if err := resp.Err(); err != nil {
105 | response.Error = err.Error()
106 | return nil
107 | }
108 |
109 | codec.response = resp
110 |
111 | return nil
112 | }
113 |
114 | func (codec *clientCodec) ReadResponseBody(v interface{}) (err error) {
115 | if v == nil {
116 | return nil
117 | }
118 | return codec.response.Unmarshal(v)
119 | }
120 |
121 | func (codec *clientCodec) Close() error {
122 | if transport, ok := codec.httpClient.Transport.(*http.Transport); ok {
123 | transport.CloseIdleConnections()
124 | }
125 |
126 | close(codec.close)
127 |
128 | return nil
129 | }
130 |
131 | // NewClient returns instance of rpc.Client object, that is used to send request to xmlrpc service.
132 | func NewClient(requrl string, transport http.RoundTripper) (*Client, error) {
133 | if transport == nil {
134 | transport = http.DefaultTransport
135 | }
136 |
137 | httpClient := &http.Client{Transport: transport}
138 |
139 | jar, err := cookiejar.New(nil)
140 |
141 | if err != nil {
142 | return nil, err
143 | }
144 |
145 | u, err := url.Parse(requrl)
146 |
147 | if err != nil {
148 | return nil, err
149 | }
150 |
151 | codec := clientCodec{
152 | url: u,
153 | httpClient: httpClient,
154 | close: make(chan uint64),
155 | ready: make(chan uint64),
156 | responses: make(map[uint64]*http.Response),
157 | cookies: jar,
158 | }
159 |
160 | return &Client{rpc.NewClientWithCodec(&codec)}, nil
161 | }
162 |
--------------------------------------------------------------------------------
/client_test.go:
--------------------------------------------------------------------------------
1 | // +build integration
2 |
3 | package xmlrpc
4 |
5 | import (
6 | "context"
7 | "io"
8 | "net/http"
9 | "net/http/httptest"
10 | "runtime"
11 | "sync"
12 | "testing"
13 | "time"
14 | )
15 |
16 | func Test_CallWithoutArgs(t *testing.T) {
17 | client := newClient(t)
18 | defer client.Close()
19 |
20 | var result time.Time
21 | if err := client.Call("service.time", nil, &result); err != nil {
22 | t.Fatalf("service.time call error: %v", err)
23 | }
24 | }
25 |
26 | func Test_CallWithOneArg(t *testing.T) {
27 | client := newClient(t)
28 | defer client.Close()
29 |
30 | var result string
31 | if err := client.Call("service.upcase", "xmlrpc", &result); err != nil {
32 | t.Fatalf("service.upcase call error: %v", err)
33 | }
34 |
35 | if result != "XMLRPC" {
36 | t.Fatalf("Unexpected result of service.upcase: %s != %s", "XMLRPC", result)
37 | }
38 | }
39 |
40 | func Test_CallWithTwoArgs(t *testing.T) {
41 | client := newClient(t)
42 | defer client.Close()
43 |
44 | var sum int
45 | if err := client.Call("service.sum", []interface{}{2, 3}, &sum); err != nil {
46 | t.Fatalf("service.sum call error: %v", err)
47 | }
48 |
49 | if sum != 5 {
50 | t.Fatalf("Unexpected result of service.sum: %d != %d", 5, sum)
51 | }
52 | }
53 |
54 | func Test_TwoCalls(t *testing.T) {
55 | client := newClient(t)
56 | defer client.Close()
57 |
58 | var upcase string
59 | if err := client.Call("service.upcase", "xmlrpc", &upcase); err != nil {
60 | t.Fatalf("service.upcase call error: %v", err)
61 | }
62 |
63 | var sum int
64 | if err := client.Call("service.sum", []interface{}{2, 3}, &sum); err != nil {
65 | t.Fatalf("service.sum call error: %v", err)
66 | }
67 |
68 | }
69 |
70 | func Test_FailedCall(t *testing.T) {
71 | client := newClient(t)
72 | defer client.Close()
73 |
74 | var result int
75 | if err := client.Call("service.error", nil, &result); err == nil {
76 | t.Fatal("expected service.error returns error, but it didn't")
77 | }
78 | }
79 |
80 | func Test_ConcurrentCalls(t *testing.T) {
81 | client := newClient(t)
82 |
83 | call := func() {
84 | var result time.Time
85 | client.Call("service.time", nil, &result)
86 | }
87 |
88 | var wg sync.WaitGroup
89 | for i := 0; i < 100; i++ {
90 | wg.Add(1)
91 | go func() {
92 | call()
93 | wg.Done()
94 | }()
95 | }
96 |
97 | wg.Wait()
98 | client.Close()
99 | }
100 |
101 | func Test_CloseMemoryLeak(t *testing.T) {
102 | expected := runtime.NumGoroutine()
103 |
104 | for i := 0; i < 3; i++ {
105 | client := newClient(t)
106 | client.Call("service.time", nil, nil)
107 | client.Close()
108 | }
109 |
110 | var actual int
111 |
112 | // It takes some time to stop running goroutinges. This function checks number of
113 | // running goroutines. It finishes execution if number is same as expected or timeout
114 | // has been reached.
115 | func() {
116 | ctx, cancel := context.WithTimeout(context.Background(), time.Second)
117 | defer cancel()
118 |
119 | for {
120 | select {
121 | case <-ctx.Done():
122 | return
123 | default:
124 | actual = runtime.NumGoroutine()
125 | if actual == expected {
126 | return
127 | }
128 | }
129 | }
130 | }()
131 |
132 | if actual != expected {
133 | t.Errorf("expected number of running goroutines to be %d, but got %d", expected, actual)
134 | }
135 | }
136 |
137 | func Test_BadStatus(t *testing.T) {
138 |
139 | // this is a mock xmlrpc server which sends an invalid status code on the first request
140 | // and an empty methodResponse for all subsequence requests
141 | first := true
142 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
143 | if first {
144 | first = false
145 | http.Error(w, "bad status", http.StatusInternalServerError)
146 | } else {
147 | io.WriteString(w, `
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 | `)
159 | }
160 | }))
161 |
162 | client, err := NewClient(ts.URL, nil)
163 | if err != nil {
164 | t.Fatalf("Can't create client: %v", err)
165 | }
166 | defer client.Close()
167 |
168 | var result interface{}
169 |
170 | // expect an error due to the bad status code
171 | if err := client.Call("method", nil, &result); err == nil {
172 | t.Fatalf("Bad status didn't result in error")
173 | }
174 |
175 | // expect subsequent calls to succeed
176 | if err := client.Call("method", nil, &result); err != nil {
177 | t.Fatalf("Failed to recover after bad status: %v", err)
178 | }
179 | }
180 |
181 | func newClient(t *testing.T) *Client {
182 | client, err := NewClient("http://localhost:5001", nil)
183 | if err != nil {
184 | t.Fatalf("Can't create client: %v", err)
185 | }
186 | return client
187 | }
188 |
--------------------------------------------------------------------------------
/decoder.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "bytes"
5 | "encoding/xml"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "reflect"
10 | "strconv"
11 | "strings"
12 | "time"
13 | )
14 |
15 | const (
16 | iso8601 = "20060102T15:04:05"
17 | iso8601Z = "20060102T15:04:05Z07:00"
18 | iso8601Hyphen = "2006-01-02T15:04:05"
19 | iso8601HyphenZ = "2006-01-02T15:04:05Z07:00"
20 | )
21 |
22 | var (
23 | // CharsetReader is a function to generate reader which converts a non UTF-8
24 | // charset into UTF-8.
25 | CharsetReader func(string, io.Reader) (io.Reader, error)
26 |
27 | timeLayouts = []string{iso8601, iso8601Z, iso8601Hyphen, iso8601HyphenZ}
28 | invalidXmlError = errors.New("invalid xml")
29 | )
30 |
31 | type TypeMismatchError string
32 |
33 | func (e TypeMismatchError) Error() string { return string(e) }
34 |
35 | type decoder struct {
36 | *xml.Decoder
37 | }
38 |
39 | func unmarshal(data []byte, v interface{}) (err error) {
40 | dec := &decoder{xml.NewDecoder(bytes.NewBuffer(data))}
41 |
42 | if CharsetReader != nil {
43 | dec.CharsetReader = CharsetReader
44 | }
45 |
46 | var tok xml.Token
47 | for {
48 | if tok, err = dec.Token(); err != nil {
49 | return err
50 | }
51 |
52 | if t, ok := tok.(xml.StartElement); ok {
53 | if t.Name.Local == "value" {
54 | val := reflect.ValueOf(v)
55 | if val.Kind() != reflect.Ptr {
56 | return errors.New("non-pointer value passed to unmarshal")
57 | }
58 | if err = dec.decodeValue(val.Elem()); err != nil {
59 | return err
60 | }
61 |
62 | break
63 | }
64 | }
65 | }
66 |
67 | // read until end of document
68 | err = dec.Skip()
69 | if err != nil && err != io.EOF {
70 | return err
71 | }
72 |
73 | return nil
74 | }
75 |
76 | func (dec *decoder) decodeValue(val reflect.Value) error {
77 | var tok xml.Token
78 | var err error
79 |
80 | if val.Kind() == reflect.Ptr {
81 | if val.IsNil() {
82 | val.Set(reflect.New(val.Type().Elem()))
83 | }
84 | val = val.Elem()
85 | }
86 |
87 | var typeName string
88 | for {
89 | if tok, err = dec.Token(); err != nil {
90 | return err
91 | }
92 |
93 | if t, ok := tok.(xml.EndElement); ok {
94 | if t.Name.Local == "value" {
95 | return nil
96 | } else {
97 | return invalidXmlError
98 | }
99 | }
100 |
101 | if t, ok := tok.(xml.StartElement); ok {
102 | typeName = t.Name.Local
103 | break
104 | }
105 |
106 | // Treat value data without type identifier as string
107 | if t, ok := tok.(xml.CharData); ok {
108 | if value := strings.TrimSpace(string(t)); value != "" {
109 | if err = checkType(val, reflect.String); err != nil {
110 | return err
111 | }
112 |
113 | val.SetString(value)
114 | return nil
115 | }
116 | }
117 | }
118 |
119 | switch typeName {
120 | case "struct":
121 | ismap := false
122 | pmap := val
123 | valType := val.Type()
124 |
125 | if err = checkType(val, reflect.Struct); err != nil {
126 | if checkType(val, reflect.Map) == nil {
127 | if valType.Key().Kind() != reflect.String {
128 | return fmt.Errorf("only maps with string key type can be unmarshalled")
129 | }
130 | ismap = true
131 | } else if checkType(val, reflect.Interface) == nil && val.IsNil() {
132 | var dummy map[string]interface{}
133 | valType = reflect.TypeOf(dummy)
134 | pmap = reflect.New(valType).Elem()
135 | val.Set(pmap)
136 | ismap = true
137 | } else {
138 | return err
139 | }
140 | }
141 |
142 | var fields map[string]reflect.Value
143 |
144 | if !ismap {
145 | fields = make(map[string]reflect.Value)
146 |
147 | for i := 0; i < valType.NumField(); i++ {
148 | field := valType.Field(i)
149 | fieldVal := val.FieldByName(field.Name)
150 |
151 | if fieldVal.CanSet() {
152 | name := field.Tag.Get("xmlrpc")
153 | name = strings.TrimSuffix(name, ",omitempty")
154 | if name == "-" {
155 | continue
156 | }
157 | if name == "" {
158 | name = field.Name
159 | }
160 | fields[name] = fieldVal
161 | }
162 | }
163 | } else {
164 | // Create initial empty map
165 | pmap.Set(reflect.MakeMap(valType))
166 | }
167 |
168 | // Process struct members.
169 | StructLoop:
170 | for {
171 | if tok, err = dec.Token(); err != nil {
172 | return err
173 | }
174 | switch t := tok.(type) {
175 | case xml.StartElement:
176 | if t.Name.Local != "member" {
177 | return invalidXmlError
178 | }
179 |
180 | tagName, fieldName, err := dec.readTag()
181 | if err != nil {
182 | return err
183 | }
184 | if tagName != "name" {
185 | return invalidXmlError
186 | }
187 |
188 | var fv reflect.Value
189 | ok := true
190 |
191 | if !ismap {
192 | fv, ok = fields[string(fieldName)]
193 | } else {
194 | fv = reflect.New(valType.Elem())
195 | }
196 |
197 | if ok {
198 | for {
199 | if tok, err = dec.Token(); err != nil {
200 | return err
201 | }
202 | if t, ok := tok.(xml.StartElement); ok && t.Name.Local == "value" {
203 | if err = dec.decodeValue(fv); err != nil {
204 | return err
205 | }
206 |
207 | //
208 | if err = dec.Skip(); err != nil {
209 | return err
210 | }
211 |
212 | break
213 | }
214 | }
215 | }
216 |
217 | //
218 | if err = dec.Skip(); err != nil {
219 | return err
220 | }
221 |
222 | if ismap {
223 | pmap.SetMapIndex(reflect.ValueOf(string(fieldName)), reflect.Indirect(fv))
224 | val.Set(pmap)
225 | }
226 | case xml.EndElement:
227 | break StructLoop
228 | }
229 | }
230 | case "array":
231 | slice := val
232 | if checkType(val, reflect.Interface) == nil && val.IsNil() {
233 | slice = reflect.ValueOf([]interface{}{})
234 | } else if err = checkType(val, reflect.Slice); err != nil {
235 | return err
236 | }
237 |
238 | ArrayLoop:
239 | for {
240 | if tok, err = dec.Token(); err != nil {
241 | return err
242 | }
243 |
244 | switch t := tok.(type) {
245 | case xml.StartElement:
246 | var index int
247 | if t.Name.Local != "data" {
248 | return invalidXmlError
249 | }
250 | DataLoop:
251 | for {
252 | if tok, err = dec.Token(); err != nil {
253 | return err
254 | }
255 |
256 | switch tt := tok.(type) {
257 | case xml.StartElement:
258 | if tt.Name.Local != "value" {
259 | return invalidXmlError
260 | }
261 |
262 | if index < slice.Len() {
263 | v := slice.Index(index)
264 | if v.Kind() == reflect.Interface {
265 | v = v.Elem()
266 | }
267 | if v.Kind() != reflect.Ptr {
268 | return errors.New("error: cannot write to non-pointer array element")
269 | }
270 | if err = dec.decodeValue(v); err != nil {
271 | return err
272 | }
273 | } else {
274 | v := reflect.New(slice.Type().Elem())
275 | if err = dec.decodeValue(v); err != nil {
276 | return err
277 | }
278 | slice = reflect.Append(slice, v.Elem())
279 | }
280 |
281 | //
282 | if err = dec.Skip(); err != nil {
283 | return err
284 | }
285 | index++
286 | case xml.EndElement:
287 | val.Set(slice)
288 | break DataLoop
289 | }
290 | }
291 | case xml.EndElement:
292 | break ArrayLoop
293 | }
294 | }
295 | default:
296 | if tok, err = dec.Token(); err != nil {
297 | return err
298 | }
299 |
300 | var data []byte
301 |
302 | switch t := tok.(type) {
303 | case xml.EndElement:
304 | return nil
305 | case xml.CharData:
306 | data = []byte(t.Copy())
307 | default:
308 | return invalidXmlError
309 | }
310 |
311 | switch typeName {
312 | case "int", "i4", "i8":
313 | if checkType(val, reflect.Interface) == nil && val.IsNil() {
314 | i, err := strconv.ParseInt(string(data), 10, 64)
315 | if err != nil {
316 | return err
317 | }
318 |
319 | pi := reflect.New(reflect.TypeOf(i)).Elem()
320 | pi.SetInt(i)
321 | val.Set(pi)
322 | } else if err = checkType(val, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64); err != nil {
323 | return err
324 | } else {
325 | i, err := strconv.ParseInt(string(data), 10, val.Type().Bits())
326 | if err != nil {
327 | return err
328 | }
329 |
330 | val.SetInt(i)
331 | }
332 | case "string", "base64":
333 | str := string(data)
334 | if checkType(val, reflect.Interface) == nil && val.IsNil() {
335 | pstr := reflect.New(reflect.TypeOf(str)).Elem()
336 | pstr.SetString(str)
337 | val.Set(pstr)
338 | } else if err = checkType(val, reflect.String); err != nil {
339 | return err
340 | } else {
341 | val.SetString(str)
342 | }
343 | case "dateTime.iso8601":
344 | var t time.Time
345 | var err error
346 |
347 | for _, layout := range timeLayouts {
348 | t, err = time.Parse(layout, string(data))
349 | if err == nil {
350 | break
351 | }
352 | }
353 | if err != nil {
354 | return err
355 | }
356 |
357 | if checkType(val, reflect.Interface) == nil && val.IsNil() {
358 | ptime := reflect.New(reflect.TypeOf(t)).Elem()
359 | ptime.Set(reflect.ValueOf(t))
360 | val.Set(ptime)
361 | } else if _, ok := val.Interface().(time.Time); !ok {
362 | return TypeMismatchError(fmt.Sprintf("error: type mismatch error - can't decode %v to time", val.Kind()))
363 | } else {
364 | val.Set(reflect.ValueOf(t))
365 | }
366 | case "boolean":
367 | v, err := strconv.ParseBool(string(data))
368 | if err != nil {
369 | return err
370 | }
371 |
372 | if checkType(val, reflect.Interface) == nil && val.IsNil() {
373 | pv := reflect.New(reflect.TypeOf(v)).Elem()
374 | pv.SetBool(v)
375 | val.Set(pv)
376 | } else if err = checkType(val, reflect.Bool); err != nil {
377 | return err
378 | } else {
379 | val.SetBool(v)
380 | }
381 | case "double":
382 | if checkType(val, reflect.Interface) == nil && val.IsNil() {
383 | i, err := strconv.ParseFloat(string(data), 64)
384 | if err != nil {
385 | return err
386 | }
387 |
388 | pdouble := reflect.New(reflect.TypeOf(i)).Elem()
389 | pdouble.SetFloat(i)
390 | val.Set(pdouble)
391 | } else if err = checkType(val, reflect.Float32, reflect.Float64); err != nil {
392 | return err
393 | } else {
394 | i, err := strconv.ParseFloat(string(data), val.Type().Bits())
395 | if err != nil {
396 | return err
397 | }
398 |
399 | val.SetFloat(i)
400 | }
401 | default:
402 | return errors.New("unsupported type")
403 | }
404 |
405 | //
406 | if err = dec.Skip(); err != nil {
407 | return err
408 | }
409 | }
410 |
411 | return nil
412 | }
413 |
414 | func (dec *decoder) readTag() (string, []byte, error) {
415 | var tok xml.Token
416 | var err error
417 |
418 | var name string
419 | for {
420 | if tok, err = dec.Token(); err != nil {
421 | return "", nil, err
422 | }
423 |
424 | if t, ok := tok.(xml.StartElement); ok {
425 | name = t.Name.Local
426 | break
427 | }
428 | }
429 |
430 | value, err := dec.readCharData()
431 | if err != nil {
432 | return "", nil, err
433 | }
434 |
435 | return name, value, dec.Skip()
436 | }
437 |
438 | func (dec *decoder) readCharData() ([]byte, error) {
439 | var tok xml.Token
440 | var err error
441 |
442 | if tok, err = dec.Token(); err != nil {
443 | return nil, err
444 | }
445 |
446 | if t, ok := tok.(xml.CharData); ok {
447 | return []byte(t.Copy()), nil
448 | } else {
449 | return nil, invalidXmlError
450 | }
451 | }
452 |
453 | func checkType(val reflect.Value, kinds ...reflect.Kind) error {
454 | if len(kinds) == 0 {
455 | return nil
456 | }
457 |
458 | if val.Kind() == reflect.Ptr {
459 | val = val.Elem()
460 | }
461 |
462 | match := false
463 |
464 | for _, kind := range kinds {
465 | if val.Kind() == kind {
466 | match = true
467 | break
468 | }
469 | }
470 |
471 | if !match {
472 | return TypeMismatchError(fmt.Sprintf("error: type mismatch - can't unmarshal %v to %v",
473 | val.Kind(), kinds[0]))
474 | }
475 |
476 | return nil
477 | }
478 |
--------------------------------------------------------------------------------
/decoder_test.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "io/ioutil"
7 | "reflect"
8 | "testing"
9 | "time"
10 |
11 | "golang.org/x/text/encoding/charmap"
12 | "golang.org/x/text/transform"
13 | )
14 |
15 | type book struct {
16 | Title string
17 | Amount int
18 | }
19 |
20 | type bookUnexported struct {
21 | title string
22 | amount int
23 | }
24 |
25 | var unmarshalTests = []struct {
26 | value interface{}
27 | ptr interface{}
28 | xml string
29 | }{
30 | // int, i4, i8
31 | {0, new(*int), ""},
32 | {100, new(*int), "100"},
33 | {389451, new(*int), "389451"},
34 | {int64(45659074), new(*int64), "45659074"},
35 |
36 | // string
37 | {"Once upon a time", new(*string), "Once upon a time"},
38 | {"Mike & Mick ", new(*string), "Mike & Mick <London, UK>"},
39 | {"Once upon a time", new(*string), "Once upon a time"},
40 |
41 | // base64
42 | {"T25jZSB1cG9uIGEgdGltZQ==", new(*string), "T25jZSB1cG9uIGEgdGltZQ=="},
43 |
44 | // boolean
45 | {true, new(*bool), "1"},
46 | {false, new(*bool), "0"},
47 |
48 | // double
49 | {12.134, new(*float32), "12.134"},
50 | {-12.134, new(*float32), "-12.134"},
51 |
52 | // datetime.iso8601
53 | {_time("2013-12-09T21:00:12Z"), new(*time.Time), "20131209T21:00:12"},
54 | {_time("2013-12-09T21:00:12Z"), new(*time.Time), "20131209T21:00:12Z"},
55 | {_time("2013-12-09T21:00:12-01:00"), new(*time.Time), "20131209T21:00:12-01:00"},
56 | {_time("2013-12-09T21:00:12+01:00"), new(*time.Time), "20131209T21:00:12+01:00"},
57 | {_time("2013-12-09T21:00:12Z"), new(*time.Time), "2013-12-09T21:00:12"},
58 | {_time("2013-12-09T21:00:12Z"), new(*time.Time), "2013-12-09T21:00:12Z"},
59 | {_time("2013-12-09T21:00:12-01:00"), new(*time.Time), "2013-12-09T21:00:12-01:00"},
60 | {_time("2013-12-09T21:00:12+01:00"), new(*time.Time), "2013-12-09T21:00:12+01:00"},
61 |
62 | // array
63 | {[]int{1, 5, 7}, new(*[]int), "157"},
64 | {[]interface{}{"A", "5"}, new(interface{}), "A5"},
65 | {[]interface{}{"A", int64(5)}, new(interface{}), "A5"},
66 |
67 | // struct
68 | {book{"War and Piece", 20}, new(*book), "TitleWar and PieceAmount20"},
69 | {bookUnexported{}, new(*bookUnexported), "titleWar and Pieceamount20"},
70 | {map[string]interface{}{"Name": "John Smith"}, new(interface{}), "NameJohn Smith"},
71 | {map[string]interface{}{}, new(interface{}), ""},
72 | }
73 |
74 | func _time(s string) time.Time {
75 | t, err := time.Parse(time.RFC3339, s)
76 | if err != nil {
77 | panic(fmt.Sprintf("time parsing error: %v", err))
78 | }
79 | return t
80 | }
81 |
82 | func Test_unmarshal(t *testing.T) {
83 | for _, tt := range unmarshalTests {
84 | v := reflect.New(reflect.TypeOf(tt.value))
85 | if err := unmarshal([]byte(tt.xml), v.Interface()); err != nil {
86 | t.Fatalf("unmarshal error: %v", err)
87 | }
88 |
89 | v = v.Elem()
90 |
91 | if v.Kind() == reflect.Slice {
92 | vv := reflect.ValueOf(tt.value)
93 | if vv.Len() != v.Len() {
94 | t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", tt.value, v.Interface())
95 | }
96 | for i := 0; i < v.Len(); i++ {
97 | if v.Index(i).Interface() != vv.Index(i).Interface() {
98 | t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", tt.value, v.Interface())
99 | }
100 | }
101 | } else {
102 | a1 := v.Interface()
103 | a2 := interface{}(tt.value)
104 |
105 | if !reflect.DeepEqual(a1, a2) {
106 | t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", tt.value, v.Interface())
107 | }
108 | }
109 | }
110 | }
111 |
112 | func Test_unmarshalToNil(t *testing.T) {
113 | for _, tt := range unmarshalTests {
114 | if err := unmarshal([]byte(tt.xml), tt.ptr); err != nil {
115 | t.Fatalf("unmarshal error: %v", err)
116 | }
117 | }
118 | }
119 |
120 | func Test_typeMismatchError(t *testing.T) {
121 | var s string
122 |
123 | encoded := "100"
124 | var err error
125 |
126 | if err = unmarshal([]byte(encoded), &s); err == nil {
127 | t.Fatal("unmarshal error: expected error, but didn't get it")
128 | }
129 |
130 | if _, ok := err.(TypeMismatchError); !ok {
131 | t.Fatal("unmarshal error: expected type mistmatch error, but didn't get it")
132 | }
133 | }
134 |
135 | func Test_unmarshalEmptyValueTag(t *testing.T) {
136 | var v int
137 |
138 | if err := unmarshal([]byte(""), &v); err != nil {
139 | t.Fatalf("unmarshal error: %v", err)
140 | }
141 | }
142 |
143 | const structEmptyXML = `
144 |
145 |
146 |
147 |
148 | `
149 |
150 | func Test_unmarshalEmptyStruct(t *testing.T) {
151 | var v interface{}
152 | if err := unmarshal([]byte(structEmptyXML), &v); err != nil {
153 | t.Fatal(err)
154 | }
155 | if v == nil {
156 | t.Fatalf("got nil map")
157 | }
158 | }
159 |
160 | const arrayValueXML = `
161 |
162 |
163 |
164 | 234
165 | 1
166 | Hello World
167 | Extra Value
168 |
169 |
170 |
171 | `
172 |
173 | func Test_unmarshalExistingArray(t *testing.T) {
174 |
175 | var (
176 | v1 int
177 | v2 bool
178 | v3 string
179 |
180 | v = []interface{}{&v1, &v2, &v3}
181 | )
182 | if err := unmarshal([]byte(arrayValueXML), &v); err != nil {
183 | t.Fatal(err)
184 | }
185 |
186 | // check pre-existing values
187 | if want := 234; v1 != want {
188 | t.Fatalf("want %d, got %d", want, v1)
189 | }
190 | if want := true; v2 != want {
191 | t.Fatalf("want %t, got %t", want, v2)
192 | }
193 | if want := "Hello World"; v3 != want {
194 | t.Fatalf("want %s, got %s", want, v3)
195 | }
196 | // check the appended result
197 | if n := len(v); n != 4 {
198 | t.Fatalf("missing appended result")
199 | }
200 | if got, ok := v[3].(string); !ok || got != "Extra Value" {
201 | t.Fatalf("got %s, want %s", got, "Extra Value")
202 | }
203 | }
204 |
205 | func Test_decodeNonUTF8Response(t *testing.T) {
206 | data, err := ioutil.ReadFile("fixtures/cp1251.xml")
207 | if err != nil {
208 | t.Fatal(err)
209 | }
210 |
211 | CharsetReader = decode
212 |
213 | var s string
214 | if err = unmarshal(data, &s); err != nil {
215 | fmt.Println(err)
216 | t.Fatal("unmarshal error: cannot decode non utf-8 response")
217 | }
218 |
219 | expected := "Л.Н. Толстой - Война и Мир"
220 |
221 | if s != expected {
222 | t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", expected, s)
223 | }
224 |
225 | CharsetReader = nil
226 | }
227 |
228 | func decode(charset string, input io.Reader) (io.Reader, error) {
229 | if charset != "cp1251" {
230 | return nil, fmt.Errorf("unsupported charset")
231 | }
232 |
233 | return transform.NewReader(input, charmap.Windows1251.NewDecoder()), nil
234 | }
235 |
--------------------------------------------------------------------------------
/encoder.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "bytes"
5 | "encoding/xml"
6 | "fmt"
7 | "reflect"
8 | "sort"
9 | "strconv"
10 | "strings"
11 | "time"
12 | )
13 |
14 | // Base64 represents value in base64 encoding
15 | type Base64 string
16 |
17 | type encodeFunc func(reflect.Value) ([]byte, error)
18 |
19 | func marshal(v interface{}) ([]byte, error) {
20 | if v == nil {
21 | return []byte{}, nil
22 | }
23 |
24 | val := reflect.ValueOf(v)
25 | return encodeValue(val)
26 | }
27 |
28 | func encodeValue(val reflect.Value) ([]byte, error) {
29 | var b []byte
30 | var err error
31 |
32 | if val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface {
33 | if val.IsNil() {
34 | return []byte(""), nil
35 | }
36 |
37 | val = val.Elem()
38 | }
39 |
40 | switch val.Kind() {
41 | case reflect.Struct:
42 | switch val.Interface().(type) {
43 | case time.Time:
44 | t := val.Interface().(time.Time)
45 | b = []byte(fmt.Sprintf("%s", t.Format(iso8601)))
46 | default:
47 | b, err = encodeStruct(val)
48 | }
49 | case reflect.Map:
50 | b, err = encodeMap(val)
51 | case reflect.Slice:
52 | b, err = encodeSlice(val)
53 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
54 | b = []byte(fmt.Sprintf("%s", strconv.FormatInt(val.Int(), 10)))
55 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
56 | b = []byte(fmt.Sprintf("%s", strconv.FormatUint(val.Uint(), 10)))
57 | case reflect.Float32, reflect.Float64:
58 | b = []byte(fmt.Sprintf("%s",
59 | strconv.FormatFloat(val.Float(), 'f', -1, val.Type().Bits())))
60 | case reflect.Bool:
61 | if val.Bool() {
62 | b = []byte("1")
63 | } else {
64 | b = []byte("0")
65 | }
66 | case reflect.String:
67 | var buf bytes.Buffer
68 |
69 | xml.Escape(&buf, []byte(val.String()))
70 |
71 | if _, ok := val.Interface().(Base64); ok {
72 | b = []byte(fmt.Sprintf("%s", buf.String()))
73 | } else {
74 | b = []byte(fmt.Sprintf("%s", buf.String()))
75 | }
76 | default:
77 | return nil, fmt.Errorf("xmlrpc encode error: unsupported type")
78 | }
79 |
80 | if err != nil {
81 | return nil, err
82 | }
83 |
84 | return []byte(fmt.Sprintf("%s", string(b))), nil
85 | }
86 |
87 | func encodeStruct(structVal reflect.Value) ([]byte, error) {
88 | var b bytes.Buffer
89 |
90 | b.WriteString("")
91 |
92 | structType := structVal.Type()
93 | for i := 0; i < structType.NumField(); i++ {
94 | fieldVal := structVal.Field(i)
95 | fieldType := structType.Field(i)
96 |
97 | name := fieldType.Tag.Get("xmlrpc")
98 | // skip ignored fields.
99 | if name == "-" {
100 | continue
101 | }
102 | // if the tag has the omitempty property, skip it
103 | if strings.HasSuffix(name, ",omitempty") && fieldVal.IsZero() {
104 | continue
105 | }
106 | name = strings.TrimSuffix(name, ",omitempty")
107 | if name == "" {
108 | name = fieldType.Name
109 | }
110 |
111 | p, err := encodeValue(fieldVal)
112 | if err != nil {
113 | return nil, err
114 | }
115 |
116 | b.WriteString("")
117 | b.WriteString(fmt.Sprintf("%s", name))
118 | b.Write(p)
119 | b.WriteString("")
120 | }
121 |
122 | b.WriteString("")
123 |
124 | return b.Bytes(), nil
125 | }
126 |
127 | var sortMapKeys bool
128 |
129 | func encodeMap(val reflect.Value) ([]byte, error) {
130 | var t = val.Type()
131 |
132 | if t.Key().Kind() != reflect.String {
133 | return nil, fmt.Errorf("xmlrpc encode error: only maps with string keys are supported")
134 | }
135 |
136 | var b bytes.Buffer
137 |
138 | b.WriteString("")
139 |
140 | keys := val.MapKeys()
141 |
142 | if sortMapKeys {
143 | sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
144 | }
145 |
146 | for i := 0; i < val.Len(); i++ {
147 | key := keys[i]
148 | kval := val.MapIndex(key)
149 |
150 | b.WriteString("")
151 | b.WriteString(fmt.Sprintf("%s", key.String()))
152 |
153 | p, err := encodeValue(kval)
154 |
155 | if err != nil {
156 | return nil, err
157 | }
158 |
159 | b.Write(p)
160 | b.WriteString("")
161 | }
162 |
163 | b.WriteString("")
164 |
165 | return b.Bytes(), nil
166 | }
167 |
168 | func encodeSlice(val reflect.Value) ([]byte, error) {
169 | var b bytes.Buffer
170 |
171 | b.WriteString("")
172 |
173 | for i := 0; i < val.Len(); i++ {
174 | p, err := encodeValue(val.Index(i))
175 | if err != nil {
176 | return nil, err
177 | }
178 |
179 | b.Write(p)
180 | }
181 |
182 | b.WriteString("")
183 |
184 | return b.Bytes(), nil
185 | }
186 |
--------------------------------------------------------------------------------
/encoder_test.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | var marshalTests = []struct {
9 | value interface{}
10 | xml string
11 | }{
12 | {100, "100"},
13 | {"Once upon a time", "Once upon a time"},
14 | {"Mike & Mick ", "Mike & Mick <London, UK>"},
15 | {Base64("T25jZSB1cG9uIGEgdGltZQ=="), "T25jZSB1cG9uIGEgdGltZQ=="},
16 | {true, "1"},
17 | {false, "0"},
18 | {12.134, "12.134"},
19 | {-12.134, "-12.134"},
20 | {738777323.0, "738777323"},
21 | {time.Unix(1386622812, 0).UTC(), "20131209T21:00:12"},
22 | {[]interface{}{1, "one"}, "1one"},
23 | {&struct {
24 | Title string
25 | Amount int
26 | }{"War and Piece", 20}, "TitleWar and PieceAmount20"},
27 | {&struct {
28 | Value interface{} `xmlrpc:"value"`
29 | }{}, "value"},
30 | {
31 | map[string]interface{}{"title": "War and Piece", "amount": 20},
32 | "amount20titleWar and Piece",
33 | },
34 | {
35 | map[string]interface{}{
36 | "Name": "John Smith",
37 | "Age": 6,
38 | "Wight": []float32{66.67, 100.5},
39 | "Dates": map[string]interface{}{"Birth": time.Date(1829, time.November, 10, 23, 0, 0, 0, time.UTC), "Death": time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)}},
40 | "Age6DatesBirth18291110T23:00:00Death20091110T23:00:00NameJohn SmithWight66.67100.5",
41 | },
42 | {&struct {
43 | Title string
44 | Amount int
45 | Author string `xmlrpc:"author,omitempty"`
46 | }{
47 | Title: "War and Piece", Amount: 20,
48 | }, "TitleWar and PieceAmount20"},
49 | {&struct {
50 | Title string
51 | Amount int
52 | Author string `xmlrpc:"author,omitempty"`
53 | }{
54 | Title: "War and Piece", Amount: 20, Author: "Leo Tolstoy",
55 | }, "TitleWar and PieceAmount20authorLeo Tolstoy"},
56 | {&struct {
57 | }{}, ""},
58 | {&struct {
59 | ID int `xmlrpc:"id"`
60 | Name string `xmlrpc:"-"`
61 | }{
62 | ID: 123, Name: "kolo",
63 | }, "id123"},
64 | }
65 |
66 | func Test_marshal(t *testing.T) {
67 | sortMapKeys = true
68 |
69 | for _, tt := range marshalTests {
70 | b, err := marshal(tt.value)
71 | if err != nil {
72 | t.Fatalf("unexpected marshal error: %v", err)
73 | }
74 |
75 | if string(b) != tt.xml {
76 | t.Fatalf("marshal error:\nexpected: %s\n got: %s", tt.xml, string(b))
77 | }
78 |
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/fixtures/cp1251.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kolo/xmlrpc/a4b6fa1dd06bbefa509944742c219846044ed934/fixtures/cp1251.xml
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/kolo/xmlrpc
2 |
3 | go 1.14
4 |
5 | require golang.org/x/text v0.3.3
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
2 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
3 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
4 |
--------------------------------------------------------------------------------
/request.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 | )
8 |
9 | func NewRequest(url string, method string, args interface{}) (*http.Request, error) {
10 | var t []interface{}
11 | var ok bool
12 | if t, ok = args.([]interface{}); !ok {
13 | if args != nil {
14 | t = []interface{}{args}
15 | }
16 | }
17 |
18 | body, err := EncodeMethodCall(method, t...)
19 | if err != nil {
20 | return nil, err
21 | }
22 |
23 | request, err := http.NewRequest("POST", url, bytes.NewReader(body))
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | request.Header.Set("Content-Type", "text/xml")
29 | request.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
30 |
31 | return request, nil
32 | }
33 |
34 | func EncodeMethodCall(method string, args ...interface{}) ([]byte, error) {
35 | var b bytes.Buffer
36 | b.WriteString(``)
37 | b.WriteString(fmt.Sprintf("%s", method))
38 |
39 | if args != nil {
40 | b.WriteString("")
41 |
42 | for _, arg := range args {
43 | p, err := marshal(arg)
44 | if err != nil {
45 | return nil, err
46 | }
47 |
48 | b.WriteString(fmt.Sprintf("%s", string(p)))
49 | }
50 |
51 | b.WriteString("")
52 | }
53 |
54 | b.WriteString("")
55 |
56 | return b.Bytes(), nil
57 | }
58 |
--------------------------------------------------------------------------------
/response.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | )
7 |
8 | var (
9 | faultRx = regexp.MustCompile(`(\s|\S)+`)
10 | )
11 |
12 | // FaultError is returned from the server when an invalid call is made
13 | type FaultError struct {
14 | Code int `xmlrpc:"faultCode"`
15 | String string `xmlrpc:"faultString"`
16 | }
17 |
18 | // Error implements the error interface
19 | func (e FaultError) Error() string {
20 | return fmt.Sprintf("Fault(%d): %s", e.Code, e.String)
21 | }
22 |
23 | type Response []byte
24 |
25 | func (r Response) Err() error {
26 | if !faultRx.Match(r) {
27 | return nil
28 | }
29 | var fault FaultError
30 | if err := unmarshal(r, &fault); err != nil {
31 | return err
32 | }
33 | return fault
34 | }
35 |
36 | func (r Response) Unmarshal(v interface{}) error {
37 | if err := unmarshal(r, v); err != nil {
38 | return err
39 | }
40 |
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/response_test.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | const faultRespXml = `
8 |
9 |
10 |
11 |
12 |
13 |
14 | faultString
15 |
16 | You must log in before using this part of Bugzilla.
17 |
18 |
19 |
20 | faultCode
21 |
22 | 410
23 |
24 |
25 |
26 |
27 |
28 | `
29 |
30 | func Test_failedResponse(t *testing.T) {
31 | resp := Response([]byte(faultRespXml))
32 |
33 | if resp.Err() == nil {
34 | t.Fatal("Err() error: expected error, got nil")
35 | }
36 |
37 | fault := resp.Err().(FaultError)
38 | if fault.Code != 410 && fault.String != "You must log in before using this part of Bugzilla." {
39 | t.Fatal("Err() error: got wrong error")
40 | }
41 | }
42 |
43 | const emptyValResp = `
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | user
52 | Joe Smith
53 |
54 |
55 | token
56 |
57 |
58 |
59 |
60 |
61 |
62 | `
63 |
64 | func Test_responseWithEmptyValue(t *testing.T) {
65 | resp := Response([]byte(emptyValResp))
66 |
67 | result := struct {
68 | User string `xmlrpc:"user"`
69 | Token string `xmlrpc:"token"`
70 | }{}
71 |
72 | if err := resp.Unmarshal(&result); err != nil {
73 | t.Fatalf("unmarshal error: %v", err)
74 | }
75 |
76 | if result.User != "Joe Smith" || result.Token != "" {
77 | t.Fatalf("unexpected result: %v", result)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/test_server.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | require "xmlrpc/server"
4 |
5 | class Service
6 | def time
7 | Time.now
8 | end
9 |
10 | def upcase(s)
11 | s.upcase
12 | end
13 |
14 | def sum(x, y)
15 | x + y
16 | end
17 |
18 | def error
19 | raise XMLRPC::FaultException.new(500, "Server error")
20 | end
21 | end
22 |
23 | server = XMLRPC::Server.new 5001, 'localhost'
24 | server.add_handler "service", Service.new
25 | server.serve
26 |
--------------------------------------------------------------------------------