Switch to generate code with go generate.
				
					
				
			This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,2 @@ | |||||||
| /gotasks/specs | *.zip | ||||||
| *.sublime-workspace | *.sublime-workspace | ||||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							| @@ -25,15 +25,19 @@ Core components: | |||||||
| Regenerating dcps generated source code: | Regenerating dcps generated source code: | ||||||
| ---------------------------------------- | ---------------------------------------- | ||||||
|  |  | ||||||
| 1. Install gotasks: `go get -u github.com/jingweno/gotask` | 1. Build code generator: | ||||||
| 2. Change to the gotasks directory: `cd gotasks` |  | ||||||
| 3. Run specgen task: `gotask specgen` | 	`go get -u github.com/huin/goupnp/cmd/goupnpdcpgen` | ||||||
|  |  | ||||||
|  | 2. Regenerate the code: | ||||||
|  |  | ||||||
|  | 	`go generate ./...` | ||||||
|  |  | ||||||
| Supporting additional UPnP devices and services: | Supporting additional UPnP devices and services: | ||||||
| ------------------------------------------------ | ------------------------------------------------ | ||||||
|  |  | ||||||
| Supporting additional services is, in the trivial case, simply a matter of | 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. | 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 | 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 | module github.com/huin/goupnp | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/codegangsta/cli v1.20.0 // indirect |  | ||||||
| 	github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150 | 	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/net v0.0.0-20181011144130-49bb7cea24b1 | ||||||
| 	golang.org/x/text v0.3.0 // indirect | 	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 h1:vlNjIqmUZ9CMAWsbURYl3a6wZbw7q5RHVvlXTNS/Bs8= | ||||||
| github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= | 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 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w= | ||||||
| golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= | 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}} |  | ||||||
| `)) |  | ||||||
		Reference in New Issue
	
	Block a user