Initial work on auto-generating SOAP clients for UPnP services.
This commit is contained in:
		
							
								
								
									
										66
									
								
								cmd/specgen/pkgtmpl.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								cmd/specgen/pkgtmpl.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"text/template"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var packageTmpl = template.Must(template.New("package").Parse(`package {{.Name}}
 | 
			
		||||
 | 
			
		||||
import "github.com/huin/goupnp/soap"
 | 
			
		||||
 | 
			
		||||
const ({{range .DeviceTypes}}
 | 
			
		||||
	{{.Const}} = "{{.URN}}"
 | 
			
		||||
{{end}})
 | 
			
		||||
 | 
			
		||||
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}}".
 | 
			
		||||
type {{$srvIdent}} struct {
 | 
			
		||||
	SOAPClient soap.SOAPClient
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
{{range .SCPD.Actions}}{{/* loops over *SCPDWithURN values */}}
 | 
			
		||||
 | 
			
		||||
{{$reqType := printf "_%s_%s_Request" $srvIdent .Name}}
 | 
			
		||||
{{$respType := printf "_%s_%s_Response" $srvIdent .Name}}
 | 
			
		||||
 | 
			
		||||
// {{$reqType}} is the XML structure for the input arguments for action {{.Name}}.
 | 
			
		||||
type {{$reqType}} struct {{"{"}}{{range .Arguments}}{{if .IsInput}}
 | 
			
		||||
	{{.Name}} string
 | 
			
		||||
{{end}}{{end}}}
 | 
			
		||||
 | 
			
		||||
// {{$respType}} is the XML structure for the output arguments for action {{.Name}}.
 | 
			
		||||
type {{$respType}} struct {{"{"}}{{range .Arguments}}{{if .IsOutput}}
 | 
			
		||||
	{{.Name}} string
 | 
			
		||||
{{end}}{{end}}}
 | 
			
		||||
 | 
			
		||||
func (client *{{$srvIdent}}) {{.Name}}({{range .Arguments}}{{if .IsInput}}
 | 
			
		||||
	{{.Name}} string,
 | 
			
		||||
{{end}}{{end}}) ({{range .Arguments}}{{if .IsOutput}}
 | 
			
		||||
	{{.Name}} string,
 | 
			
		||||
{{end}}{{end}} err error) {
 | 
			
		||||
	request := {{$reqType}}{
 | 
			
		||||
{{range .Arguments}}{{if .IsInput}}
 | 
			
		||||
	{{.Name}}: {{.Name}},
 | 
			
		||||
{{end}}{{end}}
 | 
			
		||||
	}
 | 
			
		||||
	var response {{$respType}}
 | 
			
		||||
	err = client.SOAPClient.PerformAction({{$srv.URNParts.Const}}, {{.Name}}, &request, &response)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
{{range .Arguments}}{{if .IsOutput}}
 | 
			
		||||
	{{.Name}} = response.{{.Name}}
 | 
			
		||||
{{end}}{{end}}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
{{end}}{{/* range .SCPD.Actions */}}
 | 
			
		||||
{{end}}{{/* range .Services */}}
 | 
			
		||||
`))
 | 
			
		||||
							
								
								
									
										303
									
								
								cmd/specgen/specgen.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								cmd/specgen/specgen.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,303 @@
 | 
			
		||||
// specgen generates Go code from the UPnP specification files.
 | 
			
		||||
//
 | 
			
		||||
// The specification is available for download from:
 | 
			
		||||
// http://upnp.org/resources/upnpresources.zip
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"archive/zip"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/xml"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/huin/goupnp"
 | 
			
		||||
	"github.com/huin/goupnp/scpd"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// flags
 | 
			
		||||
var (
 | 
			
		||||
	specFilename = flag.String("spec", "", "Path to the specification file.")
 | 
			
		||||
	outDir       = flag.String("out-dir", "", "Path to the output directory.")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	deviceURNPrefix  = "urn:schemas-upnp-org:device:"
 | 
			
		||||
	serviceURNPrefix = "urn:schemas-upnp-org:service:"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	if *specFilename == "" {
 | 
			
		||||
		log.Fatal("--spec is required")
 | 
			
		||||
	}
 | 
			
		||||
	if *outDir == "" {
 | 
			
		||||
		log.Fatal("--out-dir is required")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	specArchive, err := openZipfile(*specFilename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Error opening %s: %v", *specFilename)
 | 
			
		||||
	}
 | 
			
		||||
	defer specArchive.Close()
 | 
			
		||||
 | 
			
		||||
	dcpsCol := newDcpsCollection(map[string]string{
 | 
			
		||||
		"Internet Gateway_1": "internetgateway1",
 | 
			
		||||
		"Internet Gateway_2": "internetgateway2",
 | 
			
		||||
	})
 | 
			
		||||
	for _, f := range globFiles("standardizeddcps/*/*.zip", specArchive.Reader) {
 | 
			
		||||
		dirName := strings.TrimPrefix(f.Name, "standardizeddcps/")
 | 
			
		||||
		slashIndex := strings.Index(dirName, "/")
 | 
			
		||||
		if slashIndex == -1 {
 | 
			
		||||
			// Should not happen.
 | 
			
		||||
			log.Printf("Could not find / in %q", dirName)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		dirName = dirName[:slashIndex]
 | 
			
		||||
 | 
			
		||||
		dcp := dcpsCol.dcpsForDir(dirName)
 | 
			
		||||
		if dcp == nil {
 | 
			
		||||
			log.Printf("No alias defined for directory %q: skipping", dirName)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		dcp.processZipFile(f)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, dcp := range dcpsCol.dcpsByAlias {
 | 
			
		||||
		if err := dcp.writePackage(*outDir); err != nil {
 | 
			
		||||
			log.Printf("Error writing package %q: %v", dcp.Name, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type dcpsCollection struct {
 | 
			
		||||
	dcpsAliasByDir map[string]string
 | 
			
		||||
	dcpsByAlias    map[string]*DCP
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newDcpsCollection(dcpsAliasByDir map[string]string) *dcpsCollection {
 | 
			
		||||
	c := &dcpsCollection{
 | 
			
		||||
		dcpsAliasByDir: dcpsAliasByDir,
 | 
			
		||||
		dcpsByAlias:    make(map[string]*DCP),
 | 
			
		||||
	}
 | 
			
		||||
	for _, alias := range dcpsAliasByDir {
 | 
			
		||||
		c.dcpsByAlias[alias] = newDCP(alias)
 | 
			
		||||
	}
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c dcpsCollection) dcpsForDir(dirName string) *DCP {
 | 
			
		||||
	alias, ok := c.dcpsAliasByDir[dirName]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return c.dcpsByAlias[alias]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DCP collects together information about a UPnP Device Control Protocol.
 | 
			
		||||
type DCP struct {
 | 
			
		||||
	Name         string
 | 
			
		||||
	DeviceTypes  map[string]*URNParts
 | 
			
		||||
	ServiceTypes map[string]*URNParts
 | 
			
		||||
	Services     []SCPDWithURN
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newDCP(name string) *DCP {
 | 
			
		||||
	return &DCP{
 | 
			
		||||
		Name:         name,
 | 
			
		||||
		DeviceTypes:  make(map[string]*URNParts),
 | 
			
		||||
		ServiceTypes: make(map[string]*URNParts),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (dcp *DCP) processZipFile(file *zip.File) {
 | 
			
		||||
	archive, err := openChildZip(file)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println("Error reading child zip file:", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	for _, deviceFile := range globFiles("*/device/*.xml", archive) {
 | 
			
		||||
		dcp.processDeviceFile(deviceFile)
 | 
			
		||||
	}
 | 
			
		||||
	for _, scpdFile := range globFiles("*/service/*.xml", archive) {
 | 
			
		||||
		dcp.processSCPDFile(scpdFile)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (dcp *DCP) processDeviceFile(file *zip.File) {
 | 
			
		||||
	var device goupnp.Device
 | 
			
		||||
	if err := unmarshalXmlFile(file, &device); err != nil {
 | 
			
		||||
		log.Printf("Error decoding device XML from file %q: %v", file.Name, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	device.VisitDevices(func(d *goupnp.Device) {
 | 
			
		||||
		t := strings.TrimSpace(d.DeviceType)
 | 
			
		||||
		if t != "" {
 | 
			
		||||
			u, err := extractURNParts(t, deviceURNPrefix)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Println(err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			dcp.DeviceTypes[t] = u
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	device.VisitServices(func(s *goupnp.Service) {
 | 
			
		||||
		u, err := extractURNParts(s.ServiceType, serviceURNPrefix)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		dcp.ServiceTypes[s.ServiceType] = u
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (dcp *DCP) writePackage(outDir string) error {
 | 
			
		||||
	packageDirname := filepath.Join(outDir, dcp.Name)
 | 
			
		||||
	err := os.MkdirAll(packageDirname, os.ModePerm)
 | 
			
		||||
	if err != nil && !os.IsExist(err) {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	packageFilename := filepath.Join(packageDirname, dcp.Name+".go")
 | 
			
		||||
	packageFile, err := os.Create(packageFilename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer packageFile.Close()
 | 
			
		||||
	return packageTmpl.Execute(packageFile, dcp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (dcp *DCP) processSCPDFile(file *zip.File) {
 | 
			
		||||
	scpd := new(scpd.SCPD)
 | 
			
		||||
	if err := unmarshalXmlFile(file, scpd); err != nil {
 | 
			
		||||
		log.Printf("Error decoding SCPD XML from file %q: %v", file.Name, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	scpd.Clean()
 | 
			
		||||
	urnParts, err := urnPartsFromSCPDFilename(file.Name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("Could not recognize SCPD filename %q: %v", file.Name, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	dcp.Services = append(dcp.Services, SCPDWithURN{
 | 
			
		||||
		URNParts: urnParts,
 | 
			
		||||
		SCPD:     scpd,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SCPDWithURN struct {
 | 
			
		||||
	*URNParts
 | 
			
		||||
	SCPD *scpd.SCPD
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type closeableZipReader struct {
 | 
			
		||||
	io.Closer
 | 
			
		||||
	*zip.Reader
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func openZipfile(filename string) (*closeableZipReader, error) {
 | 
			
		||||
	file, err := os.Open(filename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	fi, err := file.Stat()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	archive, err := zip.NewReader(file, fi.Size())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &closeableZipReader{
 | 
			
		||||
		Closer: file,
 | 
			
		||||
		Reader: archive,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// openChildZip opens a zip file within another zip file.
 | 
			
		||||
func openChildZip(file *zip.File) (*zip.Reader, error) {
 | 
			
		||||
	zipFile, err := file.Open()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer zipFile.Close()
 | 
			
		||||
 | 
			
		||||
	zipBytes, err := ioutil.ReadAll(zipFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func globFiles(pattern string, archive *zip.Reader) []*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)
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user