First draft supporting maps as action args.
This commit is contained in:
parent
5d0813cf55
commit
f69c4d0ee2
@ -6,6 +6,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrFault can be used as a target with errors.Is.
|
// ErrFault can be used as a target with errors.Is.
|
||||||
@ -65,14 +67,48 @@ var _ xml.Marshaler = &Action{}
|
|||||||
// This is an implementation detail that allows packing elements inside the
|
// This is an implementation detail that allows packing elements inside the
|
||||||
// action element from the struct in `a.Args`.
|
// action element from the struct in `a.Args`.
|
||||||
func (a *Action) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
func (a *Action) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
// Hardcodes the XML namespace. See comment in Write() for context.
|
v := reflect.Indirect(reflect.ValueOf(a.Args))
|
||||||
return e.EncodeElement(a.Args, xml.StartElement{
|
t := v.Type()
|
||||||
Name: xml.Name{Space: "", Local: "u:" + a.XMLName.Local},
|
elemName := xml.Name{Space: "", Local: "u:" + a.XMLName.Local}
|
||||||
|
startElement := xml.StartElement{
|
||||||
|
Name: elemName,
|
||||||
Attr: []xml.Attr{{
|
Attr: []xml.Attr{{
|
||||||
Name: xml.Name{Space: "", Local: "xmlns:u"},
|
Name: xml.Name{Space: "", Local: "xmlns:u"},
|
||||||
Value: a.XMLName.Space,
|
Value: a.XMLName.Space,
|
||||||
}},
|
}},
|
||||||
})
|
}
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
// Hardcodes the XML namespace. See comment in Write() for context.
|
||||||
|
return e.EncodeElement(a.Args, startElement)
|
||||||
|
case reflect.Map:
|
||||||
|
if err := e.EncodeToken(startElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
kt := t.Key()
|
||||||
|
if kt.Kind() != reflect.String {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action wants string as map key in args: %w",
|
||||||
|
&xml.UnsupportedTypeError{Type: kt})
|
||||||
|
}
|
||||||
|
iter := v.MapRange()
|
||||||
|
for iter.Next() {
|
||||||
|
k := iter.Key()
|
||||||
|
// TODO: does this support string newtypes? convert?
|
||||||
|
ks := k.Interface().(string)
|
||||||
|
v := iter.Value()
|
||||||
|
ke := xml.StartElement{Name: xml.Name{Local: ks}}
|
||||||
|
if err := e.EncodeElement(v.Interface(), ke); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action error while encoding arg %q: %w", ks, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.EncodeToken(xml.EndElement{Name: elemName})
|
||||||
|
default:
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action does not support type as args: %w",
|
||||||
|
&xml.UnsupportedTypeError{Type: t})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ xml.Unmarshaler = &Action{}
|
var _ xml.Unmarshaler = &Action{}
|
||||||
@ -83,7 +119,75 @@ var _ xml.Unmarshaler = &Action{}
|
|||||||
// action element into the struct in `a.Args`.
|
// action element into the struct in `a.Args`.
|
||||||
func (a *Action) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
func (a *Action) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
a.XMLName = start.Name
|
a.XMLName = start.Name
|
||||||
return d.DecodeElement(a.Args, &start)
|
argsValue := reflect.Indirect(reflect.ValueOf(a.Args))
|
||||||
|
argsType := argsValue.Type()
|
||||||
|
switch argsType.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
return d.DecodeElement(a.Args, &start)
|
||||||
|
case reflect.Map:
|
||||||
|
keyType := argsType.Key()
|
||||||
|
if keyType.Kind() != reflect.String {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action wants string as map key in args: %w",
|
||||||
|
&xml.UnsupportedTypeError{Type: keyType})
|
||||||
|
}
|
||||||
|
valueType := argsType.Elem()
|
||||||
|
if valueType.Kind() == reflect.Interface {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action wants a concrete type as map value in args: %w",
|
||||||
|
&xml.UnsupportedTypeError{Type: valueType})
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
untypedToken, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch token := untypedToken.(type) {
|
||||||
|
case xml.EndElement:
|
||||||
|
return nil
|
||||||
|
case xml.StartElement:
|
||||||
|
if len(token.Attr) > 0 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action arg does not support attributes, got %v",
|
||||||
|
token.Attr)
|
||||||
|
}
|
||||||
|
if token.Name.Space != "" {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action arg does not support non-empty namespace, got %q",
|
||||||
|
token.Name.Space)
|
||||||
|
}
|
||||||
|
key := token.Name.Local
|
||||||
|
value := reflect.New(valueType)
|
||||||
|
if err := d.DecodeElement(value.Interface(), &token); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action arg %q errored while decoding: %w", key, err)
|
||||||
|
}
|
||||||
|
argsValue.SetMapIndex(reflect.ValueOf(key), reflect.Indirect(value))
|
||||||
|
case xml.Comment:
|
||||||
|
case xml.ProcInst:
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action args contained unexpected token %v",
|
||||||
|
untypedToken)
|
||||||
|
case xml.Directive:
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action args contained unexpected token %v",
|
||||||
|
untypedToken)
|
||||||
|
case xml.CharData:
|
||||||
|
cd := string(token)
|
||||||
|
if len(strings.TrimSpace(cd)) > 0 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action args contained stray text: %q", cd)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action found unknown XML token type: %T", untypedToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf(
|
||||||
|
"SOAP action does not support type as args: %w",
|
||||||
|
&xml.UnsupportedTypeError{Type: argsType})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Various "constant" bytes used in the written envelope.
|
// Various "constant" bytes used in the written envelope.
|
||||||
|
@ -12,39 +12,62 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testArgs struct {
|
type testStructArgs struct {
|
||||||
Foo string
|
Foo string
|
||||||
Bar string
|
Bar string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWriteRead tests the round-trip of writing an envelope and reading it back.
|
// TestWriteRead tests the round-trip of writing an envelope and reading it back.
|
||||||
func TestWriteRead(t *testing.T) {
|
func TestWriteRead(t *testing.T) {
|
||||||
argsIn := &testArgs{
|
tests := []struct {
|
||||||
Foo: "foo-1",
|
name string
|
||||||
Bar: "bar-2",
|
argsIn any
|
||||||
}
|
argsOut any
|
||||||
actionIn := NewSendAction("urn:schemas-upnp-org:service:FakeService:1", "MyAction", argsIn)
|
}{
|
||||||
|
{
|
||||||
buf := &bytes.Buffer{}
|
"struct",
|
||||||
err := Write(buf, actionIn)
|
&testStructArgs{
|
||||||
if err != nil {
|
Foo: "foo-1",
|
||||||
t.Fatalf("Write want success, got err=%v", err)
|
Bar: "bar-2",
|
||||||
}
|
},
|
||||||
t.Logf("Encoded envelope:\n%v", buf)
|
&testStructArgs{},
|
||||||
|
},
|
||||||
argsOut := &testArgs{}
|
{
|
||||||
actionOut := NewRecvAction(argsOut)
|
"map",
|
||||||
|
map[string]string{
|
||||||
err = Read(buf, actionOut)
|
"Foo": "foo-1",
|
||||||
if err != nil {
|
"Bar": "bar-2",
|
||||||
t.Errorf("Read want success, got err=%v", err)
|
},
|
||||||
|
map[string]string{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if diff := cmp.Diff(actionIn, actionOut); diff != "" {
|
for _, test := range tests {
|
||||||
t.Errorf("\nwant actionOut=%+v\ngot %+v\ndiff:\n%s", actionIn, actionOut, diff)
|
test := test // copy for closure
|
||||||
}
|
t.Run(test.name, func(t *testing.T) {
|
||||||
if diff := cmp.Diff(argsIn, argsOut); diff != "" {
|
actionIn := NewSendAction("urn:schemas-upnp-org:service:FakeService:1", "MyAction", test.argsIn)
|
||||||
t.Errorf("\nwant argsOut=%+v\ngot %+v\ndiff:\n%s", argsIn, argsOut, diff)
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
err := Write(buf, actionIn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Write want success, got err=%v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Encoded envelope:\n%v", buf)
|
||||||
|
|
||||||
|
actionOut := NewRecvAction(test.argsOut)
|
||||||
|
|
||||||
|
err = Read(buf, actionOut)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Read want success, got err=%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(actionIn, actionOut); diff != "" {
|
||||||
|
t.Errorf("\nwant actionOut=%+v\ngot %+v\ndiff:\n%s", actionIn, actionOut, diff)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(test.argsIn, test.argsOut); diff != "" {
|
||||||
|
t.Errorf("\nwant argsOut=%+v\ngot %+v\ndiff:\n%s", test.argsIn, test.argsOut, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +79,7 @@ func TestRead(t *testing.T) {
|
|||||||
<Bar>bar-2</Bar>
|
<Bar>bar-2</Bar>
|
||||||
</u:FakeAction>
|
</u:FakeAction>
|
||||||
</s:Body> </s:Envelope>`)
|
</s:Body> </s:Envelope>`)
|
||||||
argsOut := &testArgs{}
|
argsOut := &testStructArgs{}
|
||||||
|
|
||||||
actionOut := NewRecvAction(argsOut)
|
actionOut := NewRecvAction(argsOut)
|
||||||
|
|
||||||
@ -64,7 +87,7 @@ func TestRead(t *testing.T) {
|
|||||||
t.Fatalf("Read want success, got err=%v", err)
|
t.Fatalf("Read want success, got err=%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantArgsOut := &testArgs{
|
wantArgsOut := &testStructArgs{
|
||||||
Foo: "foo-1",
|
Foo: "foo-1",
|
||||||
Bar: "bar-2",
|
Bar: "bar-2",
|
||||||
}
|
}
|
||||||
@ -89,7 +112,7 @@ s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|||||||
</s:Envelope>
|
</s:Envelope>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
err := Read(bytes.NewBuffer(env), NewRecvAction(&testArgs{}))
|
err := Read(bytes.NewBuffer(env), NewRecvAction(&testStructArgs{}))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("want err != nil, got nil")
|
t.Fatal("want err != nil, got nil")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user