├── .gitignore
├── LICENSE
├── README.md
├── examples
├── README.md
├── example_client.go
└── example_server.go
└── xml
├── client.go
├── doc.go
├── fault.go
├── fault_test.go
├── rpc2xml.go
├── rpc2xml_test.go
├── server.go
├── xml2rpc.go
├── xml2rpc_test.go
└── xml_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | .*.swp
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, Ivan Daniluk
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice, this
11 | list of conditions and the following disclaimer in the documentation and/or
12 | other materials provided with the distribution.
13 |
14 | * Neither the name of the {organization} nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gorilla-xmlrpc #
2 |
3 | [](https://godoc.org/github.com/divan/gorilla-xmlrpc/xml)
4 |
5 | XML-RPC implementation for the Gorilla/RPC toolkit.
6 |
7 | It implements both server and client.
8 |
9 | It's built on top of gorilla/rpc package in Go(Golang) language and implements XML-RPC, according to [it's specification](http://xmlrpc.scripting.com/spec.html).
10 | Unlike net/rpc from Go strlib, gorilla/rpc allows usage of HTTP POST requests for RPC.
11 |
12 | ### Installation ###
13 | Assuming you already imported gorilla/rpc, use the following command:
14 |
15 | go get github.com/divan/gorilla-xmlrpc/xml
16 |
17 | ### Examples ###
18 |
19 | #### Server Example ####
20 |
21 | ```go
22 | package main
23 |
24 | import (
25 | "log"
26 | "net/http"
27 | "github.com/gorilla/rpc"
28 | "github.com/divan/gorilla-xmlrpc/xml"
29 | )
30 |
31 | type HelloService struct{}
32 |
33 | func (h *HelloService) Say(r *http.Request, args *struct{Who string}, reply *struct{Message string}) error {
34 | log.Println("Say", args.Who)
35 | reply.Message = "Hello, " + args.Who + "!"
36 | return nil
37 | }
38 |
39 | func main() {
40 | RPC := rpc.NewServer()
41 | xmlrpcCodec := xml.NewCodec()
42 | RPC.RegisterCodec(xmlrpcCodec, "text/xml")
43 | RPC.RegisterService(new(HelloService), "")
44 | http.Handle("/RPC2", RPC)
45 |
46 | log.Println("Starting XML-RPC server on localhost:1234/RPC2")
47 | log.Fatal(http.ListenAndServe(":1234", nil))
48 | }
49 | ```
50 |
51 | It's pretty self-explanatory and can be tested with any xmlrpc client, even raw curl request:
52 |
53 | ```bash
54 | curl -v -X POST -H "Content-Type: text/xml" -d 'HelloService.SayUser 1' http://localhost:1234/RPC2
55 | ```
56 |
57 | #### Client Example ####
58 |
59 | Implementing client is beyond the scope of this package, but with encoding/decoding handlers it should be pretty trivial. Here is an example which works with the server introduced above.
60 |
61 | ```go
62 | package main
63 |
64 | import (
65 | "log"
66 | "bytes"
67 | "net/http"
68 | "github.com/divan/gorilla-xmlrpc/xml"
69 | )
70 |
71 | func XmlRpcCall(method string, args struct{Who string}) (reply struct{Message string}, err error) {
72 | buf, _ := xml.EncodeClientRequest(method, &args)
73 |
74 | resp, err := http.Post("http://localhost:1234/RPC2", "text/xml", bytes.NewBuffer(buf))
75 | if err != nil {
76 | return
77 | }
78 | defer resp.Body.Close()
79 |
80 | err = xml.DecodeClientResponse(resp.Body, &reply)
81 | return
82 | }
83 |
84 | func main() {
85 | reply, err := XmlRpcCall("HelloService.Say", struct{Who string}{"User 1"})
86 | if err != nil {
87 | log.Fatal(err)
88 | }
89 |
90 | log.Printf("Response: %s\n", reply.Message)
91 | }
92 |
93 | ```
94 |
95 | ### Implementation details ###
96 |
97 | The main objective was to use standard encoding/xml package for XML marshalling/unmarshalling. Unfortunately, in current implementation there is no graceful way to implement common structre for marshal and unmarshal functions - marshalling doesn't handle interface{} types so far (though, it could be changed in the future).
98 | So, marshalling is implemented manually.
99 |
100 | Unmarshalling code first creates temporary structure for unmarshalling XML into, then converts it into the passed variable using *reflect* package.
101 | If XML struct member's name is lowercased, it's first letter will be uppercased, as in Go/Gorilla field name must be exported(first-letter uppercased).
102 |
103 | Marshalling code converts rpc directly to the string XML representation.
104 |
105 | For the better understanding, I use terms 'rpc2xml' and 'xml2rpc' instead of 'marshal' and 'unmarshall'.
106 |
107 | ### Supported types ###
108 |
109 | | XML-RPC | Golang |
110 | | ---------------- | ------------- |
111 | | int, i4 | int |
112 | | double | float64 |
113 | | boolean | bool |
114 | | string | string |
115 | | dateTime.iso8601 | time.Time |
116 | | base64 | []byte |
117 | | struct | struct |
118 | | array | []interface{} |
119 | | nil | nil |
120 |
121 | ### TODO ###
122 |
123 | * Add more corner cases tests
124 |
125 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | ## Examples
2 |
3 | You can test server and client implementations by running following code.
4 |
5 | In console:
6 | ```bash
7 | go run example_server.go
8 | ```
9 |
10 | In another console:
11 | ```bash
12 | go run example_client.go
13 | ```
14 |
--------------------------------------------------------------------------------
/examples/example_client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "bytes"
6 | "net/http"
7 | "github.com/divan/gorilla-xmlrpc/xml"
8 | )
9 |
10 | func XmlRpcCall(method string, args struct{Who string}) (reply struct{Message string}, err error) {
11 | buf, _ := xml.EncodeClientRequest(method, &args)
12 |
13 | resp, err := http.Post("http://localhost:1234/RPC2", "text/xml", bytes.NewBuffer(buf))
14 | if err != nil {
15 | return
16 | }
17 | defer resp.Body.Close()
18 |
19 | err = xml.DecodeClientResponse(resp.Body, &reply)
20 | return
21 | }
22 |
23 | func main() {
24 | reply, err := XmlRpcCall("HelloService.Say", struct{Who string}{"User 1"})
25 | if err != nil {
26 | log.Fatal(err)
27 | }
28 |
29 | log.Printf("Response: %s\n", reply.Message)
30 | }
--------------------------------------------------------------------------------
/examples/example_server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "github.com/gorilla/rpc"
7 | "github.com/divan/gorilla-xmlrpc/xml"
8 | )
9 |
10 | type HelloService struct{}
11 |
12 | func (h *HelloService) Say(r *http.Request, args *struct{Who string}, reply *struct{Message string}) error {
13 | log.Println("Say", args.Who)
14 | reply.Message = "Hello, " + args.Who + "!"
15 | return nil
16 | }
17 |
18 | func main() {
19 | RPC := rpc.NewServer()
20 | xmlrpcCodec := xml.NewCodec()
21 | RPC.RegisterCodec(xmlrpcCodec, "text/xml")
22 | RPC.RegisterService(new(HelloService), "")
23 | http.Handle("/RPC2", RPC)
24 |
25 | log.Println("Starting XML-RPC server on localhost:1234/RPC2")
26 | log.Fatal(http.ListenAndServe(":1234", nil))
27 | }
--------------------------------------------------------------------------------
/xml/client.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Ivan Danyliuk
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package xml
6 |
7 | import (
8 | "io"
9 | "io/ioutil"
10 | )
11 |
12 | // EncodeClientRequest encodes parameters for a XML-RPC client request.
13 | func EncodeClientRequest(method string, args interface{}) ([]byte, error) {
14 | xml, err := rpcRequest2XML(method, args)
15 | return []byte(xml), err
16 | }
17 |
18 | // DecodeClientResponse decodes the response body of a client request into
19 | // the interface reply.
20 | func DecodeClientResponse(r io.Reader, reply interface{}) error {
21 | rawxml, err := ioutil.ReadAll(r)
22 | if err != nil {
23 | return FaultSystemError
24 | }
25 | return xml2RPC(string(rawxml), reply)
26 | }
27 |
--------------------------------------------------------------------------------
/xml/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | XML-RPC implementation for the Gorilla/RPC toolkit.
3 |
4 | It's built on top of gorilla/rpc package in Go(Golang) language and implements XML-RPC, according to it's specification. Unlike net/rpc from Go strlib, gorilla/rpc allows usage of HTTP POST requests for RPC.
5 |
6 | XML-RPC spec: http://xmlrpc.scripting.com/spec.html
7 |
8 | Installation
9 |
10 | Assuming you already imported gorilla/rpc, use the following command:
11 |
12 | go get github.com/divan/gorilla-xmlrpc/xml
13 |
14 | Implementation details
15 |
16 | The main objective was to use standard encoding/xml package for XML marshalling/unmarshalling. Unfortunately, in current implementation there is no graceful way to implement common structre for marshal and unmarshal functions - marshalling doesn't handle interface{} types so far (though, it could be changed in the future). So, marshalling is implemented manually.
17 |
18 | Unmarshalling code first creates temporary structure for unmarshalling XML into, then converts it into the passed variable using reflect package. If XML struct member's name is lowercased, it's first letter will be uppercased, as in Go/Gorilla field name must be exported(first-letter uppercased).
19 |
20 | Marshalling code converts rpc directly to the string XML representation.
21 |
22 | For the better understanding, I use terms 'rpc2xml' and 'xml2rpc' instead of 'marshal' and 'unmarshall'.
23 |
24 | Types
25 |
26 | The following types are supported:
27 |
28 | XML-RPC Golang
29 | ------- ------
30 | int, i4 int
31 | double float64
32 | boolean bool
33 | stringi string
34 | dateTime.iso8601 time.Time
35 | base64 []byte
36 | struct struct
37 | array []interface{}
38 | nil nil
39 |
40 | TODO
41 |
42 | TODO list:
43 | * Add more corner cases tests
44 |
45 | Examples
46 |
47 | Checkout examples in examples/ directory.
48 |
49 | */
50 | package xml
51 |
--------------------------------------------------------------------------------
/xml/fault.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Ivan Danyliuk
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package xml
6 |
7 | import (
8 | "fmt"
9 | )
10 |
11 | // Default Faults
12 | // NOTE: XMLRPC spec doesn't specify any Fault codes.
13 | // These codes seems to be widely accepted, and taken from the http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
14 | var (
15 | FaultInvalidParams = Fault{Code: -32602, String: "Invalid Method Parameters"}
16 | FaultWrongArgumentsNumber = Fault{Code: -32602, String: "Wrong Arguments Number"}
17 | FaultInternalError = Fault{Code: -32603, String: "Internal Server Error"}
18 | FaultApplicationError = Fault{Code: -32500, String: "Application Error"}
19 | FaultSystemError = Fault{Code: -32400, String: "System Error"}
20 | FaultDecode = Fault{Code: -32700, String: "Parsing error: not well formed"}
21 | )
22 |
23 | // Fault represents XML-RPC Fault.
24 | type Fault struct {
25 | Code int `xml:"faultCode"`
26 | String string `xml:"faultString"`
27 | }
28 |
29 | // Error satisifies error interface for Fault.
30 | func (f Fault) Error() string {
31 | return fmt.Sprintf("%d: %s", f.Code, f.String)
32 | }
33 |
34 | // Fault2XML is a quick 'marshalling' replacemnt for the Fault case.
35 | func fault2XML(fault Fault) string {
36 | buffer := ""
37 | xml, _ := rpc2XML(fault)
38 | buffer += xml
39 | buffer += ""
40 | return buffer
41 | }
42 |
43 | type faultValue struct {
44 | Value value `xml:"value"`
45 | }
46 |
47 | // IsEmpty returns true if faultValue contain fault.
48 | //
49 | // faultValue should be a struct with 2 members.
50 | func (f faultValue) IsEmpty() bool {
51 | return len(f.Value.Struct) == 0
52 | }
53 |
--------------------------------------------------------------------------------
/xml/fault_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Ivan Danyliuk
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package xml
6 |
7 | import (
8 | "net/http"
9 | "strings"
10 | "testing"
11 |
12 | "github.com/gorilla/rpc"
13 | )
14 |
15 | //////////////////////////////////
16 | // Service 1
17 | //////////////////////////////////
18 | type FaultTestRequest struct {
19 | A int
20 | B int
21 | }
22 |
23 | type FaultTestBadRequest struct {
24 | A int
25 | B int
26 | C int
27 | }
28 |
29 | type FaultTestResponse struct {
30 | Result int
31 | }
32 |
33 | type FaultTestBadResponse struct {
34 | Result string
35 | }
36 |
37 | type FaultTest struct {
38 | }
39 |
40 | func (t *FaultTest) Multiply(r *http.Request, req *FaultTestRequest, res *FaultTestResponse) error {
41 | res.Result = req.A * req.B
42 | return nil
43 | }
44 |
45 | func TestFaults(t *testing.T) {
46 | s := rpc.NewServer()
47 | s.RegisterCodec(NewCodec(), "text/xml")
48 | s.RegisterService(new(FaultTest), "")
49 |
50 | var err error
51 |
52 | var res1 FaultTestResponse
53 | err = execute(t, s, "FaultTest.Multiply", &FaultTestBadRequest{4, 2, 4}, &res1)
54 | if err == nil {
55 | t.Fatal("expected err to be not nil, but got:", err)
56 | }
57 | fault, ok := err.(Fault)
58 | if !ok {
59 | t.Fatal("expected error to be of concrete type Fault, but got", err)
60 | }
61 | if fault.Code != -32602 {
62 | t.Errorf("wrong fault code: %d", fault.Code)
63 | }
64 | if fault.String != "Wrong Arguments Number" {
65 | t.Errorf("wrong fault string: %s", fault.String)
66 | }
67 |
68 | var res2 FaultTestBadResponse
69 | err = execute(t, s, "FaultTest.Multiply", &FaultTestRequest{4, 2}, &res2)
70 | if err == nil {
71 | t.Fatal("expected err to be not nil, but got:", err)
72 | }
73 | fault, ok = err.(Fault)
74 | if !ok {
75 | t.Fatal("expected error to be of concrete type Fault, but got", err)
76 | }
77 | if fault.Code != -32602 {
78 | t.Errorf("wrong fault code: %d", fault.Code)
79 | }
80 |
81 | if !strings.HasPrefix(fault.String, "Invalid Method Parameters: fields type mismatch") {
82 | t.Errorf("wrong response: %s", fault.String)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/xml/rpc2xml.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Ivan Danyliuk
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package xml
6 |
7 | import (
8 | "encoding/base64"
9 | "fmt"
10 | "reflect"
11 | "strings"
12 | "time"
13 | )
14 |
15 | func rpcRequest2XML(method string, rpc interface{}) (string, error) {
16 | buffer := ""
17 | buffer += method
18 | buffer += ""
19 | params, err := rpcParams2XML(rpc)
20 | buffer += params
21 | buffer += ""
22 | return buffer, err
23 | }
24 |
25 | func rpcResponse2XML(rpc interface{}) (string, error) {
26 | buffer := ""
27 | params, err := rpcParams2XML(rpc)
28 | buffer += params
29 | buffer += ""
30 | return buffer, err
31 | }
32 |
33 | func rpcParams2XML(rpc interface{}) (string, error) {
34 | var err error
35 | buffer := ""
36 | for i := 0; i < reflect.ValueOf(rpc).Elem().NumField(); i++ {
37 | var xml string
38 | buffer += ""
39 | xml, err = rpc2XML(reflect.ValueOf(rpc).Elem().Field(i).Interface())
40 | buffer += xml
41 | buffer += ""
42 | }
43 | buffer += ""
44 | return buffer, err
45 | }
46 |
47 | func rpc2XML(value interface{}) (string, error) {
48 | out := ""
49 | switch reflect.ValueOf(value).Kind() {
50 | case reflect.Int:
51 | out += fmt.Sprintf("%d", value.(int))
52 | case reflect.Float64:
53 | out += fmt.Sprintf("%f", value.(float64))
54 | case reflect.String:
55 | out += string2XML(value.(string))
56 | case reflect.Bool:
57 | out += bool2XML(value.(bool))
58 | case reflect.Struct:
59 | if reflect.TypeOf(value).String() != "time.Time" {
60 | out += struct2XML(value)
61 | } else {
62 | out += time2XML(value.(time.Time))
63 | }
64 | case reflect.Slice, reflect.Array:
65 | // FIXME: is it the best way to recognize '[]byte'?
66 | if reflect.TypeOf(value).String() != "[]uint8" {
67 | out += array2XML(value)
68 | } else {
69 | out += base642XML(value.([]byte))
70 | }
71 | case reflect.Ptr:
72 | if reflect.ValueOf(value).IsNil() {
73 | out += ""
74 | }
75 | }
76 | out += ""
77 | return out, nil
78 | }
79 |
80 | func bool2XML(value bool) string {
81 | var b string
82 | if value {
83 | b = "1"
84 | } else {
85 | b = "0"
86 | }
87 | return fmt.Sprintf("%s", b)
88 | }
89 |
90 | func string2XML(value string) string {
91 | value = strings.Replace(value, "&", "&", -1)
92 | value = strings.Replace(value, "\"", """, -1)
93 | value = strings.Replace(value, "<", "<", -1)
94 | value = strings.Replace(value, ">", ">", -1)
95 | return fmt.Sprintf("%s", value)
96 | }
97 |
98 | func struct2XML(value interface{}) (out string) {
99 | out += ""
100 | for i := 0; i < reflect.TypeOf(value).NumField(); i++ {
101 | field := reflect.ValueOf(value).Field(i)
102 | field_type := reflect.TypeOf(value).Field(i)
103 | var name string
104 | if field_type.Tag.Get("xml") != "" {
105 | name = field_type.Tag.Get("xml")
106 | } else {
107 | name = field_type.Name
108 | }
109 | field_value, _ := rpc2XML(field.Interface())
110 | field_name := fmt.Sprintf("%s", name)
111 | out += fmt.Sprintf("%s%s", field_name, field_value)
112 | }
113 | out += ""
114 | return
115 | }
116 |
117 | func array2XML(value interface{}) (out string) {
118 | out += ""
119 | for i := 0; i < reflect.ValueOf(value).Len(); i++ {
120 | item_xml, _ := rpc2XML(reflect.ValueOf(value).Index(i).Interface())
121 | out += item_xml
122 | }
123 | out += ""
124 | return
125 | }
126 |
127 | func time2XML(t time.Time) string {
128 | /*
129 | // TODO: find out whether we need to deal
130 | // here with TZ
131 | var tz string;
132 | zone, offset := t.Zone()
133 | if zone == "UTC" {
134 | tz = "Z"
135 | } else {
136 | tz = fmt.Sprintf("%03d00", offset / 3600 )
137 | }
138 | */
139 | return fmt.Sprintf("%04d%02d%02dT%02d:%02d:%02d",
140 | t.Year(), t.Month(), t.Day(),
141 | t.Hour(), t.Minute(), t.Second())
142 | }
143 |
144 | func base642XML(data []byte) string {
145 | str := base64.StdEncoding.EncodeToString(data)
146 | return fmt.Sprintf("%s", str)
147 | }
148 |
--------------------------------------------------------------------------------
/xml/rpc2xml_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Ivan Danyliuk
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package xml
6 |
7 | import (
8 | "testing"
9 | "time"
10 | )
11 |
12 | type SubStructRpc2Xml struct {
13 | Foo int
14 | Bar string
15 | Data []int
16 | }
17 |
18 | type StructRpc2Xml struct {
19 | Int int
20 | Float float64
21 | Str string
22 | Bool bool
23 | Sub SubStructRpc2Xml
24 | Time time.Time
25 | Base64 []byte
26 | }
27 |
28 | func TestRPC2XML(t *testing.T) {
29 | req := &StructRpc2Xml{123, 3.145926, "Hello, World!", false, SubStructRpc2Xml{42, "I'm Bar", []int{1, 2, 3}}, time.Date(2012, time.July, 17, 14, 8, 55, 0, time.Local), []byte("you can't read this!")}
30 | xml, err := rpcRequest2XML("Some.Method", req)
31 | if err != nil {
32 | t.Error("RPC2XML conversion failed", err)
33 | }
34 | expected := "Some.Method1233.145926Hello, World!0Foo42BarI'm BarData12320120717T14:08:55eW91IGNhbid0IHJlYWQgdGhpcyE="
35 | if xml != expected {
36 | t.Error("RPC2XML conversion failed")
37 | t.Error("Expected", expected)
38 | t.Error("Got", xml)
39 | }
40 | }
41 |
42 | type StructSpecialCharsRpc2Xml struct {
43 | String1 string
44 | }
45 |
46 | func TestRPC2XMLSpecialChars(t *testing.T) {
47 | req := &StructSpecialCharsRpc2Xml{" & \" < > "}
48 | xml, err := rpcResponse2XML(req)
49 | if err != nil {
50 | t.Error("RPC2XML conversion failed", err)
51 | }
52 | expected := " & " < > "
53 | if xml != expected {
54 | t.Error("RPC2XML Special chars conversion failed")
55 | t.Error("Expected", expected)
56 | t.Error("Got", xml)
57 | }
58 | }
59 |
60 | type StructNilRpc2Xml struct {
61 | Ptr *int
62 | }
63 |
64 | func TestRpc2XmlNil(t *testing.T) {
65 | req := &StructNilRpc2Xml{nil}
66 | xml, err := rpcResponse2XML(req)
67 | if err != nil {
68 | t.Error("RPC2XML conversion failed", err)
69 | }
70 | expected := ""
71 | if xml != expected {
72 | t.Error("RPC2XML Special chars conversion failed")
73 | t.Error("Expected", expected)
74 | t.Error("Got", xml)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/xml/server.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Ivan Danyliuk
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package xml
6 |
7 | import (
8 | "encoding/xml"
9 | "fmt"
10 | "io/ioutil"
11 | "net/http"
12 |
13 | "github.com/gorilla/rpc"
14 | )
15 |
16 | // ----------------------------------------------------------------------------
17 | // Codec
18 | // ----------------------------------------------------------------------------
19 |
20 | // NewCodec returns a new XML-RPC Codec.
21 | func NewCodec() *Codec {
22 | return &Codec{
23 | aliases: make(map[string]string),
24 | }
25 | }
26 |
27 | // Codec creates a CodecRequest to process each request.
28 | type Codec struct {
29 | aliases map[string]string
30 | }
31 |
32 | // RegisterAlias creates a method alias
33 | func (c *Codec) RegisterAlias(alias, method string) {
34 | c.aliases[alias] = method
35 | }
36 |
37 | // NewRequest returns a CodecRequest.
38 | func (c *Codec) NewRequest(r *http.Request) rpc.CodecRequest {
39 | rawxml, err := ioutil.ReadAll(r.Body)
40 | if err != nil {
41 | return &CodecRequest{err: err}
42 | }
43 | defer r.Body.Close()
44 |
45 | var request ServerRequest
46 | if err := xml.Unmarshal(rawxml, &request); err != nil {
47 | return &CodecRequest{err: err}
48 | }
49 | request.rawxml = string(rawxml)
50 | if method, ok := c.aliases[request.Method]; ok {
51 | request.Method = method
52 | }
53 | return &CodecRequest{request: &request}
54 | }
55 |
56 | // ----------------------------------------------------------------------------
57 | // CodecRequest
58 | // ----------------------------------------------------------------------------
59 |
60 | type ServerRequest struct {
61 | Name xml.Name `xml:"methodCall"`
62 | Method string `xml:"methodName"`
63 | rawxml string
64 | }
65 |
66 | // CodecRequest decodes and encodes a single request.
67 | type CodecRequest struct {
68 | request *ServerRequest
69 | err error
70 | }
71 |
72 | // Method returns the RPC method for the current request.
73 | //
74 | // The method uses a dotted notation as in "Service.Method".
75 | func (c *CodecRequest) Method() (string, error) {
76 | if c.err == nil {
77 | return c.request.Method, nil
78 | }
79 | return "", c.err
80 | }
81 |
82 | // ReadRequest fills the request object for the RPC method.
83 | //
84 | // args is the pointer to the Service.Args structure
85 | // it gets populated from temporary XML structure
86 | func (c *CodecRequest) ReadRequest(args interface{}) error {
87 | c.err = xml2RPC(c.request.rawxml, args)
88 | return nil
89 | }
90 |
91 | // WriteResponse encodes the response and writes it to the ResponseWriter.
92 | //
93 | // response is the pointer to the Service.Response structure
94 | // it gets encoded into the XML-RPC xml string
95 | func (c *CodecRequest) WriteResponse(w http.ResponseWriter, response interface{}, methodErr error) error {
96 | var xmlstr string
97 | if c.err != nil {
98 | var fault Fault
99 | switch c.err.(type) {
100 | case Fault:
101 | fault = c.err.(Fault)
102 | default:
103 | fault = FaultApplicationError
104 | fault.String += fmt.Sprintf(": %v", c.err)
105 | }
106 | xmlstr = fault2XML(fault)
107 | } else {
108 | xmlstr, _ = rpcResponse2XML(response)
109 | }
110 |
111 | w.Header().Set("Content-Type", "text/xml; charset=utf-8")
112 | w.Write([]byte(xmlstr))
113 | return nil
114 | }
115 |
--------------------------------------------------------------------------------
/xml/xml2rpc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Ivan Danyliuk
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package xml
6 |
7 | import (
8 | "bytes"
9 | "encoding/base64"
10 | "encoding/xml"
11 | "fmt"
12 | "reflect"
13 | "strconv"
14 | "time"
15 | "unicode"
16 | "unicode/utf8"
17 |
18 | "github.com/rogpeppe/go-charset/charset"
19 | _ "github.com/rogpeppe/go-charset/data"
20 | )
21 |
22 | // Types used for unmarshalling
23 | type response struct {
24 | Name xml.Name `xml:"methodResponse"`
25 | Params []param `xml:"params>param"`
26 | Fault faultValue `xml:"fault,omitempty"`
27 | }
28 |
29 | type param struct {
30 | Value value `xml:"value"`
31 | }
32 |
33 | type value struct {
34 | Array []value `xml:"array>data>value"`
35 | Struct []member `xml:"struct>member"`
36 | String string `xml:"string"`
37 | Int string `xml:"int"`
38 | Int4 string `xml:"i4"`
39 | Double string `xml:"double"`
40 | Boolean string `xml:"boolean"`
41 | DateTime string `xml:"dateTime.iso8601"`
42 | Base64 string `xml:"base64"`
43 | Raw string `xml:",innerxml"` // the value can be defualt string
44 | }
45 |
46 | type member struct {
47 | Name string `xml:"name"`
48 | Value value `xml:"value"`
49 | }
50 |
51 | func xml2RPC(xmlraw string, rpc interface{}) error {
52 | // Unmarshal raw XML into the temporal structure
53 | var ret response
54 | decoder := xml.NewDecoder(bytes.NewReader([]byte(xmlraw)))
55 | decoder.CharsetReader = charset.NewReader
56 | err := decoder.Decode(&ret)
57 | if err != nil {
58 | return FaultDecode
59 | }
60 |
61 | if !ret.Fault.IsEmpty() {
62 | return getFaultResponse(ret.Fault)
63 | }
64 |
65 | // Structures should have equal number of fields
66 | if reflect.TypeOf(rpc).Elem().NumField() != len(ret.Params) {
67 | return FaultWrongArgumentsNumber
68 | }
69 |
70 | // Now, convert temporal structure into the
71 | // passed rpc variable, according to it's structure
72 | for i, param := range ret.Params {
73 | field := reflect.ValueOf(rpc).Elem().Field(i)
74 | err = value2Field(param.Value, &field)
75 | if err != nil {
76 | return err
77 | }
78 | }
79 |
80 | return nil
81 | }
82 |
83 | // getFaultResponse converts faultValue to Fault.
84 | func getFaultResponse(fault faultValue) Fault {
85 | var (
86 | code int
87 | str string
88 | )
89 |
90 | for _, field := range fault.Value.Struct {
91 | if field.Name == "faultCode" {
92 | code, _ = strconv.Atoi(field.Value.Int)
93 | } else if field.Name == "faultString" {
94 | str = field.Value.String
95 | if str == "" {
96 | str = field.Value.Raw
97 | }
98 | }
99 | }
100 |
101 | return Fault{Code: code, String: str}
102 | }
103 |
104 | func value2Field(value value, field *reflect.Value) error {
105 | if !field.CanSet() {
106 | return FaultApplicationError
107 | }
108 |
109 | var (
110 | err error
111 | val interface{}
112 | )
113 |
114 | switch {
115 | case value.Int != "":
116 | val, _ = strconv.Atoi(value.Int)
117 | case value.Int4 != "":
118 | val, _ = strconv.Atoi(value.Int4)
119 | case value.Double != "":
120 | val, _ = strconv.ParseFloat(value.Double, 64)
121 | case value.String != "":
122 | val = value.String
123 | case value.Boolean != "":
124 | val = xml2Bool(value.Boolean)
125 | case value.DateTime != "":
126 | val, err = xml2DateTime(value.DateTime)
127 | case value.Base64 != "":
128 | val, err = xml2Base64(value.Base64)
129 | case len(value.Struct) != 0:
130 | if field.Kind() != reflect.Struct {
131 | fault := FaultInvalidParams
132 | fault.String += fmt.Sprintf("structure fields mismatch: %s != %s", field.Kind(), reflect.Struct.String())
133 | return fault
134 | }
135 | s := value.Struct
136 | for i := 0; i < len(s); i++ {
137 | // Uppercase first letter for field name to deal with
138 | // methods in lowercase, which cannot be used
139 | field_name := uppercaseFirst(s[i].Name)
140 | f := field.FieldByName(field_name)
141 | err = value2Field(s[i].Value, &f)
142 | }
143 | case len(value.Array) != 0:
144 | a := value.Array
145 | f := *field
146 | slice := reflect.MakeSlice(reflect.TypeOf(f.Interface()),
147 | len(a), len(a))
148 | for i := 0; i < len(a); i++ {
149 | item := slice.Index(i)
150 | err = value2Field(a[i], &item)
151 | }
152 | f = reflect.AppendSlice(f, slice)
153 | val = f.Interface()
154 | case len(value.Array) == 0:
155 | val = val
156 |
157 | default:
158 | // value field is default to string, see http://en.wikipedia.org/wiki/XML-RPC#Data_types
159 | // also can be
160 | if value.Raw != "" {
161 | val = value.Raw
162 | }
163 | }
164 |
165 | if val != nil {
166 | if reflect.TypeOf(val) != reflect.TypeOf(field.Interface()) {
167 | fault := FaultInvalidParams
168 | fault.String += fmt.Sprintf(": fields type mismatch: %s != %s",
169 | reflect.TypeOf(val),
170 | reflect.TypeOf(field.Interface()))
171 | return fault
172 | }
173 |
174 | field.Set(reflect.ValueOf(val))
175 | }
176 |
177 | return err
178 | }
179 |
180 | func xml2Bool(value string) bool {
181 | var b bool
182 | switch value {
183 | case "1", "true", "TRUE", "True":
184 | b = true
185 | case "0", "false", "FALSE", "False":
186 | b = false
187 | }
188 | return b
189 | }
190 |
191 | func xml2DateTime(value string) (time.Time, error) {
192 | var (
193 | year, month, day int
194 | hour, minute, second int
195 | )
196 | _, err := fmt.Sscanf(value, "%04d%02d%02dT%02d:%02d:%02d",
197 | &year, &month, &day,
198 | &hour, &minute, &second)
199 | t := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.Local)
200 | return t, err
201 | }
202 |
203 | func xml2Base64(value string) ([]byte, error) {
204 | return base64.StdEncoding.DecodeString(value)
205 | }
206 |
207 | func uppercaseFirst(in string) (out string) {
208 | r, n := utf8.DecodeRuneInString(in)
209 | return string(unicode.ToUpper(r)) + in[n:]
210 | }
211 |
--------------------------------------------------------------------------------
/xml/xml2rpc_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Ivan Danyliuk
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package xml
6 |
7 | import (
8 | "reflect"
9 | "testing"
10 | "time"
11 | )
12 |
13 | type SubStructXml2Rpc struct {
14 | Foo int
15 | Bar string
16 | Data []int
17 | }
18 |
19 | type StructXml2Rpc struct {
20 | Int int
21 | Float float64
22 | Str string
23 | Bool bool
24 | Sub SubStructXml2Rpc
25 | Time time.Time
26 | Base64 []byte
27 | }
28 |
29 | func TestXML2RPC(t *testing.T) {
30 | req := new(StructXml2Rpc)
31 | err := xml2RPC("Some.Method1233.145926Hello, World!0Foo42BarI'm BarData12320120717T14:08:55eW91IGNhbid0IHJlYWQgdGhpcyE=", req)
32 | if err != nil {
33 | t.Error("XML2RPC conversion failed", err)
34 | }
35 | expected_req := &StructXml2Rpc{123, 3.145926, "Hello, World!", false, SubStructXml2Rpc{42, "I'm Bar", []int{1, 2, 3}}, time.Date(2012, time.July, 17, 14, 8, 55, 0, time.Local), []byte("you can't read this!")}
36 | if !reflect.DeepEqual(req, expected_req) {
37 | t.Error("XML2RPC conversion failed")
38 | t.Error("Expected", expected_req)
39 | t.Error("Got", req)
40 | }
41 | }
42 |
43 | type StructSpecialCharsXml2Rpc struct {
44 | String1 string
45 | }
46 |
47 | func TestXML2RPCSpecialChars(t *testing.T) {
48 | req := new(StructSpecialCharsXml2Rpc)
49 | err := xml2RPC(" & " < > ", req)
50 | if err != nil {
51 | t.Error("XML2RPC conversion failed", err)
52 | }
53 | expected_req := &StructSpecialCharsXml2Rpc{" & \" < > "}
54 | if !reflect.DeepEqual(req, expected_req) {
55 | t.Error("XML2RPC conversion failed")
56 | t.Error("Expected", expected_req)
57 | t.Error("Got", req)
58 | }
59 | }
60 |
61 | type StructNilXml2Rpc struct {
62 | Ptr *int
63 | }
64 |
65 | func TestXML2RPCNil(t *testing.T) {
66 | req := new(StructNilXml2Rpc)
67 | err := xml2RPC("", req)
68 | if err != nil {
69 | t.Error("XML2RPC conversion failed", err)
70 | }
71 | expected_req := &StructNilXml2Rpc{nil}
72 | if !reflect.DeepEqual(req, expected_req) {
73 | t.Error("XML2RPC conversion failed")
74 | t.Error("Expected", expected_req)
75 | t.Error("Got", req)
76 | }
77 | }
78 |
79 | type StructXml2RpcSubArgs struct {
80 | String1 string
81 | String2 string
82 | Id int
83 | }
84 |
85 | type StructXml2RpcHelloArgs struct {
86 | Args StructXml2RpcSubArgs
87 | }
88 |
89 | func TestXML2RPCLowercasedMethods(t *testing.T) {
90 | req := new(StructXml2RpcHelloArgs)
91 | err := xml2RPC("string1I'm a first stringstring2I'm a second stringid1", req)
92 | if err != nil {
93 | t.Error("XML2RPC conversion failed", err)
94 | }
95 | args := StructXml2RpcSubArgs{"I'm a first string", "I'm a second string", 1}
96 | expected_req := &StructXml2RpcHelloArgs{args}
97 | if !reflect.DeepEqual(req, expected_req) {
98 | t.Error("XML2RPC conversion failed")
99 | t.Error("Expected", expected_req)
100 | t.Error("Got", req)
101 | }
102 | }
103 |
104 | func TestXML2PRCFaultCall(t *testing.T) {
105 | req := new(StructXml2RpcHelloArgs)
106 | data := `faultCode116faultStringError
107 | Requiredattribute'user'notfound:
108 | [{'User',"gggg"},{'Host',"sss.com"},{'Password',"ssddfsdf"}]
109 | `
110 |
111 | errstr := `Error
112 | Requiredattribute'user'notfound:
113 | [{'User',"gggg"},{'Host',"sss.com"},{'Password',"ssddfsdf"}]
114 | `
115 |
116 | err := xml2RPC(data, req)
117 |
118 | fault, ok := err.(Fault)
119 | if !ok {
120 | t.Errorf("error should be of concrete type Fault, but got %v", err)
121 | } else {
122 | if fault.Code != 116 {
123 | t.Errorf("expected fault.Code to be %d, but got %d", 116, fault.Code)
124 | }
125 | if fault.String != errstr {
126 | t.Errorf("fault.String should be:\n\n%s\n\nbut got:\n\n%s\n", errstr, fault.String)
127 | }
128 | }
129 | }
130 |
131 | func TestXML2PRCISO88591(t *testing.T) {
132 | req := new(StructXml2RpcHelloArgs)
133 | data := `faultCode116faultStringError
134 | Requiredattribute'user'notfound:
135 | [{'User',"` + "\xd6\xf1\xe4" + `"},{'Host',"sss.com"},{'Password',"ssddfsdf"}]
136 | `
137 |
138 | errstr := `Error
139 | Requiredattribute'user'notfound:
140 | [{'User',"Öñä"},{'Host',"sss.com"},{'Password',"ssddfsdf"}]
141 | `
142 |
143 | err := xml2RPC(data, req)
144 |
145 | fault, ok := err.(Fault)
146 | if !ok {
147 | t.Errorf("error should be of concrete type Fault, but got %v", err)
148 | } else {
149 | if fault.Code != 116 {
150 | t.Errorf("expected fault.Code to be %d, but got %d", 116, fault.Code)
151 | }
152 | if fault.String != errstr {
153 | t.Errorf("fault.String should be:\n\n%s\n\nbut got:\n\n%s\n", errstr, fault.String)
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/xml/xml_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Ivan Danyliuk
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package xml
6 |
7 | import (
8 | "bytes"
9 | "net/http"
10 | "net/http/httptest"
11 | "strconv"
12 | "testing"
13 |
14 | "github.com/gorilla/rpc"
15 | )
16 |
17 | //////////////////////////////////
18 | // Service 1
19 | //////////////////////////////////
20 | type Service1Request struct {
21 | A int
22 | B int
23 | }
24 |
25 | type Service1BadRequest struct {
26 | A int
27 | B int
28 | C int
29 | }
30 |
31 | type Service1Response struct {
32 | Result int
33 | }
34 |
35 | type Service1 struct {
36 | }
37 |
38 | func (t *Service1) Multiply(r *http.Request, req *Service1Request, res *Service1Response) error {
39 | res.Result = req.A * req.B
40 | return nil
41 | }
42 |
43 | //////////////////////////////////
44 | // Service 2
45 | //////////////////////////////////
46 | type Service2Request struct {
47 | Name string
48 | Age int
49 | HasPermit bool
50 | }
51 |
52 | type Service2Response struct {
53 | Message string
54 | Status int
55 | }
56 |
57 | type Service2 struct {
58 | }
59 |
60 | func (t *Service2) GetGreeting(r *http.Request, req *Service2Request, res *Service2Response) error {
61 | res.Message = "Hello, user " + req.Name + ". You're " + strconv.Itoa(req.Age) + " years old :-P"
62 | if req.HasPermit {
63 | res.Message += " And you has permit."
64 | } else {
65 | res.Message += " And you DON'T has permit."
66 | }
67 | res.Status = 42
68 | return nil
69 | }
70 |
71 | //////////////////////////////////
72 | // Service 3
73 | //////////////////////////////////
74 |
75 | type Address struct {
76 | Number int
77 | Street string
78 | Country string
79 | }
80 |
81 | type Person struct {
82 | Name string
83 | Surname string
84 | Age int
85 | Address Address
86 | }
87 |
88 | type Info struct {
89 | Facebook string
90 | Twitter string
91 | Phone string
92 | }
93 |
94 | type Service3Request struct {
95 | Person Person
96 | }
97 |
98 | type Service3Response struct {
99 | Info Info
100 | }
101 |
102 | type Service3 struct {
103 | }
104 |
105 | func (t *Service3) GetInfo(r *http.Request, req *Service3Request, res *Service3Response) error {
106 | var i Info
107 | i.Facebook = "http://facebook.com/" + req.Person.Name
108 | i.Twitter = "http://twitter.com/" + req.Person.Name
109 | i.Phone = "+55-555-555-55-55"
110 | res.Info = i
111 | return nil
112 | }
113 |
114 | func execute(t *testing.T, s *rpc.Server, method string, req, res interface{}) error {
115 | if !s.HasMethod(method) {
116 | t.Fatal("Expected to be registered:", method)
117 | }
118 |
119 | buf, _ := EncodeClientRequest(method, req)
120 | body := bytes.NewBuffer(buf)
121 | r, _ := http.NewRequest("POST", "http://localhost:8080/", body)
122 | r.Header.Set("Content-Type", "text/xml")
123 |
124 | w := httptest.NewRecorder()
125 | s.ServeHTTP(w, r)
126 |
127 | return DecodeClientResponse(w.Body, res)
128 | }
129 |
130 | func TestRPC2XMLConverter(t *testing.T) {
131 | req := &Service1Request{4, 2}
132 | xml, err := rpcRequest2XML("Some.Method", req)
133 | if err != nil {
134 | t.Error("RPC2XML conversion failed", err)
135 | }
136 |
137 | expected := "Some.Method42"
138 | if xml != expected {
139 | t.Error("RPC2XML conversion failed")
140 | t.Error("Expected", expected)
141 | t.Error("Got", xml)
142 | }
143 |
144 | req2 := &Service2Request{"Johnny", 33, true}
145 | xml, err = rpcRequest2XML("Some.Method", req2)
146 | if err != nil {
147 | t.Error("RPC2XML conversion failed", err)
148 | }
149 |
150 | expected = "Some.MethodJohnny331"
151 | if xml != expected {
152 | t.Error("RPC2XML conversion failed")
153 | t.Error("Expected", expected)
154 | t.Error("Got", xml)
155 | }
156 |
157 | address := Address{221, "Baker str.", "London"}
158 | person := Person{"Johnny", "Doe", 33, address}
159 | req3 := &Service3Request{person}
160 | xml, err = rpcRequest2XML("Some.Method", req3)
161 | if err != nil {
162 | t.Error("RPC2XML conversion failed", err)
163 | }
164 |
165 | expected = "Some.MethodNameJohnnySurnameDoeAge33AddressNumber221StreetBaker str.CountryLondon"
166 | if xml != expected {
167 | t.Error("RPC2XML conversion failed")
168 | t.Error("Expected", expected)
169 | t.Error("Got", xml)
170 | }
171 |
172 | res := &Service1Response{42}
173 | xml, err = rpcResponse2XML(res)
174 | if err != nil {
175 | t.Error("RPC2XML conversion failed", err)
176 | }
177 |
178 | expected = "42"
179 | if xml != expected {
180 | t.Error("RPC2XML conversion failed")
181 | t.Error("Expected", expected)
182 | t.Error("Got", xml)
183 | }
184 | }
185 |
186 | func TestServices(t *testing.T) {
187 | s := rpc.NewServer()
188 | s.RegisterCodec(NewCodec(), "text/xml")
189 | s.RegisterService(new(Service1), "")
190 | s.RegisterService(new(Service2), "")
191 | s.RegisterService(new(Service3), "")
192 |
193 | var res Service1Response
194 | if err := execute(t, s, "Service1.Multiply", &Service1Request{4, 2}, &res); err != nil {
195 | t.Error("Expected err to be nil, but got:", err)
196 | }
197 | if res.Result != 8 {
198 | t.Errorf("Wrong response: %v.", res.Result)
199 | }
200 |
201 | var res2 Service2Response
202 | if err := execute(t, s, "Service2.GetGreeting", &Service2Request{"Johnny", 33, true}, &res2); err != nil {
203 | t.Error("Expected err to be nil, but got:", err)
204 | }
205 | if res2.Message != "Hello, user Johnny. You're 33 years old :-P And you has permit." {
206 | t.Errorf("Wrong response: %v.", res2.Message)
207 | }
208 | if res2.Status != 42 {
209 | t.Errorf("Wrong response: %v.", res2.Status)
210 | }
211 |
212 | var res3 Service3Response
213 | address := Address{221, "Baker str.", "London"}
214 | person := Person{"Johnny", "Doe", 33, address}
215 | if err := execute(t, s, "Service3.GetInfo", &Service3Request{person}, &res3); err != nil {
216 | t.Error("Expected err to be nil, but got:", err)
217 | }
218 |
219 | if res3.Info.Phone != "+55-555-555-55-55" {
220 | t.Errorf("Wrong response: %v.", res3.Info)
221 | }
222 | }
223 |
--------------------------------------------------------------------------------