├── .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 | [![GoDoc](https://godoc.org/github.com/kolo/xmlrpc?status.svg)](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 | --------------------------------------------------------------------------------