692147512fb0 — Sean Russell 11 years ago
GET is implemented. Types are fleshed out.
7 files changed, 428 insertions(+), 6 deletions(-)

M api.go
A => cmd/sashay/main.go
A => generator.go
M resource_listing.go
M swagger_test.go
A => test/main.go
M util.go
M api.go +71 -4
@@ 66,14 66,14 @@ type Parameter struct {
 }
 
 type ResponseMessage struct {
-	code          int
-	message       string
-	responseModel string
+	Code          int
+	Message       string
+	ResponseModel string
 }
 
 type DataTypeFields struct {
 	Type         string
-	Ref          string
+	Ref          string     `json:"$ref"`
 	Format       string
 	DefaultValue interface{}
 	Enum         []string

          
@@ 82,3 82,70 @@ type DataTypeFields struct {
 	Items        map[string]string
 	UniqueItems  bool
 }
+
+func (p DataTypeFields) typeOf() string {
+	switch p.Type {
+	case "File":
+		return "io.Reader"
+	case "integer":
+		if p.Format == "int64" {
+			return p.Format
+		} else {
+			return "int32"
+		}
+	case "number":
+		if p.Format == "double" {
+			return "float64"
+		} else {
+			return "float32"
+		}
+	case "string":
+		switch p.Format {
+		default:
+			return "string"
+		case "byte":
+			return "[]byte"
+		case "date":
+			return "date.Time"
+		case "date-time":
+			return "date.Time"
+		}
+	case "boolean":
+	case "array":
+		if p.Items["type"] != "" {
+			return "[]"+p.Items["type"]
+		} else {
+			return "[]"+p.Items["$ref"]
+		}
+	default:
+		if p.Ref != "" {
+			return p.Ref
+		}
+	}
+	return p.Type
+}
+
+func (p DataTypeFields) zero() string {
+	switch p.Type {
+	case "integer": return "0"
+	case "number": return "0.0"
+	case "string":
+		switch p.Format {
+		default: return `""`
+		case "byte": return "[]byte{}"
+		case "date", "date-time": return "time.Time{}"
+		}
+	case "boolean": return "false"
+	case "array":
+		if p.Items["type"] != "" {
+			return "[]"+p.Items["type"]+"{}"
+		} else {
+			return "[]"+p.Items["$ref"]+"{}"
+		}
+	default:
+		if p.Ref != "" {
+			return p.Ref+"{}"
+		}
+		return p.Type+"{}"
+	}
+}

          
A => cmd/sashay/main.go +22 -0
@@ 0,0 1,22 @@ 
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"bitbucket.org/seanerussell/sashay"
+)
+
+func main() {
+	b, _ := ioutil.ReadFile("test/api-docs_pet.json")
+	c := make(chan string)
+	wait := make(chan bool)
+	go func() {
+		for i := range c {
+			fmt.Println(i)
+		}
+		close(wait)
+	}()
+	sashay.Generate("test", b, c)
+	close(c)
+	<-wait
+}

          
A => generator.go +244 -0
@@ 0,0 1,244 @@ 
+package sashay
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+)
+
+var swaggerToGo map[string]string
+
+func Generate(pack string, j []byte, out chan string) error {
+	t, e := Parse(j)
+	if e != nil {
+		return e
+	}
+	out <- fmt.Sprintf("package %s", pack)
+	out <- ""
+	out <- "import ("
+	out <- "\t\"encoding/json\""
+	out <- "\t\"errors\""
+	out <- "\t\"fmt\""
+	out <- "\t\"io/ioutil\""
+	out <- "\t\"io\""
+	out <- "\t\"net/http\""
+	out <- ")"
+	out <- ""
+	switch t.(type) {
+	case ApiDecl: generateApi(t.(ApiDecl), out)
+	case ResourceListing: generateResourceListing(t.(ResourceListing), out)
+	}
+	return nil
+}
+
+func generateApi(a ApiDecl, out chan string) {
+	out <- fmt.Sprintf(`const basePath = "%s"`, a.BasePath)
+	out <- ""
+	generateModels(a, out)
+	np := len(a.Apis)-1
+	for i, api := range a.Apis {
+		no := len(api.Operations)-1
+		for j, op := range api.Operations {
+			generateDocs(op, out)
+			generateFunc(api, op, out)
+			if j < no {
+				out <- ""
+			}
+		}
+		if i < np {
+			out <- ""
+		}
+	}
+}
+
+func generateModels(a ApiDecl, out chan string) {
+	for _, m := range a.Models {
+		out <- fmt.Sprintf("type %s struct {", upcase(m.Id))
+		for pk, pv := range m.Properties {
+			if pv.Description != "" {
+				out <- fmt.Sprintf("\t// %s", pv.Description)
+			}
+			out <- fmt.Sprintf("\t%s %s", upcase(pk), pv.typeOf())
+		}
+		out <- "}"
+		out <- ""
+	}
+}
+
+func generateDocs(op Operation, out chan string) {
+	out <- "/*"
+	out <- op.Summary
+	out <- ""
+	if op.Notes != "" {
+		out <- op.Notes
+		out <- ""
+	}
+	out <- "Parameters:"
+	for _, p := range op.Parameters {
+		out <- fmt.Sprintf("\t%s: %s", p.Name, p.Description)
+	}
+	out <- "*/"
+}
+
+func generateFunc(api Api, op Operation, out chan string) {
+	fname := upcase(op.Nickname)
+	s := fmt.Sprintf("func %s(%s) (%serror) {", fname, genArgs(op), genRv(op))
+	out <- s
+	switch op.Method {
+	case "DELETE":
+		// FIXME
+		//genDelete(api.Path, op.Nickname, )
+		out <- "\treturn nil"
+	case "PATCH":
+		// FIXME
+		out <- "\treturn nil, nil"
+	case "POST":
+		out <- "\treturn nil"
+	case "PUT":
+		out <- "\treturn nil"
+	case "GET":
+		genGet(api, op, out)
+	}
+	out <- "}"
+}
+
+func genGet(api Api, op Operation, out chan string) {
+	out <- fmt.Sprintf("\trv := %s", op.zero())
+	format := fmt.Sprintf(`"%%s%s`, api.Path)
+	formatArgs := []string{"basePath"}
+	params := make(map[string]string)
+	for _, p := range op.Parameters {
+		if p.Minimum != "" {
+			out <- fmt.Sprintf("\tif %s < %s(%s) {", p.Name, p.typeOf(), p.Minimum)
+			out <- "\t\treturn rv, errors.New(fmt.Sprintf(\"invalid value (%d < "+p.Minimum+")\", "+p.Name+"))"
+			out <- "\t}"
+		}
+		if p.Maximum != "" {
+			out <- fmt.Sprintf("\tif %s > %s(%s) {", p.Name, p.typeOf(), p.Maximum)
+			out <- "\t\treturn rv, errors.New(fmt.Sprintf(\"invalid value (%d > "+p.Maximum+")\", "+p.Name+"))"
+			out <- "\t}"
+		}
+		match := fmt.Sprintf("{%s}", p.Name)
+		if strings.Contains(format, match) {
+			format = strings.Replace(format, match, "%v", 1)
+			formatArgs = append(formatArgs, p.Name)
+		} else {
+			v := p.Name
+			if p.Type == "array" {
+				v = genArrayToString(p)
+			}
+			params[p.Name] = v
+		}
+	}
+	if len(params) > 0 {
+		format += "?"
+		l := len(params)-1
+		c := 0
+		for n, v := range params {
+			format = format + n + "=%s"
+			formatArgs = append(formatArgs, v)
+			if c < l {
+				format += "&"
+			}
+		}
+	}
+	format += `"`
+	url := "\turl := fmt.Sprintf("+format
+	for _, v := range formatArgs {
+		url += ", "+v
+	}
+	url += ")"
+	out <- url
+	out <- "\tresp, err := http.Get(url)"
+	out <- "\tif err != nil {"
+	out <- fmt.Sprintf("\t\treturn %s, err", op.zero())
+	out <- "\t}"
+	for _, c := range op.ResponseMessages {
+		out <- fmt.Sprintf("\tif resp.StatusCode == %d {", c.Code)
+		out <- fmt.Sprintf("\t\treturn rv, errors.New(\"%s\")", c.Message)
+		out <- "\t}"
+	}
+	genDeserialize(op, out)
+}
+
+func genArrayToString(p Parameter) string {
+	rv := bytes.Buffer{}
+	xs := fmt.Sprintf("%ss", p.Name)
+	rv.WriteString(fmt.Sprintf("\t%s := []string{}", xs))
+	rv.WriteString(fmt.Sprintf("\tfor _, v := range %s {", p.Name))
+	rv.WriteString(fmt.Sprintf("\t\t%s = append(%s, toString(v))", xs, xs))
+	rv.WriteString("\t}")
+	rv.WriteString(fmt.Sprintf("\t%sString := strings.Join(%s, \"%2C\")", p.Name, xs))
+	return string(rv.Bytes())
+}
+
+func genDeserialize(op Operation, out chan string) {
+	out <- "\tdefer resp.Body.Close()"
+	out <- "\ts, e := ioutil.ReadAll(resp.Body)"
+	out <- "\tif e != nil {"
+	out <- fmt.Sprintf("\t\treturn %s, e", op.zero())
+	out <- "\t}"
+	switch op.typeOf() {
+	case "string":
+		out <- "\treturn s, nil"
+	case "int":
+		out <- "\treturn strconv.Atoi(s)"
+	case "int64":
+		out <- "\tk, e := strconv.ParseInt(s, 10, 64)"
+		out <- "\tif e != nil {"
+		out <- fmt.Sprintf("\t\treturn %s, e", op.zero())
+		out <- "\t}"
+		out <- "\treturn k, nil"
+	default:
+		out <- "\te = json.Unmarshal(s, &rv)"
+		out <- "\tif e != nil {"
+		out <- fmt.Sprintf("\t\treturn %s, e", op.zero())
+		out <- "\t}"
+		out <- "\treturn rv, nil"
+	}
+}
+
+func genArgs(op Operation) string {
+	b := bytes.Buffer{}
+	np := len(op.Parameters)-1
+	for i, p := range op.Parameters {
+		b.WriteString(p.Name)
+		b.WriteString(" ")
+		b.WriteString(p.typeOf())
+		if i < np {
+			b.WriteString(", ")
+		}
+	}
+	return string(b.Bytes())
+}
+
+func genRv(op Operation) string {
+	switch op.Type {
+	case "void": return ""
+	case "array":
+		if op.Items["$ref"] != "" {
+			return "[]"+op.Items["$ref"]+", "
+		} else if op.Items["type"] != "" {
+			return "[]"+op.Items["type"]+", "
+		} else {
+			panic("Implement me.")
+		}
+	}
+	return op.Type+", "
+}
+
+
+func generateResourceListing(r ResourceListing, out chan string) {
+}
+
+func upcase(s string) string {
+	if len(s) == 0 {
+		return s
+	}
+	if len(s) == 1 {
+		return strings.ToUpper(s)
+	}
+	head := s[0:1]
+	tail := s[1:len(s)]
+	return strings.ToUpper(head)+tail
+}

          
M resource_listing.go +1 -0
@@ 23,6 23,7 @@ type Authorization struct {
 	Scopes     []Scope
 	GrantTypes GrantTypes
 }
+
 type Resource struct {
 	Path        string
 	Description string

          
M swagger_test.go +18 -2
@@ 1,5 1,4 @@ 
-package sashay
-
+package sashay 
 import (
 	"testing"
 	"io/ioutil"

          
@@ 53,6 52,23 @@ func (s *MySuite) TestParseApi(c *C) {
 	c.Assert(k.Apis[0].Operations[1].Produces[0], Equals, "application/json")
 }
 
+func (s *MySuite) TestType(c *C) {
+	p, e := GetType(apiDocs)
+	c.Assert(e, IsNil)
+	c.Assert(p, Equals, API)
+	q, e := GetType(resourceListing)
+	c.Assert(e, IsNil)
+	c.Assert(q, Equals, RESOURCELIST)
+}
+
+func (s *MySuite) TestParse(c *C) {
+	r, e := Parse(apiDocs)
+	c.Assert(e, IsNil)
+	c.Assert(r, FitsTypeOf, ApiDecl{})
+	t, e := Parse(resourceListing)
+	c.Assert(e, IsNil)
+	c.Assert(t, FitsTypeOf, ResourceListing{})
+}
 
 func (s *MySuite) TestGen(c *C) {
 }

          
A => test/main.go +24 -0
@@ 0,0 1,24 @@ 
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"flag"
+	"bitbucket.org/seanerussell/sashay"
+	"os"
+)
+
+func main() {
+	flag.Parse()
+	if flag.NArg() != 1 {
+		fmt.Println("USAGE: %s <json>", os.Args[0])
+		return
+	}
+	b, e := ioutil.ReadFile(flag.Args()[0])
+	if e != nil {
+		fmt.Printf("error reading %s: %s", flag.Args()[0], e.Error())
+		os.Exit(1)
+	}
+	v, e := sashay.Parse(b)
+	fmt.Printf("%+v\n", v)
+}

          
M util.go +48 -0
@@ 5,8 5,56 @@ package sashay
 
 import (
 	"encoding/json"
+	"errors"
 )
 
+const (
+	UNDEFINED = -1
+	RESOURCELIST = iota
+	API
+)
+
+func Parse(d []byte) (interface{}, error) {
+	switch t, e := GetType(d); {
+		case e != nil: return nil, e
+		case t == API: return ParseApi(d)
+		case t == RESOURCELIST: return ParseResource(d)
+	}
+	return nil, errors.New("unrecognized json format")
+}
+
+func GetType(d []byte) (int, error) {
+	t, e := getStruct(d)
+	if e != nil {
+		return UNDEFINED, e
+	}
+	switch t.(type) {
+	case ApiDecl: return API, nil
+	case ResourceListing: return RESOURCELIST, nil
+	}
+	return UNDEFINED, errors.New("json document not recognized")
+}
+
+func getStruct(d []byte) (interface{}, error) {
+	var listing ResourceListing
+	e := json.Unmarshal(d, &listing)
+	if e != nil {
+		return nil, e
+	}
+	if listing.Info != (Info{}) {
+		return listing, nil
+	}
+	var api ApiDecl
+	e = json.Unmarshal(d, &api)
+	if e != nil {
+		return nil, e
+	}
+	if api.ResourcePath != "" {
+		return api, nil
+	}
+	return nil, errors.New("json document not recognized")
+}
+
 func ParseResource(j []byte) (ResourceListing, error) {
 	var r ResourceListing
 	err := json.Unmarshal(j, &r)