First draft supporting maps as action args.
This commit is contained in:
		@@ -6,6 +6,8 @@ import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
// action element from the struct in `a.Args`.
 | 
			
		||||
func (a *Action) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
 | 
			
		||||
	// Hardcodes the XML namespace. See comment in Write() for context.
 | 
			
		||||
	return e.EncodeElement(a.Args, xml.StartElement{
 | 
			
		||||
		Name: xml.Name{Space: "", Local: "u:" + a.XMLName.Local},
 | 
			
		||||
	v := reflect.Indirect(reflect.ValueOf(a.Args))
 | 
			
		||||
	t := v.Type()
 | 
			
		||||
	elemName := xml.Name{Space: "", Local: "u:" + a.XMLName.Local}
 | 
			
		||||
	startElement := xml.StartElement{
 | 
			
		||||
		Name: elemName,
 | 
			
		||||
		Attr: []xml.Attr{{
 | 
			
		||||
			Name:  xml.Name{Space: "", Local: "xmlns:u"},
 | 
			
		||||
			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{}
 | 
			
		||||
@@ -83,7 +119,75 @@ var _ xml.Unmarshaler = &Action{}
 | 
			
		||||
// action element into the struct in `a.Args`.
 | 
			
		||||
func (a *Action) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
 | 
			
		||||
	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.
 | 
			
		||||
 
 | 
			
		||||
@@ -12,39 +12,62 @@ import (
 | 
			
		||||
	"github.com/google/go-cmp/cmp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type testArgs struct {
 | 
			
		||||
type testStructArgs struct {
 | 
			
		||||
	Foo string
 | 
			
		||||
	Bar string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TestWriteRead tests the round-trip of writing an envelope and reading it back.
 | 
			
		||||
func TestWriteRead(t *testing.T) {
 | 
			
		||||
	argsIn := &testArgs{
 | 
			
		||||
		Foo: "foo-1",
 | 
			
		||||
		Bar: "bar-2",
 | 
			
		||||
	}
 | 
			
		||||
	actionIn := NewSendAction("urn:schemas-upnp-org:service:FakeService:1", "MyAction", argsIn)
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
	argsOut := &testArgs{}
 | 
			
		||||
	actionOut := NewRecvAction(argsOut)
 | 
			
		||||
 | 
			
		||||
	err = Read(buf, actionOut)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Read want success, got err=%v", err)
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		argsIn  any
 | 
			
		||||
		argsOut any
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			"struct",
 | 
			
		||||
			&testStructArgs{
 | 
			
		||||
				Foo: "foo-1",
 | 
			
		||||
				Bar: "bar-2",
 | 
			
		||||
			},
 | 
			
		||||
			&testStructArgs{},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"map",
 | 
			
		||||
			map[string]string{
 | 
			
		||||
				"Foo": "foo-1",
 | 
			
		||||
				"Bar": "bar-2",
 | 
			
		||||
			},
 | 
			
		||||
			map[string]string{},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if diff := cmp.Diff(actionIn, actionOut); diff != "" {
 | 
			
		||||
		t.Errorf("\nwant actionOut=%+v\ngot  %+v\ndiff:\n%s", actionIn, actionOut, diff)
 | 
			
		||||
	}
 | 
			
		||||
	if diff := cmp.Diff(argsIn, argsOut); diff != "" {
 | 
			
		||||
		t.Errorf("\nwant argsOut=%+v\ngot  %+v\ndiff:\n%s", argsIn, argsOut, diff)
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		test := test // copy for closure
 | 
			
		||||
		t.Run(test.name, func(t *testing.T) {
 | 
			
		||||
			actionIn := NewSendAction("urn:schemas-upnp-org:service:FakeService:1", "MyAction", test.argsIn)
 | 
			
		||||
 | 
			
		||||
			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>
 | 
			
		||||
</u:FakeAction>
 | 
			
		||||
</s:Body> </s:Envelope>`)
 | 
			
		||||
	argsOut := &testArgs{}
 | 
			
		||||
	argsOut := &testStructArgs{}
 | 
			
		||||
 | 
			
		||||
	actionOut := NewRecvAction(argsOut)
 | 
			
		||||
 | 
			
		||||
@@ -64,7 +87,7 @@ func TestRead(t *testing.T) {
 | 
			
		||||
		t.Fatalf("Read want success, got err=%v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wantArgsOut := &testArgs{
 | 
			
		||||
	wantArgsOut := &testStructArgs{
 | 
			
		||||
		Foo: "foo-1",
 | 
			
		||||
		Bar: "bar-2",
 | 
			
		||||
	}
 | 
			
		||||
@@ -89,7 +112,7 @@ s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
 | 
			
		||||
</s:Envelope>
 | 
			
		||||
`)
 | 
			
		||||
 | 
			
		||||
	err := Read(bytes.NewBuffer(env), NewRecvAction(&testArgs{}))
 | 
			
		||||
	err := Read(bytes.NewBuffer(env), NewRecvAction(&testStructArgs{}))
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		t.Fatal("want err != nil, got nil")
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user