From 261d4fb2957d21745643d6d5ad8a812667548930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thilo=20Karra=C3=9F?= Date: Wed, 14 Oct 2020 09:03:01 +0200 Subject: [PATCH] Implement all but the funcs themselves --- cmd/generate.go | 63 +++++++++++-- encode/api.go | 16 ++++ encode/array.go | 9 +- encode/encoder.go | 19 +++- encode/func.go | 215 +++++++++++++++++++++++++++++++++++++++++++++ openapi/openapi.go | 172 ++++++++++++++++++++++++++++++++---- 6 files changed, 461 insertions(+), 33 deletions(-) create mode 100644 encode/api.go create mode 100644 encode/func.go diff --git a/cmd/generate.go b/cmd/generate.go index 17a0da4..8610eda 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -36,19 +36,39 @@ func executeGenerate(cmd *cobra.Command, args []string) error { log.WithError(err).Fatal("cannot load input data") } + // path + // - parametres + // - methods + // - parameters + // - body + // - response + + ops := make(map[string/*tag*/][]*openapi.Operation) + log.Infof("API: %v, %v", api.Info.Title, api.Info.Version) log.Info("Paths:") - for k, _ := range api.Paths { - log.Infof(" - %v", k) - for m, n := range api.Paths[k].Operations() { + for k, v := range api.Paths { + name := k + log.Infof(" - %v", name) + for m, n := range v.Operations() { + v.Name = k + tag := "" + if len(n.Tags) > 0 { + tag = n.Tags[0] + } log.Infof( " [%v] %v: %v", m, n.OperationId, n.Summary) + //if len(ops[tag])==0 { + // ops[tag]= make([]*openapi.Operation) + //} + ops[tag] = append(ops[tag], n) } } os.Mkdir(packageName, 0755) - os.Mkdir(path.Join(packageName, "schema"), 0755) - tEnc := encode.NewEncoder(packageName) + //os.Mkdir(path.Join(packageName, "model"), 0755) + tEnc := encode.NewEncoder(packageName, api) + /// schema log.Info("Types:") for k, v := range api.Components.Schemas { log.Infof("File: %v.go", strings.ToLower(k)) @@ -72,11 +92,42 @@ func executeGenerate(cmd *cobra.Command, args []string) error { log.Warnf("no data in %v", k) continue } - err = ioutil.WriteFile(path.Join(packageName, k + ".go"), []byte(tData[:]) ,0644) + err = ioutil.WriteFile(path.Join(packageName, "model_" + k + ".go"), []byte(tData[:]) ,0644) if err != nil { log.WithError(err).Fatal("cannot create file") } + } + log.Info("Parameters:") + for k, v := range api.Components.Parameters { + log.Infof(" - %v (%v) [%v]: %v", k, v.In, v.Schema.GoType(), v.Description) + } + + log.Info("Paths again:") + for tag, v := range ops { + log.Infof("tag: %v", tag) + for method, op := range v { + log.Infof(" - [%v]%v: %v", method, op.OperationId, op.Summary) + log.Infof(" %v", op.Path().Name) + for _, p := range op.Path().Parameters { + log.Infof(" %v", p.Ref) + } + for _, p := range op.Parameters { + log.Infof(" %v: %v", p.In, p.Name) + } + } + tData := tEnc.Funcs(tag, v) + log.Infof("%v.go: %v", tag, tData) + err = ioutil.WriteFile(path.Join(packageName, "api_" + tag + ".go"), []byte(tData[:]) ,0644) + if err != nil { + log.WithError(err).Fatal("cannot create file") + } + } + + log.Info("writing api.go") + err = ioutil.WriteFile(path.Join(packageName, "api.go"), []byte(tEnc.Api(*api.Servers[0])[:]) ,0644) + if err != nil { + log.WithError(err).Fatal("cannot create file") } return nil diff --git a/encode/api.go b/encode/api.go new file mode 100644 index 0000000..2bbcb31 --- /dev/null +++ b/encode/api.go @@ -0,0 +1,16 @@ +package encode // import "udico.de/uditaren/opier/encode" +import ( + "bytes" + "fmt" + "strings" + "udico.de/opier/openapi" +) + +func (e Encoder) Api(server openapi.Server) string { + tBuf := bytes.Buffer{} + tBuf.WriteString(fmt.Sprintf("package %v\n\n", e.Package)) + tBuf.WriteString(e.GeneratedHeader()) + tBuf.WriteString(Comment(server.Description, 0)) + tBuf.WriteString(fmt.Sprintf("const %v_OPENAPI_BASE string = \"%v\"", strings.ToUpper(e.Package), server.Url)) + return tBuf.String() +} diff --git a/encode/array.go b/encode/array.go index fc5d84b..ffef9cb 100644 --- a/encode/array.go +++ b/encode/array.go @@ -2,7 +2,6 @@ package encode // import "udico.de/uditaren/opier/encode" import ( "bytes" "fmt" - "strings" "udico.de/opier/openapi" ) @@ -10,13 +9,7 @@ func (e Encoder) Array(name string, schema openapi.Schema) string { tBuf := bytes.Buffer{} tBuf.WriteString(fmt.Sprintf("package %v\n\n", e.Package)) tBuf.WriteString(e.GeneratedHeader()) - - if schema.Description != "" { - tDesc := strings.Split(strings.TrimSpace(schema.Description), "\n") - for _, tLine := range tDesc { - tBuf.WriteString(fmt.Sprintf("// %v\n", tLine)) - } - } + tBuf.WriteString(Comment(schema.Description, 0)) tBuf.WriteString(fmt.Sprintf("type %v []%v\n", name, schema.Items.GoType())) return tBuf.String() } diff --git a/encode/encoder.go b/encode/encoder.go index 6638dbc..20a2a99 100644 --- a/encode/encoder.go +++ b/encode/encoder.go @@ -1,16 +1,20 @@ package encode // import "udico.de/uditaren/opier/encode" import ( + "fmt" "strings" + "udico.de/opier/openapi" "unicode" ) type Encoder struct { Package string + api *openapi.OpenAPI } -func NewEncoder(pkg string) *Encoder { +func NewEncoder(pkg string, api *openapi.OpenAPI) *Encoder { return &Encoder{ Package: pkg, + api: api, } } @@ -40,4 +44,17 @@ func (e Encoder) GeneratedHeader() string { ***********************************************/ ` +} + +func Comment(aComment string, indent int) string { + tBuf := &strings.Builder{} + tComment := strings.TrimSpace(aComment) + tInd := strings.Repeat(" ", indent) + if tComment != "" { + tCommentLines := strings.Split(tComment, "\n") + for _, tCommentLine := range tCommentLines { + tBuf.WriteString(fmt.Sprintf("%v// %v\n", tInd, tCommentLine)) + } + } + return tBuf.String() } \ No newline at end of file diff --git a/encode/func.go b/encode/func.go new file mode 100644 index 0000000..ae3acd8 --- /dev/null +++ b/encode/func.go @@ -0,0 +1,215 @@ +package encode // import "udico.de/uditaren/opier/encode" +import ( + "bytes" + "fmt" + "sort" + "strconv" + "strings" + "udico.de/opier/openapi" +) + +type simpleparam struct { + Name string + Type string + In string +} + +// implement sort interface on simpleparam +type simpleparamslice []*simpleparam + +func (s simpleparamslice) Len() int { return len(s) } +func (s simpleparamslice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s simpleparamslice) Less(i, j int) bool { + val := func(s string) int { + switch s { + case "path": + return 0 + case "head": + return 1 + case "query": + return 2 + } + return 99 + } + iVal := val(s[i].In) + jVal := val(s[j].In) + iStr := s[i].Name + jStr := s[j].Name + if iVal == jVal { + return strings.Compare(iStr, jStr) < 0 + } + return iVal < jVal +} + +// Funcs generates all functions for a given tag +// +// tag is a +// tag name +func (e Encoder) Funcs(tag string, operations []*openapi.Operation) string { + + tBuf := bytes.Buffer{} + tBuf.WriteString(fmt.Sprintf("package %v\n\n", e.Package)) + tBuf.WriteString(e.GeneratedHeader()) + tBuf.WriteString(e.tagType(tag)) + + tFuncPrefix := "p" + NormalizeName(tag) + + for _, op := range operations { + // name -> type + tParams := make([]*simpleparam, 0, 5) + + if op.Description != "" { + tDesc := strings.Split(strings.TrimSpace(op.Description), "\n") + for _, tLine := range tDesc { + tBuf.WriteString(fmt.Sprintf("// %v\n", tLine)) + } + } + for _, params := range op.Path().Parameters { + tBuf.WriteString("// param [PATH] ") + if params.Ref != "" { + // ref param + tRefParam := e.api.ResolveParameter(params.Ref) + tBuf.WriteString(fmt.Sprintf("REF: %v\n", params.Ref)) + tBuf.WriteString(Comment(tRefParam.Name, 2)) + tParams = append(tParams, &simpleparam{ + Name: tRefParam.Name, + Type: tRefParam.Schema.GoType(), + In: "path", + }) + } else { + tBuf.WriteString(fmt.Sprintf("TYPE: %v\n", params.Schema.Type)) + tParams = append(tParams, &simpleparam{ + Name: params.Name, + Type: params.Schema.GoType(), + In: "path", + }) + } + } + for _, params := range op.Parameters { + //tBuf.WriteString("// param ") + tBuf.WriteString(Comment(fmt.Sprintf("param [%v] %v: %v\n", params.In, params.Name, params.Description), 0)) + tParams = append(tParams, &simpleparam{ + Name: params.Name, + Type: params.Schema.GoType(), + In: string(params.In), + }) + } + if op.RequestBody != nil { + tBuf.WriteString(fmt.Sprintf("// body: %v\n", op.RequestBody.Description)) + for k, v := range op.RequestBody.Content { + tBuf.WriteString(fmt.Sprintf("// [%v]: %v\n", k, v.Schema.Ref)) + } + } + + var tResponseType *openapi.ResponseObject = nil + for tCode, tResponse := range op.Responses { + tBuf.WriteString(Comment(fmt.Sprintf("response [%v]: %v\n", tCode, tResponse.Description), 0)) + tCodeVal, _ := strconv.Atoi(tCode) + if tCodeVal/100 == 2 { + tResponseType = tResponse + } + } + tResponses := make([]*simpleparam, 0, 2) + if tResponseType != nil { + for k, v := range tResponseType.Headers { + // each given responseheader is a return parameter + tResponses = append(tResponses, &simpleparam{ + Name: k, + Type: v.Schema.TypeOrReference(), + In: "head", + }) + } + if len(tResponseType.Content) > 0 { + tSchema := tResponseType.Content["application/json"].Schema + tResponses = append(tResponses, &simpleparam{ + Name: "ret", + Type: tSchema.GoType(), + In: "body", + }) + } + } + + sort.Sort(simpleparamslice(tParams)) + sort.Sort(simpleparamslice(tResponses)) + + // parameter list: + tResponses = append(tResponses, &simpleparam{ + Name: "err", + Type: "error", + }) + tParamBuf := &strings.Builder{} + for i, tParam := range tParams { + if i > 0 { + tParamBuf.WriteString(", ") + } + tParamBuf.WriteString(NormalizeName(tParam.Name)) + tParamBuf.WriteString(" " + tParam.Type) + } + + // body parameter: + if op.RequestBody != nil { + ref := op.RequestBody.Content["application/json"].Schema.Ref + if ref != "" { + if tParamBuf.Len() > 0 { + tParamBuf.WriteString(", ") + } + tParamBuf.WriteString(fmt.Sprintf("data *%v", e.api.ResolveSchema(ref).Name())) + } else { + tParamBuf.WriteString(" /* IMPLEMENT BODY SCHEMA which is not $ref */") + } + } + tParamStr := tParamBuf.String() + + tResponseBuf := &strings.Builder{} + tResponseBuf.WriteString("(") + for i, tResponse := range tResponses { + if i > 0 { + tResponseBuf.WriteString(", ") + } + tResponseBuf.WriteString(tResponse.Name + " ") + tResponseBuf.WriteString(tResponse.Type) + } + tResponseBuf.WriteString(")") + tResponseStr := tResponseBuf.String() + + tBuf.WriteString(fmt.Sprintf("func (p %v) %v(%v) %v {\n", tFuncPrefix, NormalizeName(op.OperationId), tParamStr, tResponseStr)) + + // add header parameters + // insert path parameters + // add query parameters + // post/put params ("body") + tBuf.WriteString(fmt.Sprintf(" // [%v]%v operation here \n", op.Method(), op.Path().Name)) + tBuf.WriteString(fmt.Sprintf(" return\n}\n\n")) + } + + + + return tBuf.String() +} + +func (e Encoder) tagType(tag string) string { + tNorm := NormalizeName(tag) + tBuf := bytes.Buffer{} + tBuf.WriteString("import \"net/http\"\n\n") + tBuf.WriteString(Comment(e.getTagDescription(tag), 0)) + tBuf.WriteString(fmt.Sprintf("type p%v struct {\n", tNorm)) + tBuf.WriteString(" client *http.Client\n") + tBuf.WriteString(" base string\n") + tBuf.WriteString("}\n\n") + tBuf.WriteString(fmt.Sprintf("func %v(aClient *http.Client, aBase string) p%v {\n", NormalizeName("new-"+tag), tNorm)) + tBuf.WriteString(fmt.Sprintf(" return p%v{\n", tNorm)) + tBuf.WriteString(" client: aClient,\n") + tBuf.WriteString(" base: aBase,\n") + tBuf.WriteString(" }\n") + tBuf.WriteString("}\n\n") + return tBuf.String() +} + +func (e Encoder) getTagDescription(tag string) string { + for _, v := range e.api.Tags { + if v.Name == tag { + return v.Description + } + } + return "" +} diff --git a/openapi/openapi.go b/openapi/openapi.go index cda20fc..af64601 100644 --- a/openapi/openapi.go +++ b/openapi/openapi.go @@ -14,9 +14,11 @@ type OpenAPI struct { Version string Description string } + Tags []*TagObject `yaml:"tags"` Paths map[string]*PathItem `yaml:"paths"` Components *Components + Servers []*Server `yaml:"servers"` } func Load(fname string) (*OpenAPI, error) { @@ -30,6 +32,45 @@ func Load(fname string) (*OpenAPI, error) { return tRet, err } +func (o OpenAPI) ResolveParameter(name string) *Parameter { + if !strings.HasPrefix(name, "#/components/parameters/") { + return nil + } + tName := strings.TrimPrefix(name, "#/components/parameters/") + for pName, param := range o.Components.Parameters { + if tName == pName { + tParam := param + return &tParam + } + } + return nil +} + +func (o OpenAPI) ResolveSchema(name string) *Schema { + if !strings.HasPrefix(name, "#/components/schemas/") { + return nil + } + tName := strings.TrimPrefix(name, "#/components/schemas/") + for pSch, tSchema := range o.Components.Schemas { + if tName == pSch { + tSchemaP := tSchema + tSchemaP.name = tName + return &tSchemaP + } + } + return nil +} + +type Server struct { + Url string `yaml:"url"` + Description string `yaml:"description"` +} + +type TagObject struct { + Name string `yaml:"name"` + Description string `yaml:"description"` +} + // PathItem Describes the operations available on a single path. A Path Item MAY be empty, due to ACL constraints. // The path itself is still exposed to the documentation viewer but they will not know which operations and parameters // are available. @@ -40,6 +81,8 @@ type PathItem struct { // the behavior is undefined. Ref string `yaml:"$ref"` + Parameters []*Parameter + // An optional, string summary, intended to apply to all operations in this path. Summary string @@ -55,32 +98,52 @@ type PathItem struct { Head *Operation Patch *Operation Trace *Operation + + // for internal bookkeeping + Name string } -func (p PathItem) Operations() map[string]*Operation { +// Never feed the mighty spaghetti (code) monster :( +func (p *PathItem) Operations() map[string]*Operation { tRet := make(map[string]*Operation) if p.Get != nil { + p.Get.path = p + p.Get.method = "get" tRet["get"] = p.Get } if p.Put != nil { + p.Put.path = p + p.Put.method = "put" tRet["put"] = p.Put } if p.Post != nil { + p.Post.path = p + p.Post.method = "post" tRet["post"] = p.Post } if p.Delete != nil { + p.Delete.path = p + p.Delete.method = "delete" tRet["delete"] = p.Delete } if p.Options != nil { + p.Options.path = p + p.Options.method = "options" tRet["options"] = p.Options } if p.Head != nil { + p.Head.path = p + p.Head.method = "head" tRet["head"] = p.Head } if p.Patch != nil { + p.Patch.path = p + p.Patch.method = "patch" tRet["patch"] = p.Patch } if p.Trace != nil { + p.Trace.path = p + p.Trace.method = "trace" tRet["trace"] = p.Trace } return tRet @@ -107,9 +170,15 @@ type Operation struct { OperationId string `yaml:"operationId"` //Parameters [] ... - // requestBodyesObject // required - // callba - // Responses Responscks + Parameters []*Parameter + + RequestBody *RequestBodyObject `yaml:"requestBody"` + + // callbacks + + // Responses Respons + Responses map[string]*ResponseObject + Deprecated bool // A declaration of which security mechanisms can be used for this operation. @@ -122,10 +191,59 @@ type Operation struct { // An alternative server array to service this operation. If an alternative server object is specified at the Path // Item Object or Root level, it will be overridden by this value. // Servers + + // we need a backref to the Path for building nice maps + path *PathItem + method string } -type ResponsesObject struct { - // ... +func (o Operation) Path() *PathItem { + return o.path +} + +func (o Operation) Method() string { + return o.method +} + +type RequestBodyObject struct { + Description string `yaml:"description"` + Content map[string]*MediaTypeObject `yaml:"content"` + Required bool `yaml:"required"` +} + +type ResponseObject struct { + Ref string `yaml:"$ref"` + Description string `yaml:"description"` + Headers map[string]*Parameter `yaml:"headers"` + Content map[string]*MediaTypeObject `yaml:"content"` + Links map[string]*LinkObject `yaml:"links"` +} + +func (r ResponseObject) String() string { + ret := "" + for _, v := range r.Content { + if v.Schema != nil { + if v.Schema.Ref != "" { + ret = v.Schema.Ref + } else { + ret = "IMPLEMENT ME!!!" + } + break // the for loop + } + } + return ret +} + +// The Header Object follows the structure of the Parameter Object with the following changes: +// * name MUST NOT be specified, it is given in the corresponding headers map. +// * in MUST NOT be specified, it is implicitly in header. +// * All traits that are affected by the location MUST be applicable to a location of header (for example, style). +type HeaderObject struct { + Parameter +} + +type LinkObject struct { + // nope, we just don't want to have these … } type Components struct { @@ -158,15 +276,15 @@ type Parameter struct { ///--- either - Style string - Explode bool + Style string + Explode bool AllowReserved bool `yaml:"allowReserved"` - Schema *Schema + Schema *Schema // example // examples ///--- or - Content map[string]*MediaType + Content map[string]*MediaTypeObject } type Schema struct { @@ -179,16 +297,23 @@ type Schema struct { // when type==object Properties map[string]*Schema - Format string + Format string Minimum int Maximum int Default interface{} - Enum []interface{} + Enum []interface{} XEnumVarnames []string `yaml:"x-enum-varnames"` - Required []string + Required []string Description string + + // Only filled in by ResolveSchema(...) + name string +} + +func (s Schema) Name() string { + return s.name } func (s Schema) TypeOrReference() string { @@ -211,6 +336,10 @@ func (s Schema) GoType() string { tRet = "bool" case "array": tRet = fmt.Sprintf("[]%v", s.Items.GoType()) + case "": // arrays may not be tagged as such. deduce from the items element: + if s.Items != nil { + tRet = fmt.Sprintf("[]%v", s.Items.GoType()) + } } } return tRet @@ -235,11 +364,6 @@ func (s Schema) EnumNames() ([]string, int) { } type Referencable interface { - -} - -type MediaType struct { - } func NormalizeName(aName string) string { @@ -259,3 +383,15 @@ func NormalizeName(aName string) string { } return tRet.String() } + +type MediaTypeObject struct { + // The schema defining the content of the request, response, or parameter. + Schema *Schema `yaml:"schema"` + Example string `yaml:"example"` + //examples + //encoding +} + +type SchemaObject struct { + Ref string `yaml:"$ref"` +}