Switch to generate code with go generate
.
This commit is contained in:
parent
8bf4a8083e
commit
656e61dfad
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,2 @@
|
||||
/gotasks/specs
|
||||
*.zip
|
||||
*.sublime-workspace
|
12
README.md
12
README.md
@ -25,15 +25,19 @@ Core components:
|
||||
Regenerating dcps generated source code:
|
||||
----------------------------------------
|
||||
|
||||
1. Install gotasks: `go get -u github.com/jingweno/gotask`
|
||||
2. Change to the gotasks directory: `cd gotasks`
|
||||
3. Run specgen task: `gotask specgen`
|
||||
1. Build code generator:
|
||||
|
||||
`go get -u github.com/huin/goupnp/cmd/goupnpdcpgen`
|
||||
|
||||
2. Regenerate the code:
|
||||
|
||||
`go generate ./...`
|
||||
|
||||
Supporting additional UPnP devices and services:
|
||||
------------------------------------------------
|
||||
|
||||
Supporting additional services is, in the trivial case, simply a matter of
|
||||
adding the service to the `dcpMetadata` whitelist in `gotasks/specgen_task.go`,
|
||||
adding the service to the `dcpMetadata` whitelist in `cmd/goupnpdcpgen/metadata.go`,
|
||||
regenerating the source code (see above), and committing that source code.
|
||||
|
||||
However, it would be helpful if anyone needing such a service could test the
|
||||
|
153
cmd/goupnpdcpgen/codetemplate.go
Normal file
153
cmd/goupnpdcpgen/codetemplate.go
Normal file
@ -0,0 +1,153 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
)
|
||||
|
||||
var packageTmpl = template.Must(template.New("package").Parse(`{{$name := .Metadata.Name}}
|
||||
// Client for UPnP Device Control Protocol {{.Metadata.OfficialName}}.
|
||||
// {{if .Metadata.DocURL}}
|
||||
// This DCP is documented in detail at: {{.Metadata.DocURL}}{{end}}
|
||||
//
|
||||
// Typically, use one of the New* functions to create clients for services.
|
||||
package {{$name}}
|
||||
|
||||
// ***********************************************************
|
||||
// GENERATED FILE - DO NOT EDIT BY HAND. See README.md
|
||||
// ***********************************************************
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/huin/goupnp"
|
||||
"github.com/huin/goupnp/soap"
|
||||
)
|
||||
|
||||
// Hack to avoid Go complaining if time isn't used.
|
||||
var _ time.Time
|
||||
|
||||
// Device URNs:
|
||||
const ({{range .DeviceTypes}}
|
||||
{{.Const}} = "{{.URN}}"{{end}}
|
||||
)
|
||||
|
||||
// Service URNs:
|
||||
const ({{range .ServiceTypes}}
|
||||
{{.Const}} = "{{.URN}}"{{end}}
|
||||
)
|
||||
|
||||
{{range .Services}}
|
||||
{{$srv := .}}
|
||||
{{$srvIdent := printf "%s%s" .Name .Version}}
|
||||
|
||||
// {{$srvIdent}} is a client for UPnP SOAP service with URN "{{.URN}}". See
|
||||
// goupnp.ServiceClient, which contains RootDevice and Service attributes which
|
||||
// are provided for informational value.
|
||||
type {{$srvIdent}} struct {
|
||||
goupnp.ServiceClient
|
||||
}
|
||||
|
||||
// New{{$srvIdent}}Clients discovers instances of the service on the network,
|
||||
// and returns clients to any that are found. errors will contain an error for
|
||||
// any devices that replied but which could not be queried, and err will be set
|
||||
// if the discovery process failed outright.
|
||||
//
|
||||
// This is a typical entry calling point into this package.
|
||||
func New{{$srvIdent}}Clients() (clients []*{{$srvIdent}}, errors []error, err error) {
|
||||
var genericClients []goupnp.ServiceClient
|
||||
if genericClients, errors, err = goupnp.NewServiceClients({{$srv.Const}}); err != nil {
|
||||
return
|
||||
}
|
||||
clients = new{{$srvIdent}}ClientsFromGenericClients(genericClients)
|
||||
return
|
||||
}
|
||||
|
||||
// New{{$srvIdent}}ClientsByURL discovers instances of the service at the given
|
||||
// URL, and returns clients to any that are found. An error is returned if
|
||||
// there was an error probing the service.
|
||||
//
|
||||
// This is a typical entry calling point into this package when reusing an
|
||||
// previously discovered service URL.
|
||||
func New{{$srvIdent}}ClientsByURL(loc *url.URL) ([]*{{$srvIdent}}, error) {
|
||||
genericClients, err := goupnp.NewServiceClientsByURL(loc, {{$srv.Const}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return new{{$srvIdent}}ClientsFromGenericClients(genericClients), nil
|
||||
}
|
||||
|
||||
// New{{$srvIdent}}ClientsFromRootDevice discovers instances of the service in
|
||||
// a given root device, and returns clients to any that are found. An error is
|
||||
// returned if there was not at least one instance of the service within the
|
||||
// device. The location parameter is simply assigned to the Location attribute
|
||||
// of the wrapped ServiceClient(s).
|
||||
//
|
||||
// This is a typical entry calling point into this package when reusing an
|
||||
// previously discovered root device.
|
||||
func New{{$srvIdent}}ClientsFromRootDevice(rootDevice *goupnp.RootDevice, loc *url.URL) ([]*{{$srvIdent}}, error) {
|
||||
genericClients, err := goupnp.NewServiceClientsFromRootDevice(rootDevice, loc, {{$srv.Const}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return new{{$srvIdent}}ClientsFromGenericClients(genericClients), nil
|
||||
}
|
||||
|
||||
func new{{$srvIdent}}ClientsFromGenericClients(genericClients []goupnp.ServiceClient) []*{{$srvIdent}} {
|
||||
clients := make([]*{{$srvIdent}}, len(genericClients))
|
||||
for i := range genericClients {
|
||||
clients[i] = &{{$srvIdent}}{genericClients[i]}
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
{{range .SCPD.Actions}}{{/* loops over *SCPDWithURN values */}}
|
||||
|
||||
{{$winargs := $srv.WrapArguments .InputArguments}}
|
||||
{{$woutargs := $srv.WrapArguments .OutputArguments}}
|
||||
{{if $winargs.HasDoc}}
|
||||
//
|
||||
// Arguments:{{range $winargs}}{{if .HasDoc}}
|
||||
//
|
||||
// * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}}
|
||||
{{if $woutargs.HasDoc}}
|
||||
//
|
||||
// Return values:{{range $woutargs}}{{if .HasDoc}}
|
||||
//
|
||||
// * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}}
|
||||
func (client *{{$srvIdent}}) {{.Name}}({{range $winargs -}}
|
||||
{{.AsParameter}}, {{end -}}
|
||||
) ({{range $woutargs -}}
|
||||
{{.AsParameter}}, {{end}} err error) {
|
||||
// Request structure.
|
||||
request := {{if $winargs}}&{{template "argstruct" $winargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}}
|
||||
// BEGIN Marshal arguments into request.
|
||||
{{range $winargs}}
|
||||
if request.{{.Name}}, err = {{.Marshal}}; err != nil {
|
||||
return
|
||||
}{{end}}
|
||||
// END Marshal arguments into request.
|
||||
|
||||
// Response structure.
|
||||
response := {{if $woutargs}}&{{template "argstruct" $woutargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}}
|
||||
|
||||
// Perform the SOAP call.
|
||||
if err = client.SOAPClient.PerformAction({{$srv.URNParts.Const}}, "{{.Name}}", request, response); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// BEGIN Unmarshal arguments from response.
|
||||
{{range $woutargs}}
|
||||
if {{.Name}}, err = {{.Unmarshal "response"}}; err != nil {
|
||||
return
|
||||
}{{end}}
|
||||
// END Unmarshal arguments from response.
|
||||
return
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "argstruct"}}struct {{"{"}}
|
||||
{{range .}}{{.Name}} string
|
||||
{{end}}{{"}"}}{{end}}
|
||||
`))
|
222
cmd/goupnpdcpgen/dcp.go
Normal file
222
cmd/goupnpdcpgen/dcp.go
Normal file
@ -0,0 +1,222 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/huin/goupnp"
|
||||
"github.com/huin/goupnp/scpd"
|
||||
"github.com/huin/goutil/codegen"
|
||||
)
|
||||
|
||||
// DCP collects together information about a UPnP Device Control Protocol.
|
||||
type DCP struct {
|
||||
Metadata DCPMetadata
|
||||
DeviceTypes map[string]*URNParts
|
||||
ServiceTypes map[string]*URNParts
|
||||
Services []SCPDWithURN
|
||||
}
|
||||
|
||||
func newDCP(metadata DCPMetadata) *DCP {
|
||||
return &DCP{
|
||||
Metadata: metadata,
|
||||
DeviceTypes: make(map[string]*URNParts),
|
||||
ServiceTypes: make(map[string]*URNParts),
|
||||
}
|
||||
}
|
||||
|
||||
func (dcp *DCP) processZipFile(filename string) error {
|
||||
archive, err := zip.OpenReader(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading zip file %q: %v", filename, err)
|
||||
}
|
||||
defer archive.Close()
|
||||
for _, deviceFile := range globFiles("*/device/*.xml", archive) {
|
||||
if err := dcp.processDeviceFile(deviceFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, scpdFile := range globFiles("*/service/*.xml", archive) {
|
||||
if err := dcp.processSCPDFile(scpdFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dcp *DCP) processDeviceFile(file *zip.File) error {
|
||||
var device goupnp.Device
|
||||
if err := unmarshalXmlFile(file, &device); err != nil {
|
||||
return fmt.Errorf("error decoding device XML from file %q: %v", file.Name, err)
|
||||
}
|
||||
var mainErr error
|
||||
device.VisitDevices(func(d *goupnp.Device) {
|
||||
t := strings.TrimSpace(d.DeviceType)
|
||||
if t != "" {
|
||||
u, err := extractURNParts(t, deviceURNPrefix)
|
||||
if err != nil {
|
||||
mainErr = err
|
||||
}
|
||||
dcp.DeviceTypes[t] = u
|
||||
}
|
||||
})
|
||||
device.VisitServices(func(s *goupnp.Service) {
|
||||
u, err := extractURNParts(s.ServiceType, serviceURNPrefix)
|
||||
if err != nil {
|
||||
mainErr = err
|
||||
}
|
||||
dcp.ServiceTypes[s.ServiceType] = u
|
||||
})
|
||||
return mainErr
|
||||
}
|
||||
|
||||
func (dcp *DCP) writeCode(outFile string, useGofmt bool) error {
|
||||
packageFile, err := os.Create(outFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var output io.WriteCloser = packageFile
|
||||
if useGofmt {
|
||||
if output, err = codegen.NewGofmtWriteCloser(output); err != nil {
|
||||
packageFile.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = packageTmpl.Execute(output, dcp); err != nil {
|
||||
output.Close()
|
||||
return err
|
||||
}
|
||||
return output.Close()
|
||||
}
|
||||
|
||||
func (dcp *DCP) processSCPDFile(file *zip.File) error {
|
||||
scpd := new(scpd.SCPD)
|
||||
if err := unmarshalXmlFile(file, scpd); err != nil {
|
||||
return fmt.Errorf("error decoding SCPD XML from file %q: %v", file.Name, err)
|
||||
}
|
||||
scpd.Clean()
|
||||
urnParts, err := urnPartsFromSCPDFilename(file.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not recognize SCPD filename %q: %v", file.Name, err)
|
||||
}
|
||||
dcp.Services = append(dcp.Services, SCPDWithURN{
|
||||
URNParts: urnParts,
|
||||
SCPD: scpd,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
type SCPDWithURN struct {
|
||||
*URNParts
|
||||
SCPD *scpd.SCPD
|
||||
}
|
||||
|
||||
func (s *SCPDWithURN) WrapArguments(args []*scpd.Argument) (argumentWrapperList, error) {
|
||||
wrappedArgs := make(argumentWrapperList, len(args))
|
||||
for i, arg := range args {
|
||||
wa, err := s.wrapArgument(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrappedArgs[i] = wa
|
||||
}
|
||||
return wrappedArgs, nil
|
||||
}
|
||||
|
||||
func (s *SCPDWithURN) wrapArgument(arg *scpd.Argument) (*argumentWrapper, error) {
|
||||
relVar := s.SCPD.GetStateVariable(arg.RelatedStateVariable)
|
||||
if relVar == nil {
|
||||
return nil, fmt.Errorf("no such state variable: %q, for argument %q", arg.RelatedStateVariable, arg.Name)
|
||||
}
|
||||
cnv, ok := typeConvs[relVar.DataType.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown data type: %q, for state variable %q, for argument %q", relVar.DataType.Type, arg.RelatedStateVariable, arg.Name)
|
||||
}
|
||||
return &argumentWrapper{
|
||||
Argument: *arg,
|
||||
relVar: relVar,
|
||||
conv: cnv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type argumentWrapper struct {
|
||||
scpd.Argument
|
||||
relVar *scpd.StateVariable
|
||||
conv conv
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) AsParameter() string {
|
||||
return fmt.Sprintf("%s %s", arg.Name, arg.conv.ExtType)
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) HasDoc() bool {
|
||||
rng := arg.relVar.AllowedValueRange
|
||||
return ((rng != nil && (rng.Minimum != "" || rng.Maximum != "" || rng.Step != "")) ||
|
||||
len(arg.relVar.AllowedValues) > 0)
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) Document() string {
|
||||
relVar := arg.relVar
|
||||
if rng := relVar.AllowedValueRange; rng != nil {
|
||||
var parts []string
|
||||
if rng.Minimum != "" {
|
||||
parts = append(parts, fmt.Sprintf("minimum=%s", rng.Minimum))
|
||||
}
|
||||
if rng.Maximum != "" {
|
||||
parts = append(parts, fmt.Sprintf("maximum=%s", rng.Maximum))
|
||||
}
|
||||
if rng.Step != "" {
|
||||
parts = append(parts, fmt.Sprintf("step=%s", rng.Step))
|
||||
}
|
||||
return "allowed value range: " + strings.Join(parts, ", ")
|
||||
}
|
||||
if len(relVar.AllowedValues) != 0 {
|
||||
return "allowed values: " + strings.Join(relVar.AllowedValues, ", ")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) Marshal() string {
|
||||
return fmt.Sprintf("soap.Marshal%s(%s)", arg.conv.FuncSuffix, arg.Name)
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) Unmarshal(objVar string) string {
|
||||
return fmt.Sprintf("soap.Unmarshal%s(%s.%s)", arg.conv.FuncSuffix, objVar, arg.Name)
|
||||
}
|
||||
|
||||
type argumentWrapperList []*argumentWrapper
|
||||
|
||||
func (args argumentWrapperList) HasDoc() bool {
|
||||
for _, arg := range args {
|
||||
if arg.HasDoc() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type URNParts struct {
|
||||
URN string
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
func (u *URNParts) Const() string {
|
||||
return fmt.Sprintf("URN_%s_%s", u.Name, u.Version)
|
||||
}
|
||||
|
||||
// extractURNParts extracts the name and version from a URN string.
|
||||
func extractURNParts(urn, expectedPrefix string) (*URNParts, error) {
|
||||
if !strings.HasPrefix(urn, expectedPrefix) {
|
||||
return nil, fmt.Errorf("%q does not have expected prefix %q", urn, expectedPrefix)
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimPrefix(urn, expectedPrefix), ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("%q does not have a name and version", urn)
|
||||
}
|
||||
name, version := parts[0], parts[1]
|
||||
return &URNParts{urn, name, version}, nil
|
||||
}
|
88
cmd/goupnpdcpgen/fileutil.go
Normal file
88
cmd/goupnpdcpgen/fileutil.go
Normal file
@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func acquireFile(specFilename string, xmlSpecURL string) error {
|
||||
if f, err := os.Open(specFilename); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
f.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := http.Get(xmlSpecURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("could not download spec %q from %q: ",
|
||||
specFilename, xmlSpecURL, resp.Status)
|
||||
}
|
||||
|
||||
tmpFilename := specFilename + ".download"
|
||||
w, err := os.Create(tmpFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Rename(tmpFilename, specFilename)
|
||||
}
|
||||
|
||||
func globFiles(pattern string, archive *zip.ReadCloser) []*zip.File {
|
||||
var files []*zip.File
|
||||
for _, f := range archive.File {
|
||||
if matched, err := path.Match(pattern, f.Name); err != nil {
|
||||
// This shouldn't happen - all patterns are hard-coded, errors in them
|
||||
// are a programming error.
|
||||
panic(err)
|
||||
} else if matched {
|
||||
files = append(files, f)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func unmarshalXmlFile(file *zip.File, data interface{}) error {
|
||||
r, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := xml.NewDecoder(r)
|
||||
defer r.Close()
|
||||
return decoder.Decode(data)
|
||||
}
|
||||
|
||||
var scpdFilenameRe = regexp.MustCompile(
|
||||
`.*/([a-zA-Z0-9]+)([0-9]+)\.xml`)
|
||||
|
||||
func urnPartsFromSCPDFilename(filename string) (*URNParts, error) {
|
||||
parts := scpdFilenameRe.FindStringSubmatch(filename)
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("SCPD filename %q does not have expected number of parts", filename)
|
||||
}
|
||||
name, version := parts[1], parts[2]
|
||||
return &URNParts{
|
||||
URN: serviceURNPrefix + name + ":" + version,
|
||||
Name: name,
|
||||
Version: version,
|
||||
}, nil
|
||||
}
|
64
cmd/goupnpdcpgen/goupnpdcpgen.go
Normal file
64
cmd/goupnpdcpgen/goupnpdcpgen.go
Normal file
@ -0,0 +1,64 @@
|
||||
// Command to generate DCP package source from the XML specification.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
deviceURNPrefix = "urn:schemas-upnp-org:device:"
|
||||
serviceURNPrefix = "urn:schemas-upnp-org:service:"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
dcpName = flag.String("dcp_name", "", "Name of the DCP to generate.")
|
||||
specsDir = flag.String("specs_dir", ".", "Path to the specification storage directory. "+
|
||||
"This is used to find (and download if not present) the specification ZIP files.")
|
||||
useGofmt = flag.Bool("gofmt", true, "Pass the generated code through gofmt. "+
|
||||
"Disable this if debugging code generation and needing to see the generated code "+
|
||||
"prior to being passed through gofmt.")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if err := run(*dcpName, *specsDir, *useGofmt); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(dcpName, specsDir string, useGofmt bool) error {
|
||||
if err := os.MkdirAll(specsDir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("could not create specs-dir %q: %v\n", specsDir, err)
|
||||
}
|
||||
|
||||
for _, d := range dcpMetadata {
|
||||
if d.Name != dcpName {
|
||||
continue
|
||||
}
|
||||
specFilename := filepath.Join(specsDir, d.Name+".zip")
|
||||
err := acquireFile(specFilename, d.XMLSpecURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not acquire spec for %s: %v", d.Name, err)
|
||||
}
|
||||
dcp := newDCP(d)
|
||||
if err := dcp.processZipFile(specFilename); err != nil {
|
||||
return fmt.Errorf("error processing spec for %s in file %q: %v", d.Name, specFilename, err)
|
||||
}
|
||||
for i, hack := range d.Hacks {
|
||||
if err := hack(dcp); err != nil {
|
||||
return fmt.Errorf("error with Hack[%d] for %s: %v", i, d.Name, err)
|
||||
}
|
||||
}
|
||||
if err := dcp.writeCode(d.Name+".go", useGofmt); err != nil {
|
||||
return fmt.Errorf("error writing package %q: %v", dcp.Metadata.Name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("could not find DCP with name %q", dcpName)
|
||||
}
|
69
cmd/goupnpdcpgen/metadata.go
Normal file
69
cmd/goupnpdcpgen/metadata.go
Normal file
@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
// DCP contains extra metadata to use when generating DCP source files.
|
||||
type DCPMetadata struct {
|
||||
Name string // What to name the Go DCP package.
|
||||
OfficialName string // Official name for the DCP.
|
||||
DocURL string // Optional - URL for further documentation about the DCP.
|
||||
XMLSpecURL string // Where to download the XML spec from.
|
||||
// Any special-case functions to run against the DCP before writing it out.
|
||||
Hacks []DCPHackFn
|
||||
}
|
||||
|
||||
var dcpMetadata = []DCPMetadata{
|
||||
{
|
||||
Name: "internetgateway1",
|
||||
OfficialName: "Internet Gateway Device v1",
|
||||
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf",
|
||||
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-TestFiles-20010921.zip",
|
||||
Hacks: []DCPHackFn{totalBytesHack},
|
||||
},
|
||||
{
|
||||
Name: "internetgateway2",
|
||||
OfficialName: "Internet Gateway Device v2",
|
||||
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf",
|
||||
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-Testfiles-20110224.zip",
|
||||
Hacks: []DCPHackFn{
|
||||
func(dcp *DCP) error {
|
||||
missingURN := "urn:schemas-upnp-org:service:WANIPv6FirewallControl:1"
|
||||
if _, ok := dcp.ServiceTypes[missingURN]; ok {
|
||||
return nil
|
||||
}
|
||||
urnParts, err := extractURNParts(missingURN, serviceURNPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dcp.ServiceTypes[missingURN] = urnParts
|
||||
return nil
|
||||
}, totalBytesHack,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "av1",
|
||||
OfficialName: "MediaServer v1 and MediaRenderer v1",
|
||||
DocURL: "http://upnp.org/specs/av/av1/",
|
||||
XMLSpecURL: "http://upnp.org/specs/av/UPnP-av-TestFiles-20070927.zip",
|
||||
},
|
||||
}
|
||||
|
||||
func totalBytesHack(dcp *DCP) error {
|
||||
for _, service := range dcp.Services {
|
||||
if service.URN == "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1" {
|
||||
variables := service.SCPD.StateVariables
|
||||
for key, variable := range variables {
|
||||
varName := variable.Name
|
||||
if varName == "TotalBytesSent" || varName == "TotalBytesReceived" {
|
||||
// Fix size of total bytes which is by default ui4 or maximum 4 GiB.
|
||||
variable.DataType.Name = "ui8"
|
||||
variables[key] = variable
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DCPHackFn func(*DCP) error
|
35
cmd/goupnpdcpgen/typemap.go
Normal file
35
cmd/goupnpdcpgen/typemap.go
Normal file
@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
type conv struct {
|
||||
FuncSuffix string
|
||||
ExtType string
|
||||
}
|
||||
|
||||
// typeConvs maps from a SOAP type (e.g "fixed.14.4") to the function name
|
||||
// suffix inside the soap module (e.g "Fixed14_4") and the Go type.
|
||||
var typeConvs = map[string]conv{
|
||||
"ui1": {"Ui1", "uint8"},
|
||||
"ui2": {"Ui2", "uint16"},
|
||||
"ui4": {"Ui4", "uint32"},
|
||||
"ui8": {"Ui8", "uint64"},
|
||||
"i1": {"I1", "int8"},
|
||||
"i2": {"I2", "int16"},
|
||||
"i4": {"I4", "int32"},
|
||||
"int": {"Int", "int64"},
|
||||
"r4": {"R4", "float32"},
|
||||
"r8": {"R8", "float64"},
|
||||
"number": {"R8", "float64"}, // Alias for r8.
|
||||
"fixed.14.4": {"Fixed14_4", "float64"},
|
||||
"float": {"R8", "float64"},
|
||||
"char": {"Char", "rune"},
|
||||
"string": {"String", "string"},
|
||||
"date": {"Date", "time.Time"},
|
||||
"dateTime": {"DateTime", "time.Time"},
|
||||
"dateTime.tz": {"DateTimeTz", "time.Time"},
|
||||
"time": {"TimeOfDay", "soap.TimeOfDay"},
|
||||
"time.tz": {"TimeOfDayTz", "soap.TimeOfDay"},
|
||||
"boolean": {"Boolean", "bool"},
|
||||
"bin.base64": {"BinBase64", "[]byte"},
|
||||
"bin.hex": {"BinHex", "[]byte"},
|
||||
"uri": {"URI", "*url.URL"},
|
||||
}
|
2
dcps/av1/gen.go
Normal file
2
dcps/av1/gen.go
Normal file
@ -0,0 +1,2 @@
|
||||
//go:generate goupnpdcpgen -dcp_name av1
|
||||
package av1
|
2
dcps/internetgateway1/gen.go
Normal file
2
dcps/internetgateway1/gen.go
Normal file
@ -0,0 +1,2 @@
|
||||
//go:generate goupnpdcpgen -dcp_name internetgateway1
|
||||
package internetgateway1
|
2
dcps/internetgateway2/gen.go
Normal file
2
dcps/internetgateway2/gen.go
Normal file
@ -0,0 +1,2 @@
|
||||
//go:generate goupnpdcpgen -dcp_name internetgateway2
|
||||
package internetgateway2
|
3
go.mod
3
go.mod
@ -1,10 +1,7 @@
|
||||
module github.com/huin/goupnp
|
||||
|
||||
require (
|
||||
github.com/codegangsta/cli v1.20.0 // indirect
|
||||
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150
|
||||
github.com/jingweno/gotask v0.0.0-20140112180521-104f8017a597
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1
|
||||
golang.org/x/text v0.3.0 // indirect
|
||||
)
|
||||
|
6
go.sum
6
go.sum
@ -1,11 +1,5 @@
|
||||
github.com/codegangsta/cli v1.20.0 h1:iX1FXEgwzd5+XN6wk5cVHOGQj6Q3Dcp20lUeS4lHNTw=
|
||||
github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
|
||||
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150 h1:vlNjIqmUZ9CMAWsbURYl3a6wZbw7q5RHVvlXTNS/Bs8=
|
||||
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
|
||||
github.com/jingweno/gotask v0.0.0-20140112180521-104f8017a597 h1:w6OY+MtPjGk9UimnjfSkfC7uon/hHPGl/1dFU4/0PCo=
|
||||
github.com/jingweno/gotask v0.0.0-20140112180521-104f8017a597/go.mod h1:uZ3nRjkvWkOHt0xvIb3+Fev4KukoMKd7SjpDXQis37s=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w=
|
||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
|
@ -1,626 +0,0 @@
|
||||
// +build gotask
|
||||
|
||||
package gotasks
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/huin/goupnp"
|
||||
"github.com/huin/goupnp/scpd"
|
||||
"github.com/huin/goutil/codegen"
|
||||
"github.com/jingweno/gotask/tasking"
|
||||
)
|
||||
|
||||
var (
|
||||
deviceURNPrefix = "urn:schemas-upnp-org:device:"
|
||||
serviceURNPrefix = "urn:schemas-upnp-org:service:"
|
||||
)
|
||||
|
||||
// DCP contains extra metadata to use when generating DCP source files.
|
||||
type DCPMetadata struct {
|
||||
Name string // What to name the Go DCP package.
|
||||
OfficialName string // Official name for the DCP.
|
||||
DocURL string // Optional - URL for further documentation about the DCP.
|
||||
XMLSpecURL string // Where to download the XML spec from.
|
||||
// Any special-case functions to run against the DCP before writing it out.
|
||||
Hacks []DCPHackFn
|
||||
}
|
||||
|
||||
var dcpMetadata = []DCPMetadata{
|
||||
{
|
||||
Name: "internetgateway1",
|
||||
OfficialName: "Internet Gateway Device v1",
|
||||
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf",
|
||||
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-TestFiles-20010921.zip",
|
||||
Hacks: []DCPHackFn{totalBytesHack},
|
||||
},
|
||||
{
|
||||
Name: "internetgateway2",
|
||||
OfficialName: "Internet Gateway Device v2",
|
||||
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf",
|
||||
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-Testfiles-20110224.zip",
|
||||
Hacks: []DCPHackFn{
|
||||
func(dcp *DCP) error {
|
||||
missingURN := "urn:schemas-upnp-org:service:WANIPv6FirewallControl:1"
|
||||
if _, ok := dcp.ServiceTypes[missingURN]; ok {
|
||||
return nil
|
||||
}
|
||||
urnParts, err := extractURNParts(missingURN, serviceURNPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dcp.ServiceTypes[missingURN] = urnParts
|
||||
return nil
|
||||
}, totalBytesHack,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "av1",
|
||||
OfficialName: "MediaServer v1 and MediaRenderer v1",
|
||||
DocURL: "http://upnp.org/specs/av/av1/",
|
||||
XMLSpecURL: "http://upnp.org/specs/av/UPnP-av-TestFiles-20070927.zip",
|
||||
},
|
||||
}
|
||||
|
||||
func totalBytesHack(dcp *DCP) error {
|
||||
for _, service := range dcp.Services {
|
||||
if service.URN == "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1" {
|
||||
variables := service.SCPD.StateVariables
|
||||
for key, variable := range variables {
|
||||
varName := variable.Name
|
||||
if varName == "TotalBytesSent" || varName == "TotalBytesReceived" {
|
||||
// Fix size of total bytes which is by default ui4 or maximum 4 GiB.
|
||||
variable.DataType.Name = "ui8"
|
||||
variables[key] = variable
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DCPHackFn func(*DCP) error
|
||||
|
||||
// NAME
|
||||
// specgen - generates Go code from the UPnP specification files.
|
||||
//
|
||||
// DESCRIPTION
|
||||
// The specification is available for download from:
|
||||
//
|
||||
// OPTIONS
|
||||
// -s, --specs_dir=<spec directory>
|
||||
// Path to the specification storage directory. This is used to find (and download if not present) the specification ZIP files. Defaults to 'specs'
|
||||
// -o, --out_dir=<output directory>
|
||||
// Path to the output directory. This is is where the DCP source files will be placed. Should normally correspond to the directory for github.com/huin/goupnp/dcps. Defaults to '../dcps'
|
||||
// --nogofmt
|
||||
// Disable passing the output through gofmt. Do this if debugging code output problems and needing to see the generated code prior to being passed through gofmt.
|
||||
func TaskSpecgen(t *tasking.T) {
|
||||
specsDir := fallbackStrValue("specs", t.Flags.String("specs_dir"), t.Flags.String("s"))
|
||||
if err := os.MkdirAll(specsDir, os.ModePerm); err != nil {
|
||||
t.Fatalf("Could not create specs-dir %q: %v\n", specsDir, err)
|
||||
}
|
||||
outDir := fallbackStrValue("../dcps", t.Flags.String("out_dir"), t.Flags.String("o"))
|
||||
useGofmt := !t.Flags.Bool("nogofmt")
|
||||
|
||||
NEXT_DCP:
|
||||
for _, d := range dcpMetadata {
|
||||
specFilename := filepath.Join(specsDir, d.Name+".zip")
|
||||
err := acquireFile(specFilename, d.XMLSpecURL)
|
||||
if err != nil {
|
||||
t.Logf("Could not acquire spec for %s, skipping: %v\n", d.Name, err)
|
||||
continue NEXT_DCP
|
||||
}
|
||||
dcp := newDCP(d)
|
||||
if err := dcp.processZipFile(specFilename); err != nil {
|
||||
log.Printf("Error processing spec for %s in file %q: %v", d.Name, specFilename, err)
|
||||
continue NEXT_DCP
|
||||
}
|
||||
for i, hack := range d.Hacks {
|
||||
if err := hack(dcp); err != nil {
|
||||
log.Printf("Error with Hack[%d] for %s: %v", i, d.Name, err)
|
||||
continue NEXT_DCP
|
||||
}
|
||||
}
|
||||
dcp.writePackage(outDir, useGofmt)
|
||||
if err := dcp.writePackage(outDir, useGofmt); err != nil {
|
||||
log.Printf("Error writing package %q: %v", dcp.Metadata.Name, err)
|
||||
continue NEXT_DCP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fallbackStrValue(defaultValue string, values ...string) string {
|
||||
for _, v := range values {
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func acquireFile(specFilename string, xmlSpecURL string) error {
|
||||
if f, err := os.Open(specFilename); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
f.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := http.Get(xmlSpecURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("could not download spec %q from %q: ",
|
||||
specFilename, xmlSpecURL, resp.Status)
|
||||
}
|
||||
|
||||
tmpFilename := specFilename + ".download"
|
||||
w, err := os.Create(tmpFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Rename(tmpFilename, specFilename)
|
||||
}
|
||||
|
||||
// DCP collects together information about a UPnP Device Control Protocol.
|
||||
type DCP struct {
|
||||
Metadata DCPMetadata
|
||||
DeviceTypes map[string]*URNParts
|
||||
ServiceTypes map[string]*URNParts
|
||||
Services []SCPDWithURN
|
||||
}
|
||||
|
||||
func newDCP(metadata DCPMetadata) *DCP {
|
||||
return &DCP{
|
||||
Metadata: metadata,
|
||||
DeviceTypes: make(map[string]*URNParts),
|
||||
ServiceTypes: make(map[string]*URNParts),
|
||||
}
|
||||
}
|
||||
|
||||
func (dcp *DCP) processZipFile(filename string) error {
|
||||
archive, err := zip.OpenReader(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading zip file %q: %v", filename, err)
|
||||
}
|
||||
defer archive.Close()
|
||||
for _, deviceFile := range globFiles("*/device/*.xml", archive) {
|
||||
if err := dcp.processDeviceFile(deviceFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, scpdFile := range globFiles("*/service/*.xml", archive) {
|
||||
if err := dcp.processSCPDFile(scpdFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dcp *DCP) processDeviceFile(file *zip.File) error {
|
||||
var device goupnp.Device
|
||||
if err := unmarshalXmlFile(file, &device); err != nil {
|
||||
return fmt.Errorf("error decoding device XML from file %q: %v", file.Name, err)
|
||||
}
|
||||
var mainErr error
|
||||
device.VisitDevices(func(d *goupnp.Device) {
|
||||
t := strings.TrimSpace(d.DeviceType)
|
||||
if t != "" {
|
||||
u, err := extractURNParts(t, deviceURNPrefix)
|
||||
if err != nil {
|
||||
mainErr = err
|
||||
}
|
||||
dcp.DeviceTypes[t] = u
|
||||
}
|
||||
})
|
||||
device.VisitServices(func(s *goupnp.Service) {
|
||||
u, err := extractURNParts(s.ServiceType, serviceURNPrefix)
|
||||
if err != nil {
|
||||
mainErr = err
|
||||
}
|
||||
dcp.ServiceTypes[s.ServiceType] = u
|
||||
})
|
||||
return mainErr
|
||||
}
|
||||
|
||||
func (dcp *DCP) writePackage(outDir string, useGofmt bool) error {
|
||||
packageDirname := filepath.Join(outDir, dcp.Metadata.Name)
|
||||
err := os.MkdirAll(packageDirname, os.ModePerm)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
packageFilename := filepath.Join(packageDirname, dcp.Metadata.Name+".go")
|
||||
packageFile, err := os.Create(packageFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var output io.WriteCloser = packageFile
|
||||
if useGofmt {
|
||||
if output, err = codegen.NewGofmtWriteCloser(output); err != nil {
|
||||
packageFile.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = packageTmpl.Execute(output, dcp); err != nil {
|
||||
output.Close()
|
||||
return err
|
||||
}
|
||||
return output.Close()
|
||||
}
|
||||
|
||||
func (dcp *DCP) processSCPDFile(file *zip.File) error {
|
||||
scpd := new(scpd.SCPD)
|
||||
if err := unmarshalXmlFile(file, scpd); err != nil {
|
||||
return fmt.Errorf("error decoding SCPD XML from file %q: %v", file.Name, err)
|
||||
}
|
||||
scpd.Clean()
|
||||
urnParts, err := urnPartsFromSCPDFilename(file.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not recognize SCPD filename %q: %v", file.Name, err)
|
||||
}
|
||||
dcp.Services = append(dcp.Services, SCPDWithURN{
|
||||
URNParts: urnParts,
|
||||
SCPD: scpd,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
type SCPDWithURN struct {
|
||||
*URNParts
|
||||
SCPD *scpd.SCPD
|
||||
}
|
||||
|
||||
func (s *SCPDWithURN) WrapArguments(args []*scpd.Argument) (argumentWrapperList, error) {
|
||||
wrappedArgs := make(argumentWrapperList, len(args))
|
||||
for i, arg := range args {
|
||||
wa, err := s.wrapArgument(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrappedArgs[i] = wa
|
||||
}
|
||||
return wrappedArgs, nil
|
||||
}
|
||||
|
||||
func (s *SCPDWithURN) wrapArgument(arg *scpd.Argument) (*argumentWrapper, error) {
|
||||
relVar := s.SCPD.GetStateVariable(arg.RelatedStateVariable)
|
||||
if relVar == nil {
|
||||
return nil, fmt.Errorf("no such state variable: %q, for argument %q", arg.RelatedStateVariable, arg.Name)
|
||||
}
|
||||
cnv, ok := typeConvs[relVar.DataType.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown data type: %q, for state variable %q, for argument %q", relVar.DataType.Type, arg.RelatedStateVariable, arg.Name)
|
||||
}
|
||||
return &argumentWrapper{
|
||||
Argument: *arg,
|
||||
relVar: relVar,
|
||||
conv: cnv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type argumentWrapper struct {
|
||||
scpd.Argument
|
||||
relVar *scpd.StateVariable
|
||||
conv conv
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) AsParameter() string {
|
||||
return fmt.Sprintf("%s %s", arg.Name, arg.conv.ExtType)
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) HasDoc() bool {
|
||||
rng := arg.relVar.AllowedValueRange
|
||||
return ((rng != nil && (rng.Minimum != "" || rng.Maximum != "" || rng.Step != "")) ||
|
||||
len(arg.relVar.AllowedValues) > 0)
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) Document() string {
|
||||
relVar := arg.relVar
|
||||
if rng := relVar.AllowedValueRange; rng != nil {
|
||||
var parts []string
|
||||
if rng.Minimum != "" {
|
||||
parts = append(parts, fmt.Sprintf("minimum=%s", rng.Minimum))
|
||||
}
|
||||
if rng.Maximum != "" {
|
||||
parts = append(parts, fmt.Sprintf("maximum=%s", rng.Maximum))
|
||||
}
|
||||
if rng.Step != "" {
|
||||
parts = append(parts, fmt.Sprintf("step=%s", rng.Step))
|
||||
}
|
||||
return "allowed value range: " + strings.Join(parts, ", ")
|
||||
}
|
||||
if len(relVar.AllowedValues) != 0 {
|
||||
return "allowed values: " + strings.Join(relVar.AllowedValues, ", ")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) Marshal() string {
|
||||
return fmt.Sprintf("soap.Marshal%s(%s)", arg.conv.FuncSuffix, arg.Name)
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) Unmarshal(objVar string) string {
|
||||
return fmt.Sprintf("soap.Unmarshal%s(%s.%s)", arg.conv.FuncSuffix, objVar, arg.Name)
|
||||
}
|
||||
|
||||
type argumentWrapperList []*argumentWrapper
|
||||
|
||||
func (args argumentWrapperList) HasDoc() bool {
|
||||
for _, arg := range args {
|
||||
if arg.HasDoc() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type conv struct {
|
||||
FuncSuffix string
|
||||
ExtType string
|
||||
}
|
||||
|
||||
// typeConvs maps from a SOAP type (e.g "fixed.14.4") to the function name
|
||||
// suffix inside the soap module (e.g "Fixed14_4") and the Go type.
|
||||
var typeConvs = map[string]conv{
|
||||
"ui1": conv{"Ui1", "uint8"},
|
||||
"ui2": conv{"Ui2", "uint16"},
|
||||
"ui4": conv{"Ui4", "uint32"},
|
||||
"ui8": conv{"Ui8", "uint64"},
|
||||
"i1": conv{"I1", "int8"},
|
||||
"i2": conv{"I2", "int16"},
|
||||
"i4": conv{"I4", "int32"},
|
||||
"int": conv{"Int", "int64"},
|
||||
"r4": conv{"R4", "float32"},
|
||||
"r8": conv{"R8", "float64"},
|
||||
"number": conv{"R8", "float64"}, // Alias for r8.
|
||||
"fixed.14.4": conv{"Fixed14_4", "float64"},
|
||||
"float": conv{"R8", "float64"},
|
||||
"char": conv{"Char", "rune"},
|
||||
"string": conv{"String", "string"},
|
||||
"date": conv{"Date", "time.Time"},
|
||||
"dateTime": conv{"DateTime", "time.Time"},
|
||||
"dateTime.tz": conv{"DateTimeTz", "time.Time"},
|
||||
"time": conv{"TimeOfDay", "soap.TimeOfDay"},
|
||||
"time.tz": conv{"TimeOfDayTz", "soap.TimeOfDay"},
|
||||
"boolean": conv{"Boolean", "bool"},
|
||||
"bin.base64": conv{"BinBase64", "[]byte"},
|
||||
"bin.hex": conv{"BinHex", "[]byte"},
|
||||
"uri": conv{"URI", "*url.URL"},
|
||||
}
|
||||
|
||||
func globFiles(pattern string, archive *zip.ReadCloser) []*zip.File {
|
||||
var files []*zip.File
|
||||
for _, f := range archive.File {
|
||||
if matched, err := path.Match(pattern, f.Name); err != nil {
|
||||
// This shouldn't happen - all patterns are hard-coded, errors in them
|
||||
// are a programming error.
|
||||
panic(err)
|
||||
} else if matched {
|
||||
files = append(files, f)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func unmarshalXmlFile(file *zip.File, data interface{}) error {
|
||||
r, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := xml.NewDecoder(r)
|
||||
defer r.Close()
|
||||
return decoder.Decode(data)
|
||||
}
|
||||
|
||||
type URNParts struct {
|
||||
URN string
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
func (u *URNParts) Const() string {
|
||||
return fmt.Sprintf("URN_%s_%s", u.Name, u.Version)
|
||||
}
|
||||
|
||||
// extractURNParts extracts the name and version from a URN string.
|
||||
func extractURNParts(urn, expectedPrefix string) (*URNParts, error) {
|
||||
if !strings.HasPrefix(urn, expectedPrefix) {
|
||||
return nil, fmt.Errorf("%q does not have expected prefix %q", urn, expectedPrefix)
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimPrefix(urn, expectedPrefix), ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("%q does not have a name and version", urn)
|
||||
}
|
||||
name, version := parts[0], parts[1]
|
||||
return &URNParts{urn, name, version}, nil
|
||||
}
|
||||
|
||||
var scpdFilenameRe = regexp.MustCompile(
|
||||
`.*/([a-zA-Z0-9]+)([0-9]+)\.xml`)
|
||||
|
||||
func urnPartsFromSCPDFilename(filename string) (*URNParts, error) {
|
||||
parts := scpdFilenameRe.FindStringSubmatch(filename)
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("SCPD filename %q does not have expected number of parts", filename)
|
||||
}
|
||||
name, version := parts[1], parts[2]
|
||||
return &URNParts{
|
||||
URN: serviceURNPrefix + name + ":" + version,
|
||||
Name: name,
|
||||
Version: version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var packageTmpl = template.Must(template.New("package").Parse(`{{$name := .Metadata.Name}}
|
||||
// Client for UPnP Device Control Protocol {{.Metadata.OfficialName}}.
|
||||
// {{if .Metadata.DocURL}}
|
||||
// This DCP is documented in detail at: {{.Metadata.DocURL}}{{end}}
|
||||
//
|
||||
// Typically, use one of the New* functions to create clients for services.
|
||||
package {{$name}}
|
||||
|
||||
// ***********************************************************
|
||||
// GENERATED FILE - DO NOT EDIT BY HAND. See README.md
|
||||
// ***********************************************************
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/huin/goupnp"
|
||||
"github.com/huin/goupnp/soap"
|
||||
)
|
||||
|
||||
// Hack to avoid Go complaining if time isn't used.
|
||||
var _ time.Time
|
||||
|
||||
// Device URNs:
|
||||
const ({{range .DeviceTypes}}
|
||||
{{.Const}} = "{{.URN}}"{{end}}
|
||||
)
|
||||
|
||||
// Service URNs:
|
||||
const ({{range .ServiceTypes}}
|
||||
{{.Const}} = "{{.URN}}"{{end}}
|
||||
)
|
||||
|
||||
{{range .Services}}
|
||||
{{$srv := .}}
|
||||
{{$srvIdent := printf "%s%s" .Name .Version}}
|
||||
|
||||
// {{$srvIdent}} is a client for UPnP SOAP service with URN "{{.URN}}". See
|
||||
// goupnp.ServiceClient, which contains RootDevice and Service attributes which
|
||||
// are provided for informational value.
|
||||
type {{$srvIdent}} struct {
|
||||
goupnp.ServiceClient
|
||||
}
|
||||
|
||||
// New{{$srvIdent}}Clients discovers instances of the service on the network,
|
||||
// and returns clients to any that are found. errors will contain an error for
|
||||
// any devices that replied but which could not be queried, and err will be set
|
||||
// if the discovery process failed outright.
|
||||
//
|
||||
// This is a typical entry calling point into this package.
|
||||
func New{{$srvIdent}}Clients() (clients []*{{$srvIdent}}, errors []error, err error) {
|
||||
var genericClients []goupnp.ServiceClient
|
||||
if genericClients, errors, err = goupnp.NewServiceClients({{$srv.Const}}); err != nil {
|
||||
return
|
||||
}
|
||||
clients = new{{$srvIdent}}ClientsFromGenericClients(genericClients)
|
||||
return
|
||||
}
|
||||
|
||||
// New{{$srvIdent}}ClientsByURL discovers instances of the service at the given
|
||||
// URL, and returns clients to any that are found. An error is returned if
|
||||
// there was an error probing the service.
|
||||
//
|
||||
// This is a typical entry calling point into this package when reusing an
|
||||
// previously discovered service URL.
|
||||
func New{{$srvIdent}}ClientsByURL(loc *url.URL) ([]*{{$srvIdent}}, error) {
|
||||
genericClients, err := goupnp.NewServiceClientsByURL(loc, {{$srv.Const}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return new{{$srvIdent}}ClientsFromGenericClients(genericClients), nil
|
||||
}
|
||||
|
||||
// New{{$srvIdent}}ClientsFromRootDevice discovers instances of the service in
|
||||
// a given root device, and returns clients to any that are found. An error is
|
||||
// returned if there was not at least one instance of the service within the
|
||||
// device. The location parameter is simply assigned to the Location attribute
|
||||
// of the wrapped ServiceClient(s).
|
||||
//
|
||||
// This is a typical entry calling point into this package when reusing an
|
||||
// previously discovered root device.
|
||||
func New{{$srvIdent}}ClientsFromRootDevice(rootDevice *goupnp.RootDevice, loc *url.URL) ([]*{{$srvIdent}}, error) {
|
||||
genericClients, err := goupnp.NewServiceClientsFromRootDevice(rootDevice, loc, {{$srv.Const}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return new{{$srvIdent}}ClientsFromGenericClients(genericClients), nil
|
||||
}
|
||||
|
||||
func new{{$srvIdent}}ClientsFromGenericClients(genericClients []goupnp.ServiceClient) []*{{$srvIdent}} {
|
||||
clients := make([]*{{$srvIdent}}, len(genericClients))
|
||||
for i := range genericClients {
|
||||
clients[i] = &{{$srvIdent}}{genericClients[i]}
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
{{range .SCPD.Actions}}{{/* loops over *SCPDWithURN values */}}
|
||||
|
||||
{{$winargs := $srv.WrapArguments .InputArguments}}
|
||||
{{$woutargs := $srv.WrapArguments .OutputArguments}}
|
||||
{{if $winargs.HasDoc}}
|
||||
//
|
||||
// Arguments:{{range $winargs}}{{if .HasDoc}}
|
||||
//
|
||||
// * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}}
|
||||
{{if $woutargs.HasDoc}}
|
||||
//
|
||||
// Return values:{{range $woutargs}}{{if .HasDoc}}
|
||||
//
|
||||
// * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}}
|
||||
func (client *{{$srvIdent}}) {{.Name}}({{range $winargs -}}
|
||||
{{.AsParameter}}, {{end -}}
|
||||
) ({{range $woutargs -}}
|
||||
{{.AsParameter}}, {{end}} err error) {
|
||||
// Request structure.
|
||||
request := {{if $winargs}}&{{template "argstruct" $winargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}}
|
||||
// BEGIN Marshal arguments into request.
|
||||
{{range $winargs}}
|
||||
if request.{{.Name}}, err = {{.Marshal}}; err != nil {
|
||||
return
|
||||
}{{end}}
|
||||
// END Marshal arguments into request.
|
||||
|
||||
// Response structure.
|
||||
response := {{if $woutargs}}&{{template "argstruct" $woutargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}}
|
||||
|
||||
// Perform the SOAP call.
|
||||
if err = client.SOAPClient.PerformAction({{$srv.URNParts.Const}}, "{{.Name}}", request, response); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// BEGIN Unmarshal arguments from response.
|
||||
{{range $woutargs}}
|
||||
if {{.Name}}, err = {{.Unmarshal "response"}}; err != nil {
|
||||
return
|
||||
}{{end}}
|
||||
// END Unmarshal arguments from response.
|
||||
return
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "argstruct"}}struct {{"{"}}
|
||||
{{range .}}{{.Name}} string
|
||||
{{end}}{{"}"}}{{end}}
|
||||
`))
|
Loading…
Reference in New Issue
Block a user