diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d94e75d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +sudo: false + +language: go + +go: + - 1.6 + +script: + - go test -v ./wsdl ./wsdlgo diff --git a/README.md b/README.md new file mode 100644 index 0000000..dec74fa --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# wsdl2go + +wsdl2go is a command line tool to generate [Go](https://golang.org) code +from [WSDL](https://en.wikipedia.org/wiki/Web_Services_Description_Language). + +### Status + +Not fully compliant with SOAP or WSDL. Works for my needs and has been +tested with a few SOAP enterprise systems. + +There are limitations related to XML namespaces in Go, which might impact +how this program works. Details: https://github.com/golang/go/issues/14407. + +WSDL types supported: + +- [x] int +- [x] long (int64) +- [x] float (float64) +- [x] boolean (bool) +- [x] string +- [x] date +- [x] time +- [x] dateTime +- [x] complexType (struct) + +Date types are currently defined as strings, need to implement XML +Marshaler and Unmarshaler interfaces. + +The Go code generator (package wsdlgo) is capable of importing remote +parts of the WSDL via HTTP. You can configure its http.Client to support +authentication and self-signed certificates. diff --git a/contrib/release.sh b/contrib/release.sh new file mode 100644 index 0000000..eab651b --- /dev/null +++ b/contrib/release.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +VERSION=${VERSION:-`git describe --tags`} + +for OS in linux freebsd windows darwin +do + GOOS=$OS GOARCH=amd64 go build -ldflags "-w -X main.version=${VERSION}" + tar czf wsdl2go-$VERSION-$OS-amd64.tar.gz wsdl2go* +done diff --git a/main.go b/main.go new file mode 100644 index 0000000..0975194 --- /dev/null +++ b/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "crypto/tls" + "flag" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + + "github.com/fiorix/wsdl2go/wsdl" + "github.com/fiorix/wsdl2go/wsdlgo" +) + +var version = "tip" + +func main() { + opts := struct { + Src string + Dst string + Insecure bool + Version bool + }{} + flag.StringVar(&opts.Src, "i", opts.Src, "input file, url, or '-' for stdin") + flag.StringVar(&opts.Dst, "o", opts.Dst, "output file, or '-' for stdout") + flag.BoolVar(&opts.Insecure, "yolo", opts.Insecure, "accept invalid https certificates") + flag.BoolVar(&opts.Version, "version", opts.Version, "show version and exit") + flag.Parse() + if opts.Version { + fmt.Printf("wsdl2go %s\n", version) + return + } + var w io.Writer + switch opts.Dst { + case "", "-": + w = os.Stdout + default: + f, err := os.OpenFile(opts.Dst, os.O_WRONLY, 0644) + if err != nil { + log.Fatal(err) + } + defer f.Close() + w = f + } + cli := http.DefaultClient + if opts.Insecure { + cli.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + err := decode(w, opts.Src, cli) + if err != nil { + log.Fatal(err) + } +} + +func decode(w io.Writer, src string, cli *http.Client) error { + var err error + var f io.ReadCloser + if src == "" || src == "-" { + f = os.Stdin + } else if f, err = open(src, cli); err != nil { + return err + } + d, err := wsdl.Unmarshal(f) + if err != nil { + return err + } + f.Close() + enc := wsdlgo.NewEncoder(w) + enc.SetClient(cli) + return enc.Encode(d) +} + +func open(name string, cli *http.Client) (io.ReadCloser, error) { + _, err := url.Parse(name) + if err != nil { + return os.Open(name) + } + resp, err := cli.Get(name) + if err != nil { + return nil, err + } + return resp.Body, err +} diff --git a/wsdl/decoder.go b/wsdl/decoder.go new file mode 100644 index 0000000..dc1d386 --- /dev/null +++ b/wsdl/decoder.go @@ -0,0 +1,22 @@ +// Package wsdl provides Web Services Description Language (WSDL) decoder. +// +// http://www.w3schools.com/xml/xml_wsdl.asp +package wsdl + +import ( + "encoding/xml" + "io" +) + +// Unmarshal unmarshals WSDL documents starting from the tag. +// +// The Definitions object it returns is an unmarshalled version of the +// WSDL XML that can be introspected to generate the Web Services API. +func Unmarshal(r io.Reader) (*Definitions, error) { + var d Definitions + err := xml.NewDecoder(r).Decode(&d) + if err != nil { + return nil, err + } + return &d, nil +} diff --git a/wsdl/decoder_test.go b/wsdl/decoder_test.go new file mode 100644 index 0000000..581f441 --- /dev/null +++ b/wsdl/decoder_test.go @@ -0,0 +1,43 @@ +package wsdl + +import ( + "encoding/xml" + "os" + "path/filepath" + "reflect" + "testing" +) + +func TestUnmarshal(t *testing.T) { + cases := []struct { + F string + E error + }{ + { + F: "golden1.wsdl", + E: nil, + }, { + F: "golden2.wsdl", + E: xml.UnmarshalError("..."), + }, + } + for i, tc := range cases { + f, err := os.Open(filepath.Join("testdata", tc.F)) + if err != nil { + t.Errorf("test %d failed: %v", i, err) + } + defer f.Close() + _, err = Unmarshal(f) + if tc.E == nil { + if err != nil { + t.Errorf("test %d failed: want %v, have %v", i, tc.E, err) + } + continue + } + want := reflect.ValueOf(tc.E).Type().Name() + have := reflect.ValueOf(err).Type().Name() + if want != have { + t.Errorf("test %d failed: want %q, have %q", i, want, have) + } + } +} diff --git a/wsdl/testdata/golden1.wsdl b/wsdl/testdata/golden1.wsdl new file mode 100644 index 0000000..c83d04c --- /dev/null +++ b/wsdl/testdata/golden1.wsdl @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/wsdl/testdata/golden2.wsdl b/wsdl/testdata/golden2.wsdl new file mode 100644 index 0000000..6807cd6 --- /dev/null +++ b/wsdl/testdata/golden2.wsdl @@ -0,0 +1 @@ + diff --git a/wsdl/types.go b/wsdl/types.go new file mode 100644 index 0000000..938cf83 --- /dev/null +++ b/wsdl/types.go @@ -0,0 +1,190 @@ +package wsdl + +// TODO: Add all types from the spec. + +import "encoding/xml" + +// Definitions is the root element of a WSDL document. +type Definitions struct { + XMLName xml.Name `xml:"definitions"` + Name string `xml:"name,attr"` + TargetNamespace string `xml:"targetNamespace,attr"` + Service Service `xml:"service"` + Imports []*Import `xml:"import"` + Schema Schema `xml:"types>schema"` + Messages []*Message `xml:"message"` + PortType PortType `xml:"portType"` // TODO: PortType slice? + Binding Binding `xml:"binding"` +} + +// Service defines a WSDL service and with a location, like an HTTP server. +type Service struct { + Doc string `xml:"documentation"` + Ports []*Port `xml:"port"` +} + +// Port for WSDL service. +type Port struct { + XMLName xml.Name `xml:"port"` + Name string `xml:"name,attr"` + Binding string `xml:"binding,attr"` + Address Address `xml:"address"` +} + +// Address of WSDL service. +type Address struct { + XMLName xml.Name `xml:"address"` + Location string `xml:"location,attr"` +} + +// Schema of WSDL document. +type Schema struct { + XMLName xml.Name `xml:"schema"` + TargetNamespace string `xml:"targetNamespace,attr"` + ElementFormDefault string `xml:"elementFormDefault,attr"` + AttributeFormDefault string `xml:"attributeFormDefault,attr"` + Imports []*ImportSchema `xml:"import"` + SimpleTypes []*SimpleType `xml:"simpleType"` + ComplexTypes []*ComplexType `xml:"complexType"` + Elements []*Element `xml:"element"` +} + +// SimpleType describes a simple type, such as string. +type SimpleType struct { + XMLName xml.Name `xml:"simpleType"` + Name string `xml:"name,attr"` + Restriction *Restriction `xml:"restriction"` +} + +// Restriction describes the WSDL type of the simple type and +// optionally its allowed values. +type Restriction struct { + XMLName xml.Name `xml:"restriction"` + Base string `xml:"base,attr"` + Enum []*Enum `xml:"enumeration"` +} + +// Enum describes one possible value for a Restriction. +type Enum struct { + XMLName xml.Name `xml:"enumeration"` + Value string `xml:"value,attr"` +} + +// ComplexType describes a complex type, such as a struct. +type ComplexType struct { + XMLName xml.Name `xml:"complexType"` + Name string `xml:"name,attr"` + Abstract bool `xml:"abstract,attr"` + Doc string `xml:"annotation>documentation"` + AllElements []*Element `xml:"all>element"` + ComplexContent *ComplexContent `xml:"complexContent"` + Sequence *Sequence `xml:"sequence"` +} + +// ComplexContent describes complex content within a complex type. Usually +// for extending the complex type with fields from the complex content. +type ComplexContent struct { + XMLName xml.Name `xml:"complexContent"` + Extension *Extension `xml:"extension"` +} + +// Extension describes a complex content extension. +type Extension struct { + XMLName xml.Name `xml:"extension"` + Base string `xml:"base,attr"` + Sequence *Sequence `xml:"sequence"` +} + +// Sequence describes a list of elements (parameters) of a type. +type Sequence struct { + XMLName xml.Name `xml:"sequence"` + ComplexTypes []*ComplexType `xml:"complexType"` + Elements []*Element `xml:"element"` +} + +// Element describes an element of a given type. +type Element struct { + XMLName xml.Name `xml:"element"` + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + Min int `xml:"minOccurs,attr"` + Max string `xml:"maxOccurs,attr"` // can be # or unbounded + Nillable bool `xml:"nillable,attr"` + ComplexType *ComplexType `xml:"complexType"` +} + +// Import points to another WSDL to be imported at root level. +type Import struct { + XMLName xml.Name `xml:"import"` + Namespace string `xml:"namespace,attr"` + Location string `xml:"location,attr"` +} + +// ImportSchema points to another WSDL to be imported at schema level. +type ImportSchema struct { + XMLName xml.Name `xml:"import"` + Namespace string `xml:"namespace,attr"` + Location string `xml:"schemaLocation,attr"` +} + +// Message describes the data being communicated, such as functions +// and their parameters. +type Message struct { + XMLName xml.Name `xml:"message"` + Name string `xml:"name,attr"` + Parts []*Part `xml:"part"` +} + +// Part describes what Type or Element to use from the PortType. +type Part struct { + XMLName xml.Name `xml:"part"` + Name string `xml:"name,attr"` + Type string `xml:"type,attr,omitempty"` + Element string `xml:"element,attr,omitempty"` // TODO: not sure omitempty +} + +// PortType describes a set of operations. +type PortType struct { + XMLName xml.Name `xml:"portType"` + Name string `xml:"name,attr"` + Operations []*Operation `xml:"operation"` +} + +// Operation describes an operation. +type Operation struct { + XMLName xml.Name `xml:"operation"` + Name string `xml:"name,attr"` + Doc string `xml:"documentation"` + Input *IO `xml:"input"` + Output *IO `xml:"output"` +} + +// IO describes which message is linked to an operation, for input +// or output parameters. +type IO struct { + XMLName xml.Name + Message string `xml:"message,attr"` +} + +// Binding describes SOAP to WSDL binding. +type Binding struct { + XMLName xml.Name `xml:"binding"` + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + Operations []*BindingOperation `xml:"operation"` +} + +// BindingOperation describes the requirement for binding SOAP to WSDL +// operations. +type BindingOperation struct { + XMLName xml.Name `xml:"operation"` + Name string `xml:"name,attr"` + Input *BindingIO `xml:"input>body"` + Output *BindingIO `xml:"output>body"` +} + +// BindingIO describes the IO binding of SOAP operations. See IO for details. +type BindingIO struct { + Parts string `xml:"parts,attr"` + Use string `xml:"use,attr"` +} diff --git a/wsdlgo/encoder.go b/wsdlgo/encoder.go new file mode 100644 index 0000000..8485fcc --- /dev/null +++ b/wsdlgo/encoder.go @@ -0,0 +1,648 @@ +// Package wsdlgo provides an encoder from WSDL to Go code. +package wsdlgo + +// TODO: make it generate code fully compliant with the spec. +// TODO: support all WSDL types. +// TODO: fully support SOAP bindings, faults, and transports. + +import ( + "bytes" + "encoding/xml" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/fiorix/wsdl2go/wsdl" +) + +// An Encoder generates Go code from WSDL definitions. +type Encoder interface { + // Encode generates Go code from d. + Encode(d *wsdl.Definitions) error + + // SetClient records the given http client that + // is used when fetching remote parts of WSDL + // and WSDL schemas. + SetClient(c *http.Client) +} + +type goEncoder struct { + // where to write Go code + w io.Writer + + // http client + http *http.Client + + // types cache + stypes map[string]*wsdl.SimpleType + ctypes map[string]*wsdl.ComplexType + + // funcs cache + funcs map[string]*wsdl.Operation + + // messages cache + messages map[string]*wsdl.Message + + // whether to add supporting types + needsReflect bool + needsDateType bool + needsTimeType bool + needsDateTimeType bool + needsDurationType bool +} + +// NewEncoder creates and initializes an Encoder that generates code to w. +func NewEncoder(w io.Writer) Encoder { + return &goEncoder{ + w: w, + http: http.DefaultClient, + stypes: make(map[string]*wsdl.SimpleType), + ctypes: make(map[string]*wsdl.ComplexType), + funcs: make(map[string]*wsdl.Operation), + messages: make(map[string]*wsdl.Message), + } +} + +func (ge *goEncoder) SetClient(c *http.Client) { + ge.http = c +} + +func (ge *goEncoder) Encode(d *wsdl.Definitions) error { + if d == nil { + return nil + } + var b bytes.Buffer + err := ge.encode(&b, d) + if err != nil { + return err + } + if b.Len() == 0 { + return nil + } + var errb bytes.Buffer + input := b.String() + // dat pipe + cmd := exec.Cmd{ + Path: filepath.Join(os.Getenv("GOROOT"), "bin", "gofmt"), + Stdin: &b, + Stdout: ge.w, + Stderr: &errb, + } + err = cmd.Run() + if err != nil { + var x bytes.Buffer + fmt.Fprintf(&x, "gofmt: %v\n", err) + if errb.Len() > 0 { + fmt.Fprintf(&x, "gofmt stderr:\n%s\n", errb.String()) + } + fmt.Fprintf(&x, "generated code:\n%s\n", input) + return fmt.Errorf(x.String()) + } + return nil +} + +func (ge *goEncoder) encode(w io.Writer, d *wsdl.Definitions) error { + err := ge.importParts(d) + if err != nil { + return fmt.Errorf("wsdl import: %v", err) + } + ge.cacheTypes(d) + ge.cacheFuncs(d) + ge.cacheMessages(d) + pkg := strings.ToLower(d.Binding.Name) + if pkg == "" { + pkg = "internal" + } + var b bytes.Buffer + err = ge.writeGoFuncs(&b, d) // functions first, for clarity + if err != nil { + return err + } + err = ge.writeGoTypes(&b, d) + if err != nil { + return err + } + fmt.Fprintf(w, "package %s\n\nimport (\n\"errors\"\n", pkg) + if ge.needsReflect { + fmt.Fprintf(w, "\"reflect\"\n") + } + fmt.Fprintf(w, "\n\"golang.org/x/net/context\"\n)\n\n") + _, err = io.Copy(w, &b) + return err +} + +func (ge *goEncoder) importParts(d *wsdl.Definitions) error { + err := ge.importRoot(d) + if err != nil { + return err + } + return ge.importSchema(d) +} + +func (ge *goEncoder) importRoot(d *wsdl.Definitions) error { + for _, imp := range d.Imports { + if imp.Location == "" { + continue + } + err := ge.importRemote(imp.Location, &d) + if err != nil { + return err + } + } + return nil +} + +func (ge *goEncoder) importSchema(d *wsdl.Definitions) error { + for _, imp := range d.Schema.Imports { + if imp.Location == "" { + continue + } + err := ge.importRemote(imp.Location, &d.Schema) + if err != nil { + return err + } + } + return nil +} + +// download xml from url, decode in v. +func (ge *goEncoder) importRemote(url string, v interface{}) error { + resp, err := ge.http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + return xml.NewDecoder(resp.Body).Decode(v) +} + +func (ge *goEncoder) cacheTypes(d *wsdl.Definitions) { + // operation types are declared as go struct types + for _, v := range d.Schema.Elements { + if v.Type == "" && v.ComplexType != nil { + ct := *v.ComplexType + ct.Name = v.Name + ge.ctypes[v.Name] = &ct + } + } + // simple types map 1:1 to go basic types + for _, v := range d.Schema.SimpleTypes { + ge.stypes[v.Name] = v + } + // complex types are declared as go struct types + for _, v := range d.Schema.ComplexTypes { + ge.ctypes[v.Name] = v + } +} + +func (ge *goEncoder) cacheFuncs(d *wsdl.Definitions) { + // operations are declared as boilerplate go functions + for _, v := range d.PortType.Operations { + ge.funcs[v.Name] = v + } +} + +func (ge *goEncoder) cacheMessages(d *wsdl.Definitions) { + for _, v := range d.Messages { + ge.messages[v.Name] = v + } +} + +// writeGoFuncs writes Go function definitions from WSDL types to w. +// Functions are written in the same order of the WSDL document. +func (ge *goEncoder) writeGoFuncs(w io.Writer, d *wsdl.Definitions) error { + if d.Binding.Type != "" { + a, b := ge.trimns(d.Binding.Type), ge.trimns(d.PortType.Name) + if a != b { + return fmt.Errorf( + "binding %q requires port type %q but it's not defined", + d.Binding.Name, d.Binding.Type) + } + } + if d.PortType.Operations == nil { + return nil + } + for _, op := range d.PortType.Operations { + // TODO: really rename input to have Request suffix? + ge.writeComments(w, op.Name, op.Doc) + in, err := ge.inputParams(op) + if err != nil { + return err + } + out, err := ge.outputParams(op) + if err != nil { + return err + } + ret := make([]string, len(out)) + for i, p := range out { + parts := strings.SplitN(p, " ", 2) + if len(parts) == 2 { + ret[i] = ge.wsdl2goDefault(parts[1]) + } + } + ge.fixParamConflicts(in, out) + fmt.Fprintf(w, "func %s(%s) (%s) {\nreturn %s\n}\n\n", + strings.Title(op.Name), + strings.Join(in, ","), + strings.Join(out, ","), + strings.Join(ret, ","), + ) + } + return nil +} + +func (ge *goEncoder) inputParams(op *wsdl.Operation) ([]string, error) { + in := []string{"ctx context.Context"} + if op.Input == nil { + return in, nil + } + im := ge.trimns(op.Input.Message) + req, ok := ge.messages[im] + if !ok { + return nil, fmt.Errorf("operation %q wants input message %q but it's not defined", op.Name, im) + } + return append(in, ge.genParams(req, "Request")...), nil +} + +func (ge *goEncoder) outputParams(op *wsdl.Operation) ([]string, error) { + out := []string{"err error"} + if op.Output == nil { + return out, nil + } + om := ge.trimns(op.Output.Message) + resp, ok := ge.messages[om] + if !ok { + return nil, fmt.Errorf("operation %q wants output message %q but it's not defined", op.Name, om) + } + return append(ge.genParams(resp, "Response"), out[0]), nil +} + +func (ge *goEncoder) genParams(m *wsdl.Message, suffix string) []string { + params := make([]string, len(m.Parts)) + for i, param := range m.Parts { + var t string + switch { + case param.Type != "": + t = ge.wsdl2goType(param.Type, suffix) + case param.Element != "": + t = ge.wsdl2goType(param.Element, suffix) + } + params[i] = param.Name + " " + t + } + return params +} + +// Fixes request and response parameters with the same name, in place. +// Each string in the slice consists of Go's "name Type", we only +// compare names. In case of a conflict, we set the response one +// in the form of respName. +func (ge *goEncoder) fixParamConflicts(req, resp []string) { + for _, a := range req { + for j, b := range resp { + x := strings.SplitN(a, " ", 2)[0] + y := strings.SplitN(b, " ", 2) + if len(y) > 1 { + if x == y[0] { + n := strings.Title(y[0]) + resp[j] = "resp" + n + " " + y[1] + } + } + } + } +} + +// Converts types from wsdl type to Go type. If t is a complex type +// (a struct, as opposed to int or string) and a suffix is provided, +// we look for the suffix in its name and add if needed. When we do +// that, we also update the list of cached ctypes to match this new +// type name, with the suffix (e.g. ping -> pingRequest). This is +// to avoid ambiguous parameter and function names. +func (ge *goEncoder) wsdl2goType(t, suffix string) string { + // TODO: support other types. + v := ge.trimns(t) + if _, exists := ge.stypes[v]; exists { + return v + } + switch strings.ToLower(v) { + case "int": + return "int" + case "long": + return "int64" + case "float": + return "float64" + case "boolean": + return "bool" + case "string": + return "string" + case "date": + ge.needsDateType = true + return "Date" + case "time": + ge.needsTimeType = true + return "Time" + case "datetime": + ge.needsDateTimeType = true + return "DateTime" + case "duration": + ge.needsDurationType = true + return "Duration" + default: + if suffix != "" && !strings.HasSuffix(t, suffix) { + ge.renameType(t, t+suffix) + t = v + suffix + } else { + t = v + } + if len(t) == 0 { + return "FIXME" + } + return "*" + strings.Title(t) + } +} + +// Returns the default Go type for the given wsdl type. +func (ge *goEncoder) wsdl2goDefault(t string) string { + v := ge.trimns(t) + if v != "" && v[0] == '*' { + v = v[1:] + } + switch v { + case "error": + return `errors.New("not implemented")` + case "bool": + return "false" + case "int", "int64", "float64": + return "0" + case "string": + return `""` + default: + return "&" + v + "{}" + } +} + +func (ge *goEncoder) trimns(s string) string { + n := strings.SplitN(s, ":", 2) + if len(n) == 2 { + return n[1] + } + return s +} + +func (ge *goEncoder) renameType(old, name string) { + // TODO: rename Elements that point to this type also? + ct, exists := ge.ctypes[old] + if !exists { + old = ge.trimns(old) + ct, exists = ge.ctypes[old] + if !exists { + return + } + name = ge.trimns(name) + } + ct.Name = name + delete(ge.ctypes, old) + ge.ctypes[name] = ct +} + +// writeGoTypes writes Go types from WSDL types to w. +// +// Types are written in this order, alphabetically: date types that we +// generate, simple types, then complex types. +func (ge *goEncoder) writeGoTypes(w io.Writer, d *wsdl.Definitions) error { + var b bytes.Buffer + for _, name := range ge.sortedSimpleTypes() { + st := ge.stypes[name] + if st.Restriction == nil { + continue + } + ge.writeComments(&b, st.Name, "") + fmt.Fprintf(w, "type %s %s\n\n", st.Name, ge.wsdl2goType(st.Restriction.Base, "")) + ge.genValidator(&b, st.Name, st.Restriction) + } + var err error + for _, name := range ge.sortedComplexTypes() { + ct := ge.ctypes[name] + err = ge.genGoStruct(&b, ct) + if err != nil { + return err + } + } + ge.genDateTypes(w) // must be called last + _, err = io.Copy(w, &b) + return err +} + +func (ge *goEncoder) sortedSimpleTypes() []string { + keys := make([]string, len(ge.stypes)) + i := 0 + for k := range ge.stypes { + keys[i] = k + i++ + } + sort.Strings(keys) + return keys +} + +func (ge *goEncoder) sortedComplexTypes() []string { + keys := make([]string, len(ge.ctypes)) + i := 0 + for k := range ge.ctypes { + keys[i] = k + i++ + } + sort.Strings(keys) + return keys +} + +func (ge *goEncoder) genDateTypes(w io.Writer) { + cases := []struct { + needs bool + name string + code string + }{ + { + needs: ge.needsDateType, + name: "Date", + code: "type Date string\n\n", + }, + { + needs: ge.needsTimeType, + name: "Time", + code: "type Time string\n\n", + }, + { + needs: ge.needsDateTimeType, + name: "DateTime", + code: "type DateTime string\n\n", + }, + { + needs: ge.needsDurationType, + name: "Duration", + code: "type Duration string\n\n", + }, + } + for _, c := range cases { + if !c.needs { + continue + } + ge.writeComments(w, c.name, c.name+" in WSDL format.") + io.WriteString(w, c.code) + } +} + +func (ge *goEncoder) genValidator(w io.Writer, typeName string, r *wsdl.Restriction) { + if len(r.Enum) == 0 { + return + } + args := make([]string, len(r.Enum)) + t := ge.wsdl2goType(r.Base, "") + for i, v := range r.Enum { + if t == "string" { + args[i] = strconv.Quote(v.Value) + } else { + args[i] = v.Value + } + } + fmt.Fprintf(w, "// Validate validates the %s.", typeName) + fmt.Fprintf(w, "\nfunc (v %s) Validate() bool {\n", typeName) + fmt.Fprintf(w, "for _, vv := range []%s{\n", t) + fmt.Fprintf(w, "%s,\n", strings.Join(args, ",\n")) + fmt.Fprintf(w, "}{\nif reflect.DeepEqual(v, vv) { return true }\n}\nreturn false\n}\n\n") + if !ge.needsReflect { + ge.needsReflect = true + } +} + +func (ge *goEncoder) genGoStruct(w io.Writer, ct *wsdl.ComplexType) error { + if ct.Abstract { + return nil + } + c := 0 + if len(ct.AllElements) == 0 { + c++ + } + if ct.ComplexContent == nil || ct.ComplexContent.Extension == nil { + c++ + } + if ct.Sequence == nil { + c++ + } else if len(ct.Sequence.ComplexTypes) == 0 && len(ct.Sequence.Elements) == 0 { + c++ + } + if c > 2 { + // dont generate empty structs + return nil + } + ge.writeComments(w, ct.Name, ct.Doc) + fmt.Fprintf(w, "type %s struct {\n", ct.Name) + err := ge.genStructFields(w, ct) + if err != nil { + return err + } + fmt.Fprintf(w, "}\n\n") + return nil +} + +func (ge *goEncoder) genStructFields(w io.Writer, ct *wsdl.ComplexType) error { + err := ge.genComplexContent(w, ct) + if err != nil { + return err + } + return ge.genElements(w, ct) +} + +func (ge *goEncoder) genComplexContent(w io.Writer, ct *wsdl.ComplexType) error { + if ct.ComplexContent == nil || ct.ComplexContent.Extension == nil { + return nil + } + ext := ct.ComplexContent.Extension + if ext.Base != "" { + base, exists := ge.ctypes[ge.trimns(ext.Base)] + if exists { + err := ge.genStructFields(w, base) + if err != nil { + return err + } + } + } + if ext.Sequence == nil { + return nil + } + seq := ext.Sequence + for _, v := range seq.ComplexTypes { + err := ge.genElements(w, v) + if err != nil { + return err + } + } + for _, v := range ext.Sequence.Elements { + ge.genElementField(w, v) + } + return nil +} + +func (ge *goEncoder) genElements(w io.Writer, ct *wsdl.ComplexType) error { + for _, el := range ct.AllElements { + ge.genElementField(w, el) + } + if ct.Sequence == nil { + return nil + } + for _, el := range ct.Sequence.Elements { + ge.genElementField(w, el) + } + return nil +} + +func (ge *goEncoder) genElementField(w io.Writer, el *wsdl.Element) { + if el.Type == "" && el.ComplexType != nil { + seq := el.ComplexType.Sequence + if seq != nil && len(seq.Elements) == 1 { + n := el.Name + el = el.ComplexType.Sequence.Elements[0] + el.Name = n + } + } + fmt.Fprintf(w, "%s ", strings.Title(el.Name)) + if el.Max != "" && el.Max != "1" { + fmt.Fprintf(w, "[]") + } + fmt.Fprint(w, ge.wsdl2goType(el.Type, "")) + fmt.Fprintf(w, " `xml:\"%s", el.Name) + if el.Nillable || el.Min == 0 { + fmt.Fprintf(w, ",omitempty") + } + fmt.Fprintf(w, "\"`\n") +} + +// writeComments writes comments to w, capped at ~80 columns. +func (ge *goEncoder) writeComments(w io.Writer, typeName, comment string) { + comment = strings.Trim(strings.Replace(comment, "\n", " ", -1), " ") + if comment == "" { + comment = strings.Title(typeName) + " was auto-generated from WSDL." + } + count, line := 0, "" + words := strings.Split(comment, " ") + for _, word := range words { + if line == "" { + count, line = 2, "//" + } + count += len(word) + if count > 60 { + fmt.Fprintf(w, "%s %s\n", line, word) + count, line = 0, "" + continue + } + line = line + " " + word + count++ + } + if line != "" { + fmt.Fprintf(w, "%s\n", line) + } + return +} diff --git a/wsdlgo/encoder_test.go b/wsdlgo/encoder_test.go new file mode 100644 index 0000000..2d54ba9 --- /dev/null +++ b/wsdlgo/encoder_test.go @@ -0,0 +1,117 @@ +package wsdlgo + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/fiorix/wsdl2go/wsdl" +) + +func LoadDefinition(t *testing.T, filename string, want error) *wsdl.Definitions { + f, err := os.Open(filepath.Join("testdata", filename)) + if err != nil { + t.Errorf("missing wsdl file %q: %v", filename, err) + } + defer f.Close() + d, err := wsdl.Unmarshal(f) + if err != want { + t.Errorf("%q failed: want %v, have %v", filename, want, err) + } + return d +} + +var EncoderCases = []struct { + F string + G string + E error +}{ + {F: "broken.wsdl", E: io.EOF}, + {F: "w3cexample1.wsdl", G: "w3cexample1.golden", E: nil}, + {F: "w3cexample2.wsdl", G: "w3cexample2.golden", E: nil}, + {F: "w3example1.wsdl", G: "w3example1.golden", E: nil}, + {F: "w3example2.wsdl", G: "w3example2.golden", E: nil}, + {F: "memcache.wsdl", G: "memcache.golden", E: nil}, + {F: "importer.wsdl", G: "memcache.golden", E: nil}, +} + +func NewTestServer(t *testing.T) *httptest.Server { + mux := http.NewServeMux() + fs := http.FileServer(http.Dir("testdata")) + mux.Handle("/", fs) + s := httptest.NewUnstartedServer(mux) + l, err := net.Listen("tcp4", ":9999") + if err != nil { + t.Fatalf("cannot listen on 9999: %v", err) + } + s.Listener = l + s.Start() + return s +} + +func TestEncoder(t *testing.T) { + s := NewTestServer(t) + defer s.Close() + for i, tc := range EncoderCases { + d := LoadDefinition(t, tc.F, tc.E) + var err error + var want []byte + var have bytes.Buffer + err = NewEncoder(&have).Encode(d) + if err != nil { + t.Errorf("test %d, encoding %q: %v", i, tc.F, err) + } + if tc.G == "" { + continue + } + want, err = ioutil.ReadFile(filepath.Join("testdata", tc.G)) + if err != nil { + t.Errorf("test %d: missing golden file %q: %v", i, tc.G, err) + } + if !bytes.Equal(have.Bytes(), want) { + err := Diff("_diff", "go", want, have.Bytes()) + t.Errorf("test %d, %q != %q: %v\ngenerated:\n%s\n", + i, tc.F, tc.G, err, have.Bytes()) + } + } +} + +func Diff(prefix, ext string, a, b []byte) error { + diff, err := exec.LookPath("diff") + if err != nil { + return fmt.Errorf("diff: %v", err) + } + cases := []struct { + File string + Data []byte + }{ + {File: prefix + "-a." + ext, Data: a}, + {File: prefix + "-b." + ext, Data: b}, + } + for _, c := range cases { + defer os.Remove(c.File) + if err = ioutil.WriteFile(c.File, c.Data, 0600); err != nil { + return err + } + } + var stdout, stderr bytes.Buffer + cmd := exec.Cmd{ + Path: diff, + Args: []string{"-u", cases[0].File, cases[1].File}, + Stdout: &stdout, + Stderr: &stderr, + } + err = cmd.Run() + if err != nil { + return fmt.Errorf("%v: %s", err, stdout.String()) + } + return nil +} diff --git a/wsdlgo/testdata/broken.wsdl b/wsdlgo/testdata/broken.wsdl new file mode 100644 index 0000000..57c5494 --- /dev/null +++ b/wsdlgo/testdata/broken.wsdl @@ -0,0 +1 @@ +fail-me diff --git a/wsdlgo/testdata/importer-root.wsdl b/wsdlgo/testdata/importer-root.wsdl new file mode 100644 index 0000000..d2c8407 --- /dev/null +++ b/wsdlgo/testdata/importer-root.wsdl @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wsdlgo/testdata/importer-schema.wsdl b/wsdlgo/testdata/importer-schema.wsdl new file mode 100644 index 0000000..bf82274 --- /dev/null +++ b/wsdlgo/testdata/importer-schema.wsdl @@ -0,0 +1,36 @@ + + + + + SetRequest carries a key-value pair. + + + + + + + + + + + + GetResponse carries value and TTL. + + + + + + + + + + + + + + + + + + + diff --git a/wsdlgo/testdata/importer.wsdl b/wsdlgo/testdata/importer.wsdl new file mode 100644 index 0000000..dc5537a --- /dev/null +++ b/wsdlgo/testdata/importer.wsdl @@ -0,0 +1,55 @@ + + + + xmlns:tns="http://localhost:8080/EchoService.wsdl" + + + + + + + + + + + + + + + + + + + + + + + + + + + + WSDL File for HelloService + + + + + + + diff --git a/wsdlgo/testdata/memcache.golden b/wsdlgo/testdata/memcache.golden new file mode 100644 index 0000000..a6db8f1 --- /dev/null +++ b/wsdlgo/testdata/memcache.golden @@ -0,0 +1,48 @@ +package memoryservice + +import ( + "errors" + + "golang.org/x/net/context" +) + +// Get was auto-generated from WSDL. +func Get(ctx context.Context, key string) (resp *GetResponse, err error) { + return &GetResponse{}, errors.New("not implemented") +} + +// Set was auto-generated from WSDL. +func Set(ctx context.Context, info *SetRequest) (ok bool, err error) { + return false, errors.New("not implemented") +} + +// GetMulti was auto-generated from WSDL. +func GetMulti(ctx context.Context, keys *GetMultiRequest) (values *GetMultiResponse, err error) { + return &GetMultiResponse{}, errors.New("not implemented") +} + +// Duration in WSDL format. +type Duration string + +// GetMultiRequest was auto-generated from WSDL. +type GetMultiRequest struct { + Keys []string `xml:"Keys"` +} + +// GetMultiResponse was auto-generated from WSDL. +type GetMultiResponse struct { + Values []*GetResponse `xml:"Values,omitempty"` +} + +// GetResponse carries value and TTL. +type GetResponse struct { + Value string `xml:"Value,omitempty"` + TTL Duration `xml:"TTL,omitempty"` +} + +// SetRequest carries a key-value pair. +type SetRequest struct { + Key string `xml:"Key"` + Value string `xml:"Value"` + Expiration Duration `xml:"Expiration,omitempty"` +} diff --git a/wsdlgo/testdata/memcache.wsdl b/wsdlgo/testdata/memcache.wsdl new file mode 100644 index 0000000..da83010 --- /dev/null +++ b/wsdlgo/testdata/memcache.wsdl @@ -0,0 +1,130 @@ + + + + xmlns:tns="http://localhost:8080/EchoService.wsdl" + + + + + + + SetRequest carries a key-value pair. + + + + + + + + + + + + GetResponse carries value and TTL. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WSDL File for HelloService + + + + + + diff --git a/wsdlgo/testdata/tpexample1.golden b/wsdlgo/testdata/tpexample1.golden new file mode 100644 index 0000000..015b1cb --- /dev/null +++ b/wsdlgo/testdata/tpexample1.golden @@ -0,0 +1,12 @@ +package hello_binding + +import ( + "errors" + + "golang.org/x/net/context" +) + +// SayHello was auto-generated from WSDL. +func SayHello(ctx context.Context, firstName string) (greeting string, err error) { + return "", errors.New("not implemented") +} diff --git a/wsdlgo/testdata/tpexample1.wsdl b/wsdlgo/testdata/tpexample1.wsdl new file mode 100644 index 0000000..4a87ea5 --- /dev/null +++ b/wsdlgo/testdata/tpexample1.wsdl @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WSDL File for HelloService + + + + + diff --git a/wsdlgo/testdata/w3cexample1.golden b/wsdlgo/testdata/w3cexample1.golden new file mode 100644 index 0000000..096f08a --- /dev/null +++ b/wsdlgo/testdata/w3cexample1.golden @@ -0,0 +1,12 @@ +package internal + +import ( + "errors" + + "golang.org/x/net/context" +) + +// GetTerm was auto-generated from WSDL. +func GetTerm(ctx context.Context, term string) (value string, err error) { + return "", errors.New("not implemented") +} diff --git a/wsdlgo/testdata/w3cexample1.wsdl b/wsdlgo/testdata/w3cexample1.wsdl new file mode 100644 index 0000000..c83d04c --- /dev/null +++ b/wsdlgo/testdata/w3cexample1.wsdl @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/wsdlgo/testdata/w3cexample2.golden b/wsdlgo/testdata/w3cexample2.golden new file mode 100644 index 0000000..3bc4db7 --- /dev/null +++ b/wsdlgo/testdata/w3cexample2.golden @@ -0,0 +1,12 @@ +package internal + +import ( + "errors" + + "golang.org/x/net/context" +) + +// SetTerm was auto-generated from WSDL. +func SetTerm(ctx context.Context, term string, value string) (err error) { + return errors.New("not implemented") +} diff --git a/wsdlgo/testdata/w3cexample2.wsdl b/wsdlgo/testdata/w3cexample2.wsdl new file mode 100644 index 0000000..d47f2cb --- /dev/null +++ b/wsdlgo/testdata/w3cexample2.wsdl @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/wsdlgo/testdata/w3example1.golden b/wsdlgo/testdata/w3example1.golden new file mode 100644 index 0000000..56246dd --- /dev/null +++ b/wsdlgo/testdata/w3example1.golden @@ -0,0 +1,28 @@ +package endorsementsearchsoapbinding + +import ( + "errors" + + "golang.org/x/net/context" +) + +// GetEndorsingBoarder was auto-generated from WSDL. +func GetEndorsingBoarder(ctx context.Context, body *GetEndorsingBoarderRequest) (respBody *GetEndorsingBoarderResponse, err error) { + return &GetEndorsingBoarderResponse{}, errors.New("not implemented") +} + +// GetEndorsingBoarderFault was auto-generated from WSDL. +type GetEndorsingBoarderFault struct { + ErrorMessage string `xml:"errorMessage,omitempty"` +} + +// GetEndorsingBoarderRequest was auto-generated from WSDL. +type GetEndorsingBoarderRequest struct { + Manufacturer string `xml:"manufacturer,omitempty"` + Model string `xml:"model,omitempty"` +} + +// GetEndorsingBoarderResponse was auto-generated from WSDL. +type GetEndorsingBoarderResponse struct { + EndorsingBoarder string `xml:"endorsingBoarder,omitempty"` +} diff --git a/wsdlgo/testdata/w3example1.wsdl b/wsdlgo/testdata/w3example1.wsdl new file mode 100644 index 0000000..396f58b --- /dev/null +++ b/wsdlgo/testdata/w3example1.wsdl @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + snowboarding-info.com Endorsement Service + + + + + + + + + + diff --git a/wsdlgo/testdata/w3example2.golden b/wsdlgo/testdata/w3example2.golden new file mode 100644 index 0000000..565b840 --- /dev/null +++ b/wsdlgo/testdata/w3example2.golden @@ -0,0 +1,22 @@ +package stockquotesoapbinding + +import ( + "errors" + + "golang.org/x/net/context" +) + +// GetLastTradePrice was auto-generated from WSDL. +func GetLastTradePrice(ctx context.Context, body *TradePriceRequest) (respBody *TradePriceResponse, err error) { + return &TradePriceResponse{}, errors.New("not implemented") +} + +// TradePriceRequest was auto-generated from WSDL. +type TradePriceRequest struct { + TickerSymbol string `xml:"tickerSymbol,omitempty"` +} + +// TradePriceResponse was auto-generated from WSDL. +type TradePriceResponse struct { + Price float64 `xml:"price,omitempty"` +} diff --git a/wsdlgo/testdata/w3example2.wsdl b/wsdlgo/testdata/w3example2.wsdl new file mode 100644 index 0000000..fbc5ed8 --- /dev/null +++ b/wsdlgo/testdata/w3example2.wsdl @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + My first service + + + + + + + + + +