diff --git a/v2alpha/cmd/goupnp2srvgen/main.go b/v2alpha/cmd/goupnp2srvgen/main.go index cc53222..3f34b27 100644 --- a/v2alpha/cmd/goupnp2srvgen/main.go +++ b/v2alpha/cmd/goupnp2srvgen/main.go @@ -6,14 +6,22 @@ import ( "flag" "fmt" "os" + "path/filepath" + "sort" + "strconv" "strings" + "text/template" + "github.com/huin/goupnp/v2alpha/cmd/goupnp2srvgen/tmplfuncs" "github.com/huin/goupnp/v2alpha/cmd/goupnp2srvgen/zipread" "github.com/huin/goupnp/v2alpha/description/srvdesc" + "github.com/huin/goupnp/v2alpha/description/typedesc" "github.com/huin/goupnp/v2alpha/description/xmlsrvdesc" + "github.com/huin/goupnp/v2alpha/soap/types" ) var ( + srvTemplate = flag.String("srv_template", "", "Path to srv.gotemplate.") upnpresourcesZip = flag.String("upnpresources_zip", "", "Path to upnpresources.zip.") ) @@ -29,6 +37,17 @@ func run() error { if len(flag.Args()) > 0 { return fmt.Errorf("unused arguments: %s", strings.Join(flag.Args(), " ")) } + if *srvTemplate == "" { + return errors.New("-srv_template is a required flag.") + } + tmpl, err := template.New(filepath.Base(*srvTemplate)).Funcs(template.FuncMap{ + "args": tmplfuncs.Args, + "quote": strconv.Quote, + }).ParseFiles(*srvTemplate) + if err != nil { + return fmt.Errorf("loading srv_template %q: %w", *srvTemplate, err) + } + if *upnpresourcesZip == "" { return errors.New("-upnpresources_zip is a required flag.") } @@ -41,8 +60,13 @@ func run() error { if err != nil { return err } + + // Use default type map for now. Addtional types could be use instead or + // as well as necessary for extended types. + typeMap := types.TypeMap() + for _, m := range manifests { - if err := processDCP(upnpresources, m); err != nil { + if err := processDCP(upnpresources, m, typeMap, tmpl); err != nil { return fmt.Errorf("processing DCP %s: %w", m.Path, err) } } @@ -52,9 +76,17 @@ func run() error { var manifests = []*DCPSpecManifest{ { Path: "standardizeddcps/Internet Gateway_2/UPnP-gw-IGD-TestFiles-20101210.zip", - Services: map[string]string{ - "LANHostConfigManagement:1": "xml data files/service/LANHostConfigManagement1.xml", - "WANPPPConnection:1": "xml data files/service/WANPPPConnection1.xml", + Services: []*ServiceManifest{ + { + Package: "lanhostconfigmanagement1", + Type: "urn:schemas-upnp-org:service:LANHostConfigManagement:1", + Path: "xml data files/service/LANHostConfigManagement1.xml", + }, + { + Package: "wanpppconnection1", + Type: "urn:schemas-upnp-org:service:WANPPPConnection:1", + Path: "xml data files/service/WANPPPConnection1.xml", + }, }, }, } @@ -62,14 +94,16 @@ var manifests = []*DCPSpecManifest{ func processDCP( upnpresources *zipread.ZipRead, manifest *DCPSpecManifest, + typeMap typedesc.TypeMap, + tmpl *template.Template, ) error { dcpSpecData, err := upnpresources.OpenZip(manifest.Path) if err != nil { return err } - for name, path := range manifest.Services { - if err := processService(dcpSpecData, name, path); err != nil { - return fmt.Errorf("processing service %s: %w", name, err) + for _, srvManifest := range manifest.Services { + if err := processService(dcpSpecData, srvManifest, typeMap, tmpl); err != nil { + return fmt.Errorf("processing service %s: %w", srvManifest.Type, err) } } return nil @@ -77,11 +111,11 @@ func processDCP( func processService( dcpSpecData *zipread.ZipRead, - name string, - path string, + srvManifest *ServiceManifest, + typeMap typedesc.TypeMap, + tmpl *template.Template, ) error { - fmt.Printf("%s\n", name) - f, err := dcpSpecData.Open(path) + f, err := dcpSpecData.Open(srvManifest.Path) if err != nil { return err } @@ -108,10 +142,24 @@ func processService( } } - _, err = srvdesc.FromXML(xmlSCPD) + sd, err := srvdesc.FromXML(xmlSCPD) + if err != nil { + return fmt.Errorf("transforming service description: %w", err) + } + + imps, err := accumulateImports(sd, typeMap) if err != nil { return err } + + err = tmpl.ExecuteTemplate(os.Stdout, "service", tmplArgs{ + Imps: imps, + SCPD: sd, + }) + if err != nil { + return fmt.Errorf("executing srv_template: %w", err) + } + return nil } @@ -120,5 +168,128 @@ type DCPSpecManifest struct { Path string // Services maps from a service name (e.g. "FooBar:1") to a path within the DCP spec ZIP file // (e.g. "xml data files/service/FooBar1.xml"). - Services map[string]string + Services []*ServiceManifest +} + +type ServiceManifest struct { + // Package is the Go package name to generate e.g. "foo1". + Package string + // Type is the SOAP namespace and service type that identifes the service e.g. + // "urn:schemas-upnp-org:service:Foo:1" + Type string + // Path within the DCP spec ZIP file e.g. "xml data files/service/Foo1.xml". + Path string +} + +type tmplArgs struct { + Imps *imports + SCPD *srvdesc.SCPD +} + +type imports struct { + // Maps from a type name like "ui4" to the `alias.name` for the import. + TypeRefByTypeName map[string]string + // Each required import line, ordered by path. + ImportLines []importItem +} + +type importItem struct { + Alias string + Path string +} + +func accumulateImports(srvDesc *srvdesc.SCPD, typeMap typedesc.TypeMap) (*imports, error) { + typeNames := make(map[string]bool) + err := visitTypesSCPD(srvDesc, func(typeName string) { + typeNames[typeName] = true + }) + if err != nil { + return nil, err + } + + // Have sorted list of import package paths. Partly for aesthetics of generated code, but also + // to have stable-generated aliases. + paths := make(map[string]bool) + for typeName := range typeNames { + t, ok := typeMap[typeName] + if !ok { + return nil, fmt.Errorf("unknown type %q", typeName) + } + pkgPath := t.GoType.PkgPath() + if pkgPath == "" { + // Builtin type, ignore. + continue + } + paths[pkgPath] = true + } + sortedPaths := make([]string, 0, len(paths)) + for path := range paths { + sortedPaths = append(sortedPaths, path) + } + sort.Strings(sortedPaths) + + // Generate import aliases. + index := 1 + aliasByPath := make(map[string]string, len(paths)) + importLines := make([]importItem, 0, len(paths)) + for _, path := range sortedPaths { + alias := fmt.Sprintf("pkg%d", index) + index++ + importLines = append(importLines, importItem{ + Alias: alias, + Path: path, + }) + aliasByPath[path] = alias + } + + // Populate typeRefByTypeName. + typeRefByTypeName := make(map[string]string, len(typeNames)) + for typeName := range typeNames { + goType := typeMap[typeName] + pkgPath := goType.GoType.PkgPath() + alias := aliasByPath[pkgPath] + if alias == "" { + // Builtin type. + typeRefByTypeName[typeName] = goType.GoType.Name() + } else { + typeRefByTypeName[typeName] = fmt.Sprintf( + "%s.%s", alias, goType.GoType.Name()) + } + } + + return &imports{ + TypeRefByTypeName: typeRefByTypeName, + ImportLines: importLines, + }, nil +} + +type typeVisitor func(typeName string) + +// visitTypesSCPD calls `visitor` with each data type name (e.g. "ui4") referenced +// by action arguments.` +func visitTypesSCPD(scpd *srvdesc.SCPD, visitor typeVisitor) error { + for _, action := range scpd.ActionByName { + if err := visitTypesAction(action, visitor); err != nil { + return err + } + } + return nil +} + +func visitTypesAction(action *srvdesc.Action, visitor typeVisitor) error { + for _, arg := range action.InArgs { + sv, err := arg.RelatedStateVariable() + if err != nil { + return err + } + visitor(sv.DataType) + } + for _, arg := range action.OutArgs { + sv, err := arg.RelatedStateVariable() + if err != nil { + return err + } + visitor(sv.DataType) + } + return nil } diff --git a/v2alpha/cmd/goupnp2srvgen/tmplfuncs/tmplfuncs.go b/v2alpha/cmd/goupnp2srvgen/tmplfuncs/tmplfuncs.go new file mode 100644 index 0000000..dbfb9e0 --- /dev/null +++ b/v2alpha/cmd/goupnp2srvgen/tmplfuncs/tmplfuncs.go @@ -0,0 +1,30 @@ +// Package tmplfuncs contains functions for injection into templates. +package tmplfuncs + +import ( + "errors" + "fmt" +) + +// Args accepts pairs of string names and any values and constructs a map from them. +// This is to help passing multiple arguments to a template from within a template. +func Args(args ...any) (map[string]any, error) { + if len(args)%2 != 0 { + return nil, errors.New("args must have an even number of arguments") + } + res := make(map[string]any, len(args)/2) + + for i := 0; i < len(args); i += 2 { + name, ok := args[i].(string) + if !ok { + return nil, fmt.Errorf("argument %d: want string, got %T", i, args[i]) + } + value := args[i+1] + if _, exists := res[name]; exists { + return nil, fmt.Errorf("argument name %q occurs more than once", name) + } + res[name] = value + } + + return res, nil +} diff --git a/v2alpha/description/srvdesc/srvdesc.go b/v2alpha/description/srvdesc/srvdesc.go index fd57bd1..4e36c41 100644 --- a/v2alpha/description/srvdesc/srvdesc.go +++ b/v2alpha/description/srvdesc/srvdesc.go @@ -11,85 +11,84 @@ import ( var ( BadDescriptionError = errors.New("bad XML description") + MissingDefinitionError = errors.New("missing definition") UnsupportedDescriptionError = errors.New("unsupported XML description") ) // SCPD is the top level service description. type SCPD struct { - actionByName map[string]*Action - variableByName map[string]*StateVariable + ActionByName map[string]*Action + VariableByName map[string]*StateVariable } // FromXML creates an SCPD from XML data. // // It assumes that xmlDesc.Clean() has been called. func FromXML(xmlDesc *xmlsrvdesc.SCPD) (*SCPD, error) { - stateVariables := make(map[string]*StateVariable, len(xmlDesc.StateVariables)) + scpd := &SCPD{ + ActionByName: make(map[string]*Action, len(xmlDesc.Actions)), + VariableByName: make(map[string]*StateVariable, len(xmlDesc.StateVariables)), + } + stateVariables := scpd.VariableByName for _, xmlSV := range xmlDesc.StateVariables { sv, err := stateVariableFromXML(xmlSV) if err != nil { return nil, fmt.Errorf("processing state variable %q: %w", xmlSV.Name, err) } - if _, exists := stateVariables[sv.name]; exists { + if _, exists := stateVariables[sv.Name]; exists { return nil, fmt.Errorf("%w: multiple state variables with name %q", - BadDescriptionError, sv.name) + BadDescriptionError, sv.Name) } - stateVariables[sv.name] = sv + stateVariables[sv.Name] = sv } - actions := make(map[string]*Action, len(xmlDesc.Actions)) + actions := scpd.ActionByName for _, xmlAction := range xmlDesc.Actions { - action, err := actionFromXML(xmlAction) + action, err := actionFromXML(xmlAction, scpd) if err != nil { return nil, fmt.Errorf("processing action %q: %w", xmlAction.Name, err) } - if _, exists := actions[action.name]; exists { + if _, exists := actions[action.Name]; exists { return nil, fmt.Errorf("%w: multiple actions with name %q", - BadDescriptionError, action.name) + BadDescriptionError, action.Name) } - actions[action.name] = action + actions[action.Name] = action } - return &SCPD{ - actionByName: actions, - variableByName: stateVariables, - }, nil + return scpd, nil } -// ActionNames returns the ordered names of each action. -func (scpd *SCPD) ActionNames() []string { - names := make([]string, 0, len(scpd.actionByName)) - for name := range scpd.actionByName { - names = append(names, name) +// SortedActions returns the actions, in order of name. +func (scpd *SCPD) SortedActions() []*Action { + actions := make([]*Action, 0, len(scpd.ActionByName)) + for _, a := range scpd.ActionByName { + actions = append(actions, a) } - sort.Strings(names) - return names -} - -// Action returns an action with the given name. -func (scpd *SCPD) Action(name string) *Action { - return scpd.actionByName[name] -} - -// Variable returns a state variable with the given name. -func (scpd *SCPD) Variable(name string) *StateVariable { - return scpd.variableByName[name] + sort.Slice(actions, func(i, j int) bool { + return actions[i].Name < actions[j].Name + }) + return actions } // Action describes a single UPnP SOAP action. type Action struct { - name string - inArgs []*Argument - outArgs []*Argument + SCPD *SCPD + Name string + InArgs []*Argument + OutArgs []*Argument } // actionFromXML creates an Action from the given XML description. -func actionFromXML(xmlAction *xmlsrvdesc.Action) (*Action, error) { +func actionFromXML(xmlAction *xmlsrvdesc.Action, scpd *SCPD) (*Action, error) { if xmlAction.Name == "" { return nil, fmt.Errorf("%w: empty action name", BadDescriptionError) } + action := &Action{ + SCPD: scpd, + Name: xmlAction.Name, + } var inArgs []*Argument var outArgs []*Argument for _, xmlArg := range xmlAction.Arguments { - arg, err := argumentFromXML(xmlArg) + arg, err := argumentFromXML(xmlArg, action) if err != nil { return nil, fmt.Errorf("processing argument %q: %w", xmlArg.Name, err) } @@ -103,21 +102,20 @@ func actionFromXML(xmlAction *xmlsrvdesc.Action) (*Action, error) { BadDescriptionError, xmlArg.Name, xmlArg.Direction) } } - return &Action{ - name: xmlAction.Name, - inArgs: inArgs, - outArgs: outArgs, - }, nil + action.InArgs = inArgs + action.OutArgs = outArgs + return action, nil } // Argument description data. type Argument struct { - name string - relatedStateVariable string + Action *Action + Name string + RelatedStateVariableName string } // argumentFromXML creates an Argument from the XML description. -func argumentFromXML(xmlArg *xmlsrvdesc.Argument) (*Argument, error) { +func argumentFromXML(xmlArg *xmlsrvdesc.Argument, action *Action) (*Argument, error) { if xmlArg.Name == "" { return nil, fmt.Errorf("%w: empty argument name", BadDescriptionError) } @@ -125,23 +123,23 @@ func argumentFromXML(xmlArg *xmlsrvdesc.Argument) (*Argument, error) { return nil, fmt.Errorf("%w: empty related state variable", BadDescriptionError) } return &Argument{ - name: xmlArg.Name, - relatedStateVariable: xmlArg.RelatedStateVariable, + Action: action, + Name: xmlArg.Name, + RelatedStateVariableName: xmlArg.RelatedStateVariable, }, nil } -func (arg *Argument) Name() string { - return arg.name -} - -func (arg *Argument) RelatedStateVariableName() string { - return arg.relatedStateVariable +func (arg *Argument) RelatedStateVariable() (*StateVariable, error) { + if v, ok := arg.Action.SCPD.VariableByName[arg.RelatedStateVariableName]; ok { + return v, nil + } + return nil, fmt.Errorf("%w: state variable %q", MissingDefinitionError, arg.RelatedStateVariableName) } // StateVariable description data. type StateVariable struct { - name string - dataType string + Name string + DataType string } func stateVariableFromXML(xmlSV *xmlsrvdesc.StateVariable) (*StateVariable, error) { @@ -153,15 +151,7 @@ func stateVariableFromXML(xmlSV *xmlsrvdesc.StateVariable) (*StateVariable, erro UnsupportedDescriptionError, xmlSV.DataType.Type) } return &StateVariable{ - name: xmlSV.Name, - dataType: xmlSV.DataType.Name, + Name: xmlSV.Name, + DataType: xmlSV.DataType.Name, }, nil } - -func (sv *StateVariable) Name() string { - return sv.name -} - -func (sv *StateVariable) DataTypeName() string { - return sv.dataType -} diff --git a/v2alpha/description/typedesc/typedesc.go b/v2alpha/description/typedesc/typedesc.go new file mode 100644 index 0000000..7cc9074 --- /dev/null +++ b/v2alpha/description/typedesc/typedesc.go @@ -0,0 +1,9 @@ +package typedesc + +import "reflect" + +type TypeDesc struct { + GoType reflect.Type +} + +type TypeMap map[string]TypeDesc diff --git a/v2alpha/soap/types/types.go b/v2alpha/soap/types/types.go index 853dbc8..c34c649 100644 --- a/v2alpha/soap/types/types.go +++ b/v2alpha/soap/types/types.go @@ -14,12 +14,47 @@ import ( "errors" "fmt" "net/url" + "reflect" "regexp" "strconv" "time" "unicode/utf8" + + "github.com/huin/goupnp/v2alpha/description/typedesc" ) +// TypeMap returns the builtin type map description. +func TypeMap() typedesc.TypeMap { + return typedesc.TypeMap{ + "ui1": {GoType: reflect.TypeOf(UI1(0))}, + "ui2": {GoType: reflect.TypeOf(UI2(0))}, + "ui4": {GoType: reflect.TypeOf(UI4(0))}, + "ui8": {GoType: reflect.TypeOf(UI8(0))}, + "i1": {GoType: reflect.TypeOf(I1(0))}, + "i2": {GoType: reflect.TypeOf(I2(0))}, + "i4": {GoType: reflect.TypeOf(I4(0))}, + "i8": {GoType: reflect.TypeOf(I8(0))}, + "int": {GoType: reflect.TypeOf(I8(0))}, + "r4": {GoType: reflect.TypeOf(R4(0))}, + "r8": {GoType: reflect.TypeOf(R8(0))}, + "number": {GoType: reflect.TypeOf(R8(0))}, + "fixed.14.4": {GoType: reflect.TypeOf(Fixed14_4{})}, + "float": {GoType: reflect.TypeOf(R8(0))}, + "char": {GoType: reflect.TypeOf(Char(' '))}, + "string": {GoType: reflect.TypeOf("")}, + "date": {GoType: reflect.TypeOf(Date{})}, + "dateTime": {GoType: reflect.TypeOf(DateTime{})}, + "dateTime.tz": {GoType: reflect.TypeOf(DateTimeTZ{})}, + "time": {GoType: reflect.TypeOf(TimeOfDay{})}, + "time.tz": {GoType: reflect.TypeOf(TimeOfDayTZ{})}, + "boolean": {GoType: reflect.TypeOf(Boolean(false))}, + "bin.base64": {GoType: reflect.TypeOf(BinBase64{})}, + "bin.hex": {GoType: reflect.TypeOf(BinHex{})}, + "uri": {GoType: reflect.TypeOf(URI{})}, + "uuid": {GoType: reflect.TypeOf("")}, + } +} + type SOAPValue interface { encoding.TextMarshaler encoding.TextUnmarshaler diff --git a/v2alpha/srv/srv.gotemplate b/v2alpha/srv/srv.gotemplate new file mode 100644 index 0000000..e8dc16e --- /dev/null +++ b/v2alpha/srv/srv.gotemplate @@ -0,0 +1,31 @@ +{{define "service"}} +{{- $Imps := .Imps}} +package TODO + +import ( +{{- range .Imps.ImportLines}} + {{quote .Alias}} {{.Path}} +{{- end}} +) + +{{range .SCPD.SortedActions}} +{{- template "action" args "Action" . "Imps" $Imps}} +{{end}} +{{end}} + +{{define "action"}} +type {{.Action.Name}}Request struct { +{{- template "args" args "Args" .Action.InArgs "Imps" .Imps -}} +} + +type {{.Action.Name}}Response struct { +{{- template "args" args "Args" .Action.OutArgs "Imps" .Imps -}} +} +{{- end}} + +{{define "args"}} +{{- $Imps := .Imps}} +{{- range .Args}} + {{.Name}} {{index $Imps.TypeRefByTypeName .RelatedStateVariable.DataType}} +{{end}} +{{- end}}