diff --git a/client.go b/client.go
index 643dc1c..11e2e83 100644
--- a/client.go
+++ b/client.go
@@ -15,6 +15,19 @@ type Client struct {
*rpc.Client
}
+// Multicall performs a multicall request.
+// `calls` should be constructed with `NewMulticallArg`
+// and `outs` must be a slice of pointers.
+// If the response contains at least one fault,
+// the first is returned as a `MulticallFault` error.
+func (c Client) Multicall(calls []MulticallArg, outs ...interface{}) error {
+ if len(calls) != len(outs) {
+ return errors.New("lengths of calls and responses are not matching")
+ }
+ tmp := multicallOut{calls: calls, datas: outs}
+ return c.Call("system.multicall", calls, tmp)
+}
+
// clientCodec is rpc.ClientCodec interface implementation.
type clientCodec struct {
// url presents url of xmlrpc service
diff --git a/encoder.go b/encoder.go
index 7ab271a..6a1cf95 100644
--- a/encoder.go
+++ b/encoder.go
@@ -14,8 +14,6 @@ import (
// Base64 represents value in base64 encoding
type Base64 string
-type encodeFunc func(reflect.Value) ([]byte, error)
-
func marshal(v interface{}) ([]byte, error) {
if v == nil {
return []byte{}, nil
diff --git a/fixtures/multicall_error.xml b/fixtures/multicall_error.xml
new file mode 100644
index 0000000..6bf13dd
--- /dev/null
+++ b/fixtures/multicall_error.xml
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ label
+
+ Test
+
+
+
+ orderby
+
+ date
+
+
+
+ ordertype
+
+ ascending
+
+
+
+ date
+
+ 1565526014000
+
+
+
+ createddate
+
+ 1565526019000
+
+
+
+ lastvisitdate
+
+ 1565526459000
+
+
+
+ lastfileaddeddate
+
+ 1565526368000
+
+
+
+ public
+
+ 0
+
+
+
+ allowdownload
+
+ 1
+
+
+
+ allowupload
+
+ 0
+
+
+
+ allowprintorder
+
+ 1
+
+
+
+ allowsendcomments
+
+ 1
+
+
+
+
+
+
+
+ label
+
+ test2
+
+
+
+ orderby
+
+ date
+
+
+
+ ordertype
+
+ ascending
+
+
+
+ date
+
+ 1565526625000
+
+
+
+ createddate
+
+ 1565526629000
+
+
+
+ lastvisitdate
+
+ 1581931485000
+
+
+
+ lastfileaddeddate
+
+ 1565526652000
+
+
+
+ public
+
+ 0
+
+
+
+ allowdownload
+
+ 1
+
+
+
+ allowupload
+
+ 0
+
+
+
+ allowprintorder
+
+ 1
+
+
+
+ allowsendcomments
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+ faultCode
+
+ 205
+
+
+
+ faultString
+
+ Error (205) : Can't get properties for the session xxxx
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/multicall_ok.xml b/fixtures/multicall_ok.xml
new file mode 100644
index 0000000..3262ad3
--- /dev/null
+++ b/fixtures/multicall_ok.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ nbfiles
+
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+ nbfiles
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/is_zero.go b/is_zero.go
index 65276d0..b3aae8f 100644
--- a/is_zero.go
+++ b/is_zero.go
@@ -39,6 +39,6 @@ func isZero(v Value) bool {
default:
// This should never happens, but will act as a safeguard for
// later, as a default value doesn't makes sense here.
- panic(&ValueError{"reflect.Value.IsZero", v.Kind()})
+ panic(&ValueError{Method: "reflect.Value.IsZero", Kind: v.Kind()})
}
}
diff --git a/multicall.go b/multicall.go
new file mode 100644
index 0000000..4bfd1c8
--- /dev/null
+++ b/multicall.go
@@ -0,0 +1,144 @@
+package xmlrpc
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "reflect"
+ "strconv"
+)
+
+// Types used for unmarshalling
+
+// main fault should be already checked
+type response struct {
+ Name xml.Name `xml:"methodResponse"`
+ Params []param `xml:"params>param"`
+}
+
+type param struct {
+ Value value `xml:"value"`
+}
+
+type member struct {
+ Name string `xml:"name"`
+ Value value `xml:"value"`
+}
+
+type value struct {
+ Array []value `xml:"array>data>value"` // used for returns
+ Struct []member `xml:"struct>member"` // used for fault
+ String string `xml:"string"` // used for faults
+ Int string `xml:"int"` // used for faults
+ Raw []byte `xml:",innerxml"`
+}
+
+// getFaultResponse converts faultValue to Fault.
+func getFaultResponse(fault []member) FaultError {
+ var (
+ code int
+ str string
+ )
+
+ for _, field := range fault {
+ if field.Name == "faultCode" {
+ code, _ = strconv.Atoi(field.Value.Int)
+ } else if field.Name == "faultString" {
+ str = field.Value.String
+ if str == "" {
+ str = string(field.Value.Raw)
+ }
+ }
+ }
+
+ return FaultError{Code: code, String: str}
+}
+
+// MulticallFault tracks the position of the fault.
+type MulticallFault struct {
+ FaultError
+ Index int // 0 based
+ methodName string // for better message
+}
+
+func (m MulticallFault) Error() string {
+ return fmt.Sprintf("fault in call %d (%s) : %s", m.Index, m.methodName, m.FaultError.Error())
+}
+
+func (r Response) unmarshalMulticall(out multicallOut) error {
+ switch ki := reflect.TypeOf(out.datas).Kind(); ki {
+ case reflect.Array, reflect.Slice: // OK
+ default:
+ return fmt.Errorf("destination for multicall must be Array or Slice, got %s", ki)
+ }
+ outSlice := reflect.ValueOf(out.datas)
+
+ parts, err := splitMulticall(r)
+ if multicallErr, ok := err.(MulticallFault); ok {
+ multicallErr.methodName = out.calls[multicallErr.Index].MethodName
+ return multicallErr
+ } else if err != nil {
+ return err
+ }
+
+ if outSlice.Len() != len(parts) {
+ return fmt.Errorf("invalid number of return destinations : response needs %d, got %d", len(parts), outSlice.Len())
+ }
+ for i, xmlReturn := range parts {
+ // pointer to one call's destination
+ elem := outSlice.Index(i).Interface()
+
+ // unmarshal expect a wrapping tag
+ xmlReturn = append(append([]byte(""), xmlReturn...), ""...)
+ if err := unmarshal(xmlReturn, elem); err != nil {
+ return fmt.Errorf("unmarshall number %d failed : %s", i, err.Error())
+ }
+ }
+ return nil
+}
+
+// returns xml encoded chunks, one for each multicall response
+// if there is (at least) one fault, returns the first one
+// as error
+func splitMulticall(xmlraw []byte) ([][]byte, error) {
+ // Unmarshal raw XML into the temporal structure
+ var ret response
+
+ dec := xml.NewDecoder(bytes.NewReader(xmlraw))
+ if CharsetReader != nil {
+ dec.CharsetReader = CharsetReader
+ }
+
+ if err := dec.Decode(&ret); err != nil {
+ return nil, err
+ }
+ if L := len(ret.Params); L != 1 {
+ return nil, fmt.Errorf("unexpected number of arguments : got %d", L)
+ }
+ // multicall returns one array of values
+ returns := ret.Params[0].Value.Array
+
+ out := make([][]byte, len(returns))
+ for i, oneReturn := range returns {
+ // multicall return are always wrapped in one-sized array
+ // otherwise, it's a fault
+ if len(oneReturn.Array) != 1 {
+ fault := getFaultResponse(oneReturn.Struct)
+ return nil, MulticallFault{Index: i, FaultError: fault}
+ }
+ // unwrap the value and store raw xml
+ // to further process
+ out[i] = oneReturn.Array[0].Raw
+ }
+ return out, nil
+}
+
+// MulticallArg stores one call
+type MulticallArg struct {
+ MethodName string `xmlrpc:"methodName"`
+ Params []interface{} `xmlrpc:"params"` // 1-sized list containing the real arguments
+}
+
+func NewMulticallArg(method string, args interface{}) MulticallArg {
+ return MulticallArg{MethodName: method, Params: []interface{}{args}}
+}
diff --git a/multicall_test.go b/multicall_test.go
new file mode 100644
index 0000000..45f0f24
--- /dev/null
+++ b/multicall_test.go
@@ -0,0 +1,75 @@
+package xmlrpc
+
+import (
+ "io/ioutil"
+ "testing"
+)
+
+func Test_splitMulticall(t *testing.T) {
+ b, err := ioutil.ReadFile("fixtures/multicall_error.xml")
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = splitMulticall(b)
+ mf, ok := err.(MulticallFault)
+ if !ok {
+ t.Errorf("expected multicall fault, got %s", err)
+ }
+ if mf.Index != 1 {
+ t.Errorf("wrong position for fault %d", mf.Index)
+ }
+
+ b, err = ioutil.ReadFile("fixtures/multicall_ok.xml")
+ if err != nil {
+ t.Fatal(err)
+ }
+ out, err := splitMulticall(b)
+ if err != nil {
+ t.Error(err)
+ }
+ if L := len(out); L != 2 {
+ t.Errorf("expected 2 answers, got %d", L)
+ }
+}
+
+func TestUnmarshal(t *testing.T) {
+ b, err := ioutil.ReadFile("fixtures/multicall_ok.xml")
+ if err != nil {
+ t.Fatal(err)
+ }
+ calls := make([]MulticallArg, 2)
+ type data struct {
+ NbFiles int `xmlrpc:"nbfiles"`
+ }
+ var d1, d2 data
+ out := []interface{}{&d1, &d2}
+ err = Response(b).unmarshalMulticall(multicallOut{calls: calls, datas: out})
+ if err != nil {
+ t.Error(err)
+ }
+ if nb1 := d1.NbFiles; nb1 != 4 {
+ t.Errorf("expected 4, got %d", nb1)
+ }
+ if nb2 := d2.NbFiles; nb2 != 1 {
+ t.Errorf("expected 4, got %d", nb2)
+ }
+
+ outArray := [2]interface{}{&d1, &d2}
+ err = Response(b).unmarshalMulticall(multicallOut{calls: calls, datas: outArray})
+ if err != nil {
+ t.Error(err)
+ }
+ if nb1 := d1.NbFiles; nb1 != 4 {
+ t.Errorf("expected 4, got %d", nb1)
+ }
+ if nb2 := d2.NbFiles; nb2 != 1 {
+ t.Errorf("expected 4, got %d", nb2)
+ }
+
+ var outWrong string
+ err = Response(b).unmarshalMulticall(multicallOut{calls: calls, datas: &outWrong})
+ if err == nil {
+ t.Error("expected error")
+ }
+
+}
diff --git a/response.go b/response.go
index 18e6d36..64a3f7d 100644
--- a/response.go
+++ b/response.go
@@ -33,10 +33,19 @@ func (r Response) Err() error {
return fault
}
+// tmp storage for multicall responses
+type multicallOut struct {
+ calls []MulticallArg // for error messages
+ datas interface{} // slice/array of pointers
+}
+
func (r Response) Unmarshal(v interface{}) error {
+ if mc, isMulticall := v.(multicallOut); isMulticall {
+ return r.unmarshalMulticall(mc)
+ }
+
if err := unmarshal(r, v); err != nil {
return err
}
-
return nil
}