Initial code to generate service code.

This commit is contained in:
John Beisley 2022-06-06 18:00:39 +01:00
parent 0e8fff04df
commit e3e16da35f
6 changed files with 344 additions and 78 deletions

View File

@ -6,14 +6,22 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"path/filepath"
"sort"
"strconv"
"strings" "strings"
"text/template"
"github.com/huin/goupnp/v2alpha/cmd/goupnp2srvgen/tmplfuncs"
"github.com/huin/goupnp/v2alpha/cmd/goupnp2srvgen/zipread" "github.com/huin/goupnp/v2alpha/cmd/goupnp2srvgen/zipread"
"github.com/huin/goupnp/v2alpha/description/srvdesc" "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/description/xmlsrvdesc"
"github.com/huin/goupnp/v2alpha/soap/types"
) )
var ( var (
srvTemplate = flag.String("srv_template", "", "Path to srv.gotemplate.")
upnpresourcesZip = flag.String("upnpresources_zip", "", "Path to upnpresources.zip.") upnpresourcesZip = flag.String("upnpresources_zip", "", "Path to upnpresources.zip.")
) )
@ -29,6 +37,17 @@ func run() error {
if len(flag.Args()) > 0 { if len(flag.Args()) > 0 {
return fmt.Errorf("unused arguments: %s", strings.Join(flag.Args(), " ")) 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 == "" { if *upnpresourcesZip == "" {
return errors.New("-upnpresources_zip is a required flag.") return errors.New("-upnpresources_zip is a required flag.")
} }
@ -41,8 +60,13 @@ func run() error {
if err != nil { if err != nil {
return err 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 { 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) return fmt.Errorf("processing DCP %s: %w", m.Path, err)
} }
} }
@ -52,9 +76,17 @@ func run() error {
var manifests = []*DCPSpecManifest{ var manifests = []*DCPSpecManifest{
{ {
Path: "standardizeddcps/Internet Gateway_2/UPnP-gw-IGD-TestFiles-20101210.zip", Path: "standardizeddcps/Internet Gateway_2/UPnP-gw-IGD-TestFiles-20101210.zip",
Services: map[string]string{ Services: []*ServiceManifest{
"LANHostConfigManagement:1": "xml data files/service/LANHostConfigManagement1.xml", {
"WANPPPConnection:1": "xml data files/service/WANPPPConnection1.xml", 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( func processDCP(
upnpresources *zipread.ZipRead, upnpresources *zipread.ZipRead,
manifest *DCPSpecManifest, manifest *DCPSpecManifest,
typeMap typedesc.TypeMap,
tmpl *template.Template,
) error { ) error {
dcpSpecData, err := upnpresources.OpenZip(manifest.Path) dcpSpecData, err := upnpresources.OpenZip(manifest.Path)
if err != nil { if err != nil {
return err return err
} }
for name, path := range manifest.Services { for _, srvManifest := range manifest.Services {
if err := processService(dcpSpecData, name, path); err != nil { if err := processService(dcpSpecData, srvManifest, typeMap, tmpl); err != nil {
return fmt.Errorf("processing service %s: %w", name, err) return fmt.Errorf("processing service %s: %w", srvManifest.Type, err)
} }
} }
return nil return nil
@ -77,11 +111,11 @@ func processDCP(
func processService( func processService(
dcpSpecData *zipread.ZipRead, dcpSpecData *zipread.ZipRead,
name string, srvManifest *ServiceManifest,
path string, typeMap typedesc.TypeMap,
tmpl *template.Template,
) error { ) error {
fmt.Printf("%s\n", name) f, err := dcpSpecData.Open(srvManifest.Path)
f, err := dcpSpecData.Open(path)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err 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 return nil
} }
@ -120,5 +168,128 @@ type DCPSpecManifest struct {
Path string Path string
// Services maps from a service name (e.g. "FooBar:1") to a path within the DCP spec ZIP file // 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"). // (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
} }

View File

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

View File

@ -11,85 +11,84 @@ import (
var ( var (
BadDescriptionError = errors.New("bad XML description") BadDescriptionError = errors.New("bad XML description")
MissingDefinitionError = errors.New("missing definition")
UnsupportedDescriptionError = errors.New("unsupported XML description") UnsupportedDescriptionError = errors.New("unsupported XML description")
) )
// SCPD is the top level service description. // SCPD is the top level service description.
type SCPD struct { type SCPD struct {
actionByName map[string]*Action ActionByName map[string]*Action
variableByName map[string]*StateVariable VariableByName map[string]*StateVariable
} }
// FromXML creates an SCPD from XML data. // FromXML creates an SCPD from XML data.
// //
// It assumes that xmlDesc.Clean() has been called. // It assumes that xmlDesc.Clean() has been called.
func FromXML(xmlDesc *xmlsrvdesc.SCPD) (*SCPD, error) { 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 { for _, xmlSV := range xmlDesc.StateVariables {
sv, err := stateVariableFromXML(xmlSV) sv, err := stateVariableFromXML(xmlSV)
if err != nil { if err != nil {
return nil, fmt.Errorf("processing state variable %q: %w", xmlSV.Name, err) 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", 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 { for _, xmlAction := range xmlDesc.Actions {
action, err := actionFromXML(xmlAction) action, err := actionFromXML(xmlAction, scpd)
if err != nil { if err != nil {
return nil, fmt.Errorf("processing action %q: %w", xmlAction.Name, err) 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", 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{ return scpd, nil
actionByName: actions,
variableByName: stateVariables,
}, nil
} }
// ActionNames returns the ordered names of each action. // SortedActions returns the actions, in order of name.
func (scpd *SCPD) ActionNames() []string { func (scpd *SCPD) SortedActions() []*Action {
names := make([]string, 0, len(scpd.actionByName)) actions := make([]*Action, 0, len(scpd.ActionByName))
for name := range scpd.actionByName { for _, a := range scpd.ActionByName {
names = append(names, name) actions = append(actions, a)
} }
sort.Strings(names) sort.Slice(actions, func(i, j int) bool {
return names return actions[i].Name < actions[j].Name
} })
return actions
// 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]
} }
// Action describes a single UPnP SOAP action. // Action describes a single UPnP SOAP action.
type Action struct { type Action struct {
name string SCPD *SCPD
inArgs []*Argument Name string
outArgs []*Argument InArgs []*Argument
OutArgs []*Argument
} }
// actionFromXML creates an Action from the given XML description. // 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 == "" { if xmlAction.Name == "" {
return nil, fmt.Errorf("%w: empty action name", BadDescriptionError) return nil, fmt.Errorf("%w: empty action name", BadDescriptionError)
} }
action := &Action{
SCPD: scpd,
Name: xmlAction.Name,
}
var inArgs []*Argument var inArgs []*Argument
var outArgs []*Argument var outArgs []*Argument
for _, xmlArg := range xmlAction.Arguments { for _, xmlArg := range xmlAction.Arguments {
arg, err := argumentFromXML(xmlArg) arg, err := argumentFromXML(xmlArg, action)
if err != nil { if err != nil {
return nil, fmt.Errorf("processing argument %q: %w", xmlArg.Name, err) 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) BadDescriptionError, xmlArg.Name, xmlArg.Direction)
} }
} }
return &Action{ action.InArgs = inArgs
name: xmlAction.Name, action.OutArgs = outArgs
inArgs: inArgs, return action, nil
outArgs: outArgs,
}, nil
} }
// Argument description data. // Argument description data.
type Argument struct { type Argument struct {
name string Action *Action
relatedStateVariable string Name string
RelatedStateVariableName string
} }
// argumentFromXML creates an Argument from the XML description. // 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 == "" { if xmlArg.Name == "" {
return nil, fmt.Errorf("%w: empty argument name", BadDescriptionError) 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 nil, fmt.Errorf("%w: empty related state variable", BadDescriptionError)
} }
return &Argument{ return &Argument{
name: xmlArg.Name, Action: action,
relatedStateVariable: xmlArg.RelatedStateVariable, Name: xmlArg.Name,
RelatedStateVariableName: xmlArg.RelatedStateVariable,
}, nil }, nil
} }
func (arg *Argument) Name() string { func (arg *Argument) RelatedStateVariable() (*StateVariable, error) {
return arg.name if v, ok := arg.Action.SCPD.VariableByName[arg.RelatedStateVariableName]; ok {
} return v, nil
}
func (arg *Argument) RelatedStateVariableName() string { return nil, fmt.Errorf("%w: state variable %q", MissingDefinitionError, arg.RelatedStateVariableName)
return arg.relatedStateVariable
} }
// StateVariable description data. // StateVariable description data.
type StateVariable struct { type StateVariable struct {
name string Name string
dataType string DataType string
} }
func stateVariableFromXML(xmlSV *xmlsrvdesc.StateVariable) (*StateVariable, error) { func stateVariableFromXML(xmlSV *xmlsrvdesc.StateVariable) (*StateVariable, error) {
@ -153,15 +151,7 @@ func stateVariableFromXML(xmlSV *xmlsrvdesc.StateVariable) (*StateVariable, erro
UnsupportedDescriptionError, xmlSV.DataType.Type) UnsupportedDescriptionError, xmlSV.DataType.Type)
} }
return &StateVariable{ return &StateVariable{
name: xmlSV.Name, Name: xmlSV.Name,
dataType: xmlSV.DataType.Name, DataType: xmlSV.DataType.Name,
}, nil }, nil
} }
func (sv *StateVariable) Name() string {
return sv.name
}
func (sv *StateVariable) DataTypeName() string {
return sv.dataType
}

View File

@ -0,0 +1,9 @@
package typedesc
import "reflect"
type TypeDesc struct {
GoType reflect.Type
}
type TypeMap map[string]TypeDesc

View File

@ -14,12 +14,47 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
"reflect"
"regexp" "regexp"
"strconv" "strconv"
"time" "time"
"unicode/utf8" "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 { type SOAPValue interface {
encoding.TextMarshaler encoding.TextMarshaler
encoding.TextUnmarshaler encoding.TextUnmarshaler

View File

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