First draft supporting maps as action args.

This commit is contained in:
John Beisley 2022-03-31 18:09:53 +01:00
parent 5d0813cf55
commit f69c4d0ee2
2 changed files with 160 additions and 33 deletions

View File

@ -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.

View File

@ -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")
} }