From 852369c450e77318b503a603445992ca8f18e02d Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 19 Apr 2022 15:11:57 -0300 Subject: [PATCH 01/31] Added paths functionality to compose encoding for openAPI --- encoding/openapi/openapi.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go index 6eaebb8cae0..527f317575f 100644 --- a/encoding/openapi/openapi.go +++ b/encoding/openapi/openapi.go @@ -92,7 +92,12 @@ func Generate(inst *cue.Instance, c *Config) (*ast.File, error) { if err != nil { return nil, err } - top, err := c.compose(inst, all) + paths, err := paths(c, inst) + if err != nil { + return nil, err + } + + top, err := c.compose(inst, all, paths) if err != nil { return nil, err } @@ -108,7 +113,12 @@ func (g *Generator) All(inst *cue.Instance) (*OrderedMap, error) { if err != nil { return nil, err } - top, err := g.compose(inst, all) + paths, err := paths(g, inst) + if err != nil { + return nil, err + } + + top, err := g.compose(inst, all, paths) return (*OrderedMap)(top), err } @@ -125,7 +135,7 @@ func toCUE(name string, x interface{}) (v ast.Expr, err error) { } -func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.StructLit, err error) { +func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit, paths *ast.StructLit) (x *ast.StructLit, err error) { var errs errors.Error @@ -133,7 +143,7 @@ func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.Str var info *ast.StructLit for i, _ := inst.Value().Fields(cue.Definitions(true)); i.Next(); { - if i.IsDefinition() { + if i.IsDefinition() || strings.HasPrefix(i.Label(), "$") { continue } label := i.Label() @@ -210,7 +220,7 @@ func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.Str return ast.NewStruct( "openapi", ast.NewString(c.Version), "info", info, - "paths", ast.NewStruct(), + "paths", paths, "components", ast.NewStruct("schemas", schemas), ), errs } From 7da9a340777b3145923896da4ebabc45cfd9a4a3 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 19 Apr 2022 15:14:27 -0300 Subject: [PATCH 02/31] Added main functions to build paths struct --- encoding/openapi/build.go | 105 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index f22a921ba1a..4e02ffa9100 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -46,6 +46,8 @@ type buildContext struct { fieldFilter *regexp.Regexp evalDepth int // detect cycles when resolving references + paths *OrderedMap + schemas *OrderedMap // Track external schemas. @@ -68,9 +70,89 @@ type externalType struct { } type oaSchema = OrderedMap +type pathOperations = OrderedMap type typeFunc func(b *builder, a cue.Value) +func paths(g *Generator, inst *cue.Instance) (paths *ast.StructLit, err error) { + var fieldFilter *regexp.Regexp + if g.FieldFilter != "" { + fieldFilter, err = regexp.Compile(g.FieldFilter) + if err != nil { + return nil, errors.Newf(token.NoPos, "invalid field filter: %v", err) + } + + // verify that certain elements are still passed. + for _, f := range strings.Split( + "version,title,allOf,anyOf,not,enum,Schema/properties,Schema/items"+ + "nullable,type", ",") { + if fieldFilter.MatchString(f) { + return nil, errors.Newf(token.NoPos, "field filter may not exclude %q", f) + } + } + } + + c := buildContext{ + inst: inst, + instExt: inst, + refPrefix: "components/schemas", + expandRefs: g.ExpandReferences, + structural: g.ExpandReferences, + nameFunc: g.ReferenceFunc, + descFunc: g.DescriptionFunc, + paths: &OrderedMap{}, + schemas: &OrderedMap{}, + externalRefs: map[string]*externalType{}, + fieldFilter: fieldFilter, + } + + switch g.Version { + case "3.0.0": + c.exclusiveBool = true + case "3.1.0": + default: + return nil, errors.Newf(token.NoPos, "unsupported version %s", g.Version) + } + + defer func() { + switch x := recover().(type) { + case nil: + case *openapiError: + err = x + default: + panic(x) + } + }() + + i, err := inst.Value().Fields(cue.Definitions(true)) + if err != nil { + return nil, err + } + for i.Next() { + label := i.Label() + + if i.IsDefinition() || !strings.HasPrefix(label, "$") || c.isInternal(label) { + continue + } + + label = label[1:] + ref := c.makeRef(inst, []string{label}) + if ref == "" { + continue + } + c.paths.Set(ref, c.buildPath(i.Value())) + } + + a := c.paths.Elts + sort.Slice(a, func(i, j int) bool { + x, _, _ := ast.LabelName(a[i].(*ast.Field).Label) + y, _, _ := ast.LabelName(a[j].(*ast.Field).Label) + return x < y + }) + + return (*ast.StructLit)(c.paths), c.errs +} + func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err error) { var fieldFilter *regexp.Regexp if g.FieldFilter != "" { @@ -101,6 +183,7 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro structural: g.ExpandReferences, nameFunc: g.ReferenceFunc, descFunc: g.DescriptionFunc, + paths: &OrderedMap{}, schemas: &OrderedMap{}, externalRefs: map[string]*externalType{}, fieldFilter: fieldFilter, @@ -146,6 +229,7 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro if ref == "" { continue } + println(ref, label) c.schemas.Set(ref, c.build(label, i.Value())) } @@ -180,6 +264,10 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro return (*ast.StructLit)(c.schemas), c.errs } +func (c *buildContext) buildPath(v cue.Value) *ast.StructLit { + return newRootPathBuilder(c).path(v) +} + func (c *buildContext) build(name string, v cue.Value) *ast.StructLit { return newCoreBuilder(c).schema(nil, name, v) } @@ -212,6 +300,13 @@ func (b *builder) checkArgs(a []cue.Value, n int) { } } +func (pb *PathBuilder) path(v cue.Value) *ast.StructLit { + str, _ := v.Lookup("description").String() + desc_str := ast.NewString(str) + + return ast.NewStruct("description", desc_str) +} + func (b *builder) schema(core *builder, name string, v cue.Value) *ast.StructLit { oldPath := b.ctx.path b.ctx.path = append(b.ctx.path, name) @@ -1093,6 +1188,12 @@ func (b *builder) bytes(v cue.Value) { } } +type PathBuilder struct { + ctx *buildContext + description string + operations *pathOperations +} + type builder struct { ctx *buildContext typ string @@ -1112,6 +1213,10 @@ type builder struct { items *builder } +func newRootPathBuilder(c *buildContext) *PathBuilder { + return &PathBuilder{ctx: c} +} + func newRootBuilder(c *buildContext) *builder { return &builder{ctx: c} } From 496044e7feb1231634276be9928d212b30d36858 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 19 Apr 2022 16:47:32 -0300 Subject: [PATCH 03/31] Updated description creation in path --- encoding/openapi/build.go | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index 4e02ffa9100..a5a18b33bfb 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -300,11 +300,21 @@ func (b *builder) checkArgs(a []cue.Value, n int) { } } +func (pb *PathBuilder) pathDescription(v cue.Value) { + description, err := v.String() + if err != nil { + description = "" + } + pb.description = ast.NewString(description) +} + func (pb *PathBuilder) path(v cue.Value) *ast.StructLit { - str, _ := v.Lookup("description").String() - desc_str := ast.NewString(str) - return ast.NewStruct("description", desc_str) + pb.pathDescription(v.Lookup("description")) + pb.securityList(v.Lookup("security")) + + return ast.NewStruct("description", pb.description, + "security", pb.security) } func (b *builder) schema(core *builder, name string, v cue.Value) *ast.StructLit { @@ -994,6 +1004,17 @@ func (b *builder) array(v cue.Value) { } } +func (pb *PathBuilder) securityList(v cue.Value) { + items := []ast.Expr{} + + for i, _ := v.List(); i.Next(); { + items = append(items, pb.decode(i.Value())) + } + + pb.security = ast.NewList(items...) + +} + func (b *builder) listCap(v cue.Value) { switch op, a := v.Expr(); op { case cue.LessThanOp: @@ -1190,8 +1211,9 @@ func (b *builder) bytes(v cue.Value) { type PathBuilder struct { ctx *buildContext - description string + description *ast.BasicLit operations *pathOperations + security *ast.ListLit } type builder struct { @@ -1422,6 +1444,11 @@ func (b *builder) decode(v cue.Value) ast.Expr { return v.Syntax(cue.Final()).(ast.Expr) } +func (pb *PathBuilder) decode(v cue.Value) ast.Expr { + v, _ = v.Default() + return v.Syntax(cue.Final()).(ast.Expr) +} + func (b *builder) big(v cue.Value) ast.Expr { v, _ = v.Default() return v.Syntax(cue.Final()).(ast.Expr) From f5d51633a143c4ad5f2b53031dcc485ad7b81628 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Thu, 21 Apr 2022 17:59:35 -0300 Subject: [PATCH 04/31] Add parsing functionalities to path's operations objects --- encoding/openapi/build.go | 79 +++++++++++++++++++++++++++----- encoding/openapi/pathResponse.go | 72 +++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 encoding/openapi/pathResponse.go diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index a5a18b33bfb..f21a4e6b868 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -183,7 +183,6 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro structural: g.ExpandReferences, nameFunc: g.ReferenceFunc, descFunc: g.DescriptionFunc, - paths: &OrderedMap{}, schemas: &OrderedMap{}, externalRefs: map[string]*externalType{}, fieldFilter: fieldFilter, @@ -265,7 +264,7 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro } func (c *buildContext) buildPath(v cue.Value) *ast.StructLit { - return newRootPathBuilder(c).path(v) + return newRootPathBuilder(c).buildPath(v) } func (c *buildContext) build(name string, v cue.Value) *ast.StructLit { @@ -305,16 +304,71 @@ func (pb *PathBuilder) pathDescription(v cue.Value) { if err != nil { description = "" } - pb.description = ast.NewString(description) + pb.path.Set("description", ast.NewString(description)) +} + +func (pb *PathBuilder) operation(v cue.Value) { + operations := &OrderedMap{} + for i, _ := v.Value().Fields(cue.Definitions(false)); i.Next(); { + // searching http status + label, err := strconv.Atoi(i.Label()) + if err != nil { + continue + } + + // add an error for wrong http status? + if label > 599 || label < 100 { + continue + } + + responseStruct := Response(i.Value(), pb.ctx) + operations.Set(strconv.Itoa(label), responseStruct) + + } + + label, _ := v.Label() + pb.path.Set(label, operations) } -func (pb *PathBuilder) path(v cue.Value) *ast.StructLit { +func isPathLabel(s string) bool { + pathLabels := map[string]bool{"description": true, + "security": true, + "get": true, + "put": true, + "post": true, + "delete": true, + "options": true, + "head": true, + "patch": true, + "trace": true} + _, ok := pathLabels[s] + return ok +} + +func (pb *PathBuilder) buildPath(v cue.Value) *ast.StructLit { + + for i, _ := v.Value().Fields(cue.Definitions(true)); i.Next(); { + label := i.Label() + + if !isPathLabel(label) { + continue + } - pb.pathDescription(v.Lookup("description")) - pb.securityList(v.Lookup("security")) + switch label { + case "description": + pb.pathDescription(v.Lookup("description")) + case "security": + pb.securityList(v.Lookup("security")) + case "get", "put", "post", "delete", "options", "head", "patch", "trace": + pb.operation(v.Lookup(label)) - return ast.NewStruct("description", pb.description, - "security", pb.security) + } + + } + + //return ast.NewStruct("description", pb.description, + // "security", pb.security) + return (*ast.StructLit)(pb.path) } func (b *builder) schema(core *builder, name string, v cue.Value) *ast.StructLit { @@ -1011,8 +1065,8 @@ func (pb *PathBuilder) securityList(v cue.Value) { items = append(items, pb.decode(i.Value())) } - pb.security = ast.NewList(items...) - + //pb.security = ast.NewList(items...) + pb.path.Set("security", ast.NewList(items...)) } func (b *builder) listCap(v cue.Value) { @@ -1214,6 +1268,8 @@ type PathBuilder struct { description *ast.BasicLit operations *pathOperations security *ast.ListLit + + path *OrderedMap } type builder struct { @@ -1236,7 +1292,8 @@ type builder struct { } func newRootPathBuilder(c *buildContext) *PathBuilder { - return &PathBuilder{ctx: c} + return &PathBuilder{ctx: c, + path: &OrderedMap{}} } func newRootBuilder(c *buildContext) *builder { diff --git a/encoding/openapi/pathResponse.go b/encoding/openapi/pathResponse.go new file mode 100644 index 00000000000..eb4653a8a1c --- /dev/null +++ b/encoding/openapi/pathResponse.go @@ -0,0 +1,72 @@ +package openapi + +import ( + "cuelang.org/go/cue" + "cuelang.org/go/cue/ast" +) + +type ResponseObjectBuilder struct { + ctx *buildContext + description string + mediaTypes *OrderedMap +} + +func newResponseBuilder(c *buildContext) *ResponseObjectBuilder { + return &ResponseObjectBuilder{ctx: c, mediaTypes: &OrderedMap{}} +} + +func isMediaType(s string) bool { + mediaTypes := map[string]bool{"application/json": true, + "application/xml": true, + "application/x-www-form-urlencoded": true, + "multipart/form-data": true, + "text/plain": true, + "text/html": true, + "application/pdf": true, + "image/png": true} + + _, ok := mediaTypes[s] + return ok +} + +func (rb *ResponseObjectBuilder) mediaType(v cue.Value) { + schema := &OrderedMap{} + schemaStruct := rb.ctx.build("schema", v.Lookup("schema")) + + schema.Set("schema", schemaStruct) + + label, _ := v.Label() + rb.mediaTypes.Set(label, schema) +} + +func (rb *ResponseObjectBuilder) buildResponse(v cue.Value) *ast.StructLit { + + response := &OrderedMap{} + + description, err := v.Lookup("description").String() + if err != nil { + description = "" + } + rb.description = description + + contentStruct := v.Lookup("content") + for i, _ := contentStruct.Value().Fields(cue.Definitions(false)); i.Next(); { + label := i.Label() + if !isMediaType(label) { + continue + } + rb.mediaType(i.Value()) + + } + + //rb.mediaTypes.Set("description", description) + + response.Set("description", description) + response.Set("content", rb.mediaTypes) + + return (*ast.StructLit)(response) +} + +func Response(v cue.Value, c *buildContext) *ast.StructLit { + return newResponseBuilder(c).buildResponse(v) +} From dbb3bcab1d3fb503fe76dff0e9af000dff0d24ab Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 26 Apr 2022 15:23:33 -0300 Subject: [PATCH 05/31] Add error handling for PathBuilder struct Update pathBuilder struct --- encoding/openapi/build.go | 38 ++++++++++---------------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index f21a4e6b868..383340d78df 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -228,7 +228,6 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro if ref == "" { continue } - println(ref, label) c.schemas.Set(ref, c.build(label, i.Value())) } @@ -316,9 +315,8 @@ func (pb *PathBuilder) operation(v cue.Value) { continue } - // add an error for wrong http status? if label > 599 || label < 100 { - continue + pb.failf(v, "wrong HTTP Status code %v", label) } responseStruct := Response(i.Value(), pb.ctx) @@ -330,19 +328,12 @@ func (pb *PathBuilder) operation(v cue.Value) { pb.path.Set(label, operations) } -func isPathLabel(s string) bool { - pathLabels := map[string]bool{"description": true, - "security": true, - "get": true, - "put": true, - "post": true, - "delete": true, - "options": true, - "head": true, - "patch": true, - "trace": true} - _, ok := pathLabels[s] - return ok +func (pb *PathBuilder) failf(v cue.Value, format string, args ...interface{}) { + panic(&openapiError{ + errors.NewMessage(format, args), + pb.ctx.path, + v.Pos(), + }) } func (pb *PathBuilder) buildPath(v cue.Value) *ast.StructLit { @@ -350,10 +341,6 @@ func (pb *PathBuilder) buildPath(v cue.Value) *ast.StructLit { for i, _ := v.Value().Fields(cue.Definitions(true)); i.Next(); { label := i.Label() - if !isPathLabel(label) { - continue - } - switch label { case "description": pb.pathDescription(v.Lookup("description")) @@ -361,13 +348,12 @@ func (pb *PathBuilder) buildPath(v cue.Value) *ast.StructLit { pb.securityList(v.Lookup("security")) case "get", "put", "post", "delete", "options", "head", "patch", "trace": pb.operation(v.Lookup(label)) - + default: + pb.failf(i.Value(), "unsupported field \"%v\" for path struct", label) } } - //return ast.NewStruct("description", pb.description, - // "security", pb.security) return (*ast.StructLit)(pb.path) } @@ -1264,11 +1250,7 @@ func (b *builder) bytes(v cue.Value) { } type PathBuilder struct { - ctx *buildContext - description *ast.BasicLit - operations *pathOperations - security *ast.ListLit - + ctx *buildContext path *OrderedMap } From e1ae134cee17ade34b4a64218a0e4a328903f5cd Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Wed, 27 Apr 2022 15:24:32 -0300 Subject: [PATCH 06/31] Updated path parsing prefix & added parsing for content struct in operations now the paths are checked with a prefix of the form $/ instead of $ --- encoding/openapi/build.go | 38 +++++++++++++++++++++++++++++++------ encoding/openapi/openapi.go | 2 +- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index 383340d78df..88388fe99e7 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -131,7 +131,7 @@ func paths(g *Generator, inst *cue.Instance) (paths *ast.StructLit, err error) { for i.Next() { label := i.Label() - if i.IsDefinition() || !strings.HasPrefix(label, "$") || c.isInternal(label) { + if i.IsDefinition() || !strings.HasPrefix(label, "$/") || c.isInternal(label) { continue } @@ -306,13 +306,13 @@ func (pb *PathBuilder) pathDescription(v cue.Value) { pb.path.Set("description", ast.NewString(description)) } -func (pb *PathBuilder) operation(v cue.Value) { - operations := &OrderedMap{} +func (pb *PathBuilder) responses(v cue.Value) *ast.StructLit { + responses := &OrderedMap{} for i, _ := v.Value().Fields(cue.Definitions(false)); i.Next(); { // searching http status label, err := strconv.Atoi(i.Label()) if err != nil { - continue + pb.failf(v, "%v is no HTTP Status code", label) } if label > 599 || label < 100 { @@ -320,12 +320,38 @@ func (pb *PathBuilder) operation(v cue.Value) { } responseStruct := Response(i.Value(), pb.ctx) - operations.Set(strconv.Itoa(label), responseStruct) + responses.Set(strconv.Itoa(label), responseStruct) } + return (*ast.StructLit)(responses) +} + +func (pb *PathBuilder) operation(v cue.Value) { + /* + operations := &OrderedMap{} + for i, _ := v.Value().Fields(cue.Definitions(false)); i.Next(); { + // searching http status + label, err := strconv.Atoi(i.Label()) + if err != nil { + continue + } + + if label > 599 || label < 100 { + pb.failf(v, "wrong HTTP Status code %v", label) + } + + responseStruct := Response(i.Value(), pb.ctx) + operations.Set(strconv.Itoa(label), responseStruct) + + } + */ + operation := &OrderedMap{} + responses := pb.responses(v.Lookup("responses")) + operation.Set("responses", responses) + label, _ := v.Label() - pb.path.Set(label, operations) + pb.path.Set(label, operation) } func (pb *PathBuilder) failf(v cue.Value, format string, args ...interface{}) { diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go index 527f317575f..1bcc09c7d7b 100644 --- a/encoding/openapi/openapi.go +++ b/encoding/openapi/openapi.go @@ -143,7 +143,7 @@ func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit, paths *ast. var info *ast.StructLit for i, _ := inst.Value().Fields(cue.Definitions(true)); i.Next(); { - if i.IsDefinition() || strings.HasPrefix(i.Label(), "$") { + if i.IsDefinition() || strings.HasPrefix(i.Label(), "$/") { continue } label := i.Label() From 85a75c8ec6dc891cfa803c4d960665ad7550f9fa Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Wed, 27 Apr 2022 15:27:04 -0300 Subject: [PATCH 07/31] Update tests for paths implementation --- encoding/openapi/openapi_test.go | 4 +++ encoding/openapi/testdata/simple-path.cue | 17 +++++++++++ encoding/openapi/testdata/simple-path.json | 33 ++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 encoding/openapi/testdata/simple-path.cue create mode 100644 encoding/openapi/testdata/simple-path.json diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go index ebfba56516f..385d070cfca 100644 --- a/encoding/openapi/openapi_test.go +++ b/encoding/openapi/openapi_test.go @@ -151,6 +151,10 @@ func TestParseDefinitions(t *testing.T) { in: "cycle.cue", config: &openapi.Config{Info: info, ExpandReferences: true}, err: "cycle", + }, { + in: "simple-path.cue", + out: "simple-path.json", + config: defaultConfig, }} for _, tc := range testCases { t.Run(tc.out, func(t *testing.T) { diff --git a/encoding/openapi/testdata/simple-path.cue b/encoding/openapi/testdata/simple-path.cue new file mode 100644 index 00000000000..8026446e23c --- /dev/null +++ b/encoding/openapi/testdata/simple-path.cue @@ -0,0 +1,17 @@ +"$/ping": { + security: ["token", "user"] + description: "Ping endpoint" + get: { + description: "Returns pong" + responses:{ + '200':{ + content: { + "text/plain":{ + schema: string + + } + } + } + } + } +} diff --git a/encoding/openapi/testdata/simple-path.json b/encoding/openapi/testdata/simple-path.json new file mode 100644 index 00000000000..259c95b6891 --- /dev/null +++ b/encoding/openapi/testdata/simple-path.json @@ -0,0 +1,33 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Generated by cue.", + "version": "no version" + }, + "paths": { + "/ping": { + "security": [ + "token", + "user" + ], + "description": "Ping endpoint", + "get": { + "responses": { + "200": { + "description": "", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": {} + } +} \ No newline at end of file From a9eb4a5ba37499c6592f1bce5e68986c3ce3a17c Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Wed, 27 Apr 2022 16:47:52 -0300 Subject: [PATCH 08/31] Added checking for empty content in responses --- encoding/openapi/pathResponse.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/encoding/openapi/pathResponse.go b/encoding/openapi/pathResponse.go index eb4653a8a1c..452b8cb2a08 100644 --- a/encoding/openapi/pathResponse.go +++ b/encoding/openapi/pathResponse.go @@ -62,7 +62,9 @@ func (rb *ResponseObjectBuilder) buildResponse(v cue.Value) *ast.StructLit { //rb.mediaTypes.Set("description", description) response.Set("description", description) - response.Set("content", rb.mediaTypes) + if rb.mediaTypes.len() != 0 { + response.Set("content", rb.mediaTypes) + } return (*ast.StructLit)(response) } From 46cff34f9fe252c577322f37fec0efe263200c50 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Wed, 27 Apr 2022 16:49:04 -0300 Subject: [PATCH 09/31] Add test for case of empty content in responses of paths --- encoding/openapi/openapi_test.go | 4 ++++ encoding/openapi/testdata/no-content-path.cue | 10 +++++++++ .../openapi/testdata/no-content-path.json | 21 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 encoding/openapi/testdata/no-content-path.cue create mode 100644 encoding/openapi/testdata/no-content-path.json diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go index 385d070cfca..27438810af8 100644 --- a/encoding/openapi/openapi_test.go +++ b/encoding/openapi/openapi_test.go @@ -155,6 +155,10 @@ func TestParseDefinitions(t *testing.T) { in: "simple-path.cue", out: "simple-path.json", config: defaultConfig, + }, { + in: "no-content-path.cue", + out: "no-content-path.json", + config: defaultConfig, }} for _, tc := range testCases { t.Run(tc.out, func(t *testing.T) { diff --git a/encoding/openapi/testdata/no-content-path.cue b/encoding/openapi/testdata/no-content-path.cue new file mode 100644 index 00000000000..837a4997a13 --- /dev/null +++ b/encoding/openapi/testdata/no-content-path.cue @@ -0,0 +1,10 @@ +"$/live": { + get: { + responses: { + "200": { + description: "live endpoint" + } + } + } +} + diff --git a/encoding/openapi/testdata/no-content-path.json b/encoding/openapi/testdata/no-content-path.json new file mode 100644 index 00000000000..449e353bd23 --- /dev/null +++ b/encoding/openapi/testdata/no-content-path.json @@ -0,0 +1,21 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Generated by cue.", + "version": "no version" + }, + "paths": { + "/live": { + "get": { + "responses": { + "200": { + "description": "live endpoint" + } + } + } + } + }, + "components": { + "schemas": {} + } +} \ No newline at end of file From 99a1864be21706ec370c677ed0859195d954df6e Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 3 May 2022 14:58:11 -0300 Subject: [PATCH 10/31] Add tests for checking multiples responses in a path, and failing in the case of a wrong http status code --- encoding/openapi/openapi_test.go | 36 ++++++++++++++++ .../testdata/multiple-responses-path.cue | 24 +++++++++++ .../testdata/multiple-responses-path.json | 43 +++++++++++++++++++ .../openapi/testdata/wrong-http-status.cue | 16 +++++++ 4 files changed, 119 insertions(+) create mode 100644 encoding/openapi/testdata/multiple-responses-path.cue create mode 100644 encoding/openapi/testdata/multiple-responses-path.json create mode 100644 encoding/openapi/testdata/wrong-http-status.cue diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go index 27438810af8..a2f15c95233 100644 --- a/encoding/openapi/openapi_test.go +++ b/encoding/openapi/openapi_test.go @@ -159,6 +159,10 @@ func TestParseDefinitions(t *testing.T) { in: "no-content-path.cue", out: "no-content-path.json", config: defaultConfig, + }, { + in: "multiple-responses-path.cue", + out: "multiple-responses-path.json", + config: defaultConfig, }} for _, tc := range testCases { t.Run(tc.out, func(t *testing.T) { @@ -264,3 +268,35 @@ func TestX(t *testing.T) { _ = json.Indent(out, b, "", " ") t.Error(out.String()) } + +func TestExpectedError(t *testing.T) { + defaultConfig := &openapi.Config{} + testCases := []struct { + in, out string + config *openapi.Config + err string + }{{ + in: "wrong-http-status.cue", + config: defaultConfig, + }} + + for _, tc := range testCases { + t.Run(tc.out, func(t *testing.T) { + filename := filepath.FromSlash(tc.in) + + inst := cue.Build(load.Instances([]string{filename}, &load.Config{ + Dir: "./testdata", + }))[0] + if inst.Err != nil { + t.Fatal(errors.Details(inst.Err, nil)) + } + + _, err := openapi.Gen(inst, tc.config) + if err == nil { + t.Fatal("expected error") + return + } + }) + } + +} diff --git a/encoding/openapi/testdata/multiple-responses-path.cue b/encoding/openapi/testdata/multiple-responses-path.cue new file mode 100644 index 00000000000..e912ec4dd72 --- /dev/null +++ b/encoding/openapi/testdata/multiple-responses-path.cue @@ -0,0 +1,24 @@ +"$/ping": { + security: ["token", "user"] + description: "Ping endpoint" + get: { + description: "Returns pong" + responses:{ + '200':{ + content: { + "text/plain":{ + schema: string + + } + } + } + '400': { + content: { + "text/plain": { + schema: string + } + } + } + } + } +} diff --git a/encoding/openapi/testdata/multiple-responses-path.json b/encoding/openapi/testdata/multiple-responses-path.json new file mode 100644 index 00000000000..bb78b0c1c8f --- /dev/null +++ b/encoding/openapi/testdata/multiple-responses-path.json @@ -0,0 +1,43 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Generated by cue.", + "version": "no version" + }, + "paths": { + "/ping": { + "security": [ + "token", + "user" + ], + "description": "Ping endpoint", + "get": { + "responses": { + "200": { + "description": "", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": {} + } +} \ No newline at end of file diff --git a/encoding/openapi/testdata/wrong-http-status.cue b/encoding/openapi/testdata/wrong-http-status.cue new file mode 100644 index 00000000000..e67a4b273b0 --- /dev/null +++ b/encoding/openapi/testdata/wrong-http-status.cue @@ -0,0 +1,16 @@ +"$/ping": { + security: ["token", "user"] + description: "Ping endpoint" + get: { + description: "Returns pong" + responses:{ + '666':{ + content: { + "application/json":{ + schema: {} + } + } + } + } + } +} From 4516632cc5d49e8d4cf92b0e6a6ea79940584a60 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 3 May 2022 15:27:36 -0300 Subject: [PATCH 11/31] Update content parsing now checking with a regex with words of the form (...)/(...) --- encoding/openapi/pathResponse.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/encoding/openapi/pathResponse.go b/encoding/openapi/pathResponse.go index 452b8cb2a08..5621ac8caf2 100644 --- a/encoding/openapi/pathResponse.go +++ b/encoding/openapi/pathResponse.go @@ -1,6 +1,8 @@ package openapi import ( + "regexp" + "cuelang.org/go/cue" "cuelang.org/go/cue/ast" ) @@ -52,7 +54,9 @@ func (rb *ResponseObjectBuilder) buildResponse(v cue.Value) *ast.StructLit { contentStruct := v.Lookup("content") for i, _ := contentStruct.Value().Fields(cue.Definitions(false)); i.Next(); { label := i.Label() - if !isMediaType(label) { + matched, _ := regexp.MatchString(`([^\s]+)[/]([^\s]+)`, label) + + if !matched { continue } rb.mediaType(i.Value()) From ead97927f1b2caca329bbdbb73b8beb3e550682b Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Mon, 16 May 2022 18:07:25 -0300 Subject: [PATCH 12/31] Add security and description parsing to operation object parsing --- encoding/openapi/build.go | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index 88388fe99e7..171e26191d2 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -347,7 +347,28 @@ func (pb *PathBuilder) operation(v cue.Value) { } */ operation := &OrderedMap{} + var security *ast.ListLit + + if v.Lookup("description").Exists() { + description, err := v.Lookup("description").String() + if err != nil { + description = "" + } + operation.Set("description", description) + } + + if v.Lookup("security").Exists() { + security = pb.securityList(v.Lookup("security")) + } else if pb.security != nil { + security = pb.security + } + if security != nil { + operation.Set("security", security) + + } + responses := pb.responses(v.Lookup("responses")) + operation.Set("responses", responses) label, _ := v.Label() @@ -371,7 +392,7 @@ func (pb *PathBuilder) buildPath(v cue.Value) *ast.StructLit { case "description": pb.pathDescription(v.Lookup("description")) case "security": - pb.securityList(v.Lookup("security")) + pb.security = pb.securityList(v.Lookup("security")) case "get", "put", "post", "delete", "options", "head", "patch", "trace": pb.operation(v.Lookup(label)) default: @@ -1070,15 +1091,15 @@ func (b *builder) array(v cue.Value) { } } -func (pb *PathBuilder) securityList(v cue.Value) { +func (pb *PathBuilder) securityList(v cue.Value) *ast.ListLit { items := []ast.Expr{} for i, _ := v.List(); i.Next(); { items = append(items, pb.decode(i.Value())) } - //pb.security = ast.NewList(items...) - pb.path.Set("security", ast.NewList(items...)) + return ast.NewList(items...) + //pb.path.Set("security", ast.NewList(items...)) } func (b *builder) listCap(v cue.Value) { @@ -1278,6 +1299,8 @@ func (b *builder) bytes(v cue.Value) { type PathBuilder struct { ctx *buildContext path *OrderedMap + + security *ast.ListLit } type builder struct { From 38d5b69af00809ec597a2573f52d3ea5bdc4aab2 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 17 May 2022 14:27:55 -0300 Subject: [PATCH 13/31] Correct unit tests according to changes in security parsing and description parsing in operations --- .../openapi/testdata/multiple-responses-path.cue | 5 ++++- .../openapi/testdata/multiple-responses-path.json | 15 +++++++++++---- encoding/openapi/testdata/simple-path.cue | 5 ++++- encoding/openapi/testdata/simple-path.json | 15 +++++++++++---- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/encoding/openapi/testdata/multiple-responses-path.cue b/encoding/openapi/testdata/multiple-responses-path.cue index e912ec4dd72..21dfe4a28d1 100644 --- a/encoding/openapi/testdata/multiple-responses-path.cue +++ b/encoding/openapi/testdata/multiple-responses-path.cue @@ -1,5 +1,8 @@ "$/ping": { - security: ["token", "user"] + security: [{ + "type": ["http"], + "scheme": ["basic"] + }] description: "Ping endpoint" get: { description: "Returns pong" diff --git a/encoding/openapi/testdata/multiple-responses-path.json b/encoding/openapi/testdata/multiple-responses-path.json index bb78b0c1c8f..c06ad9104b3 100644 --- a/encoding/openapi/testdata/multiple-responses-path.json +++ b/encoding/openapi/testdata/multiple-responses-path.json @@ -6,12 +6,19 @@ }, "paths": { "/ping": { - "security": [ - "token", - "user" - ], "description": "Ping endpoint", "get": { + "description": "Returns pong", + "security": [ + { + "type": [ + "http" + ], + "scheme": [ + "basic" + ] + } + ], "responses": { "200": { "description": "", diff --git a/encoding/openapi/testdata/simple-path.cue b/encoding/openapi/testdata/simple-path.cue index 8026446e23c..75a79e2213c 100644 --- a/encoding/openapi/testdata/simple-path.cue +++ b/encoding/openapi/testdata/simple-path.cue @@ -1,5 +1,8 @@ "$/ping": { - security: ["token", "user"] + security: [{ + "type": ["http"], + "scheme": ["basic"] + }] description: "Ping endpoint" get: { description: "Returns pong" diff --git a/encoding/openapi/testdata/simple-path.json b/encoding/openapi/testdata/simple-path.json index 259c95b6891..452b828dae6 100644 --- a/encoding/openapi/testdata/simple-path.json +++ b/encoding/openapi/testdata/simple-path.json @@ -6,12 +6,19 @@ }, "paths": { "/ping": { - "security": [ - "token", - "user" - ], "description": "Ping endpoint", "get": { + "description": "Returns pong", + "security": [ + { + "type": [ + "http" + ], + "scheme": [ + "basic" + ] + } + ], "responses": { "200": { "description": "", From bb0aa1fffeeccab0cbed9f55a40b78846f3d7dfc Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Thu, 19 May 2022 17:33:23 -0300 Subject: [PATCH 14/31] Remove dead code & change Regexp Match String with Compile --- encoding/openapi/build.go | 1 - encoding/openapi/pathResponse.go | 17 ++--------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index 171e26191d2..6b8f5072973 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -70,7 +70,6 @@ type externalType struct { } type oaSchema = OrderedMap -type pathOperations = OrderedMap type typeFunc func(b *builder, a cue.Value) diff --git a/encoding/openapi/pathResponse.go b/encoding/openapi/pathResponse.go index 5621ac8caf2..f3d9da7aedd 100644 --- a/encoding/openapi/pathResponse.go +++ b/encoding/openapi/pathResponse.go @@ -17,20 +17,6 @@ func newResponseBuilder(c *buildContext) *ResponseObjectBuilder { return &ResponseObjectBuilder{ctx: c, mediaTypes: &OrderedMap{}} } -func isMediaType(s string) bool { - mediaTypes := map[string]bool{"application/json": true, - "application/xml": true, - "application/x-www-form-urlencoded": true, - "multipart/form-data": true, - "text/plain": true, - "text/html": true, - "application/pdf": true, - "image/png": true} - - _, ok := mediaTypes[s] - return ok -} - func (rb *ResponseObjectBuilder) mediaType(v cue.Value) { schema := &OrderedMap{} schemaStruct := rb.ctx.build("schema", v.Lookup("schema")) @@ -52,9 +38,10 @@ func (rb *ResponseObjectBuilder) buildResponse(v cue.Value) *ast.StructLit { rb.description = description contentStruct := v.Lookup("content") + r, _ := regexp.Compile(`([^\s]+)[/]([^\s]+)`) for i, _ := contentStruct.Value().Fields(cue.Definitions(false)); i.Next(); { label := i.Label() - matched, _ := regexp.MatchString(`([^\s]+)[/]([^\s]+)`, label) + matched := r.MatchString(label) if !matched { continue From 662e56befdc5c4be9af95ed1759b11ad17d992a9 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Thu, 19 May 2022 17:54:15 -0300 Subject: [PATCH 15/31] Add unit tests for more cases of openapi paths --- encoding/openapi/openapi_test.go | 15 ++++- .../testdata/path-multiple-operations.cue | 34 ++++++++++ .../testdata/path-multiple-operations.json | 65 +++++++++++++++++++ encoding/openapi/testdata/path-with-ref.cue | 11 ++++ encoding/openapi/testdata/path-with-ref.json | 38 +++++++++++ 5 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 encoding/openapi/testdata/path-multiple-operations.cue create mode 100644 encoding/openapi/testdata/path-multiple-operations.json create mode 100644 encoding/openapi/testdata/path-with-ref.cue create mode 100644 encoding/openapi/testdata/path-with-ref.json diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go index a2f15c95233..67f9745c43e 100644 --- a/encoding/openapi/openapi_test.go +++ b/encoding/openapi/openapi_test.go @@ -160,10 +160,19 @@ func TestParseDefinitions(t *testing.T) { out: "no-content-path.json", config: defaultConfig, }, { - in: "multiple-responses-path.cue", - out: "multiple-responses-path.json", + in: "path-with-ref.cue", + out: "path-with-ref.json", config: defaultConfig, - }} + }, { + in: "path-multiple-operations.cue", + out: "path-multiple-operations.json", + config: defaultConfig, + }, + { + in: "multiple-responses-path.cue", + out: "multiple-responses-path.json", + config: defaultConfig, + }} for _, tc := range testCases { t.Run(tc.out, func(t *testing.T) { filename := filepath.FromSlash(tc.in) diff --git a/encoding/openapi/testdata/path-multiple-operations.cue b/encoding/openapi/testdata/path-multiple-operations.cue new file mode 100644 index 00000000000..e93a81242a6 --- /dev/null +++ b/encoding/openapi/testdata/path-multiple-operations.cue @@ -0,0 +1,34 @@ +"$/ping": { + security: [{ + "type": ["http"], + "scheme": ["basic"] + }] + description: "Ping endpoint" + get: { + description: "Returns pong" + responses:{ + '200':{ + content: { + "text/plain":{ + schema: string + + } + } + } + } + } + post: { + description: "Received a pong" + responses:{ + '200':{ + content: { + "application/json":{ + schema: int + } + } + } + } + } + + +} diff --git a/encoding/openapi/testdata/path-multiple-operations.json b/encoding/openapi/testdata/path-multiple-operations.json new file mode 100644 index 00000000000..46b530e1264 --- /dev/null +++ b/encoding/openapi/testdata/path-multiple-operations.json @@ -0,0 +1,65 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Generated by cue.", + "version": "no version" + }, + "paths": { + "/ping": { + "description": "Ping endpoint", + "get": { + "description": "Returns pong", + "security": [ + { + "type": [ + "http" + ], + "scheme": [ + "basic" + ] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "description": "Received a pong", + "security": [ + { + "type": [ + "http" + ], + "scheme": [ + "basic" + ] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + } + } + }, + "components": { + "schemas": {} + } +} \ No newline at end of file diff --git a/encoding/openapi/testdata/path-with-ref.cue b/encoding/openapi/testdata/path-with-ref.cue new file mode 100644 index 00000000000..f843bbb03ce --- /dev/null +++ b/encoding/openapi/testdata/path-with-ref.cue @@ -0,0 +1,11 @@ +info: { + title: "Foo API" + version: "v1" +} + +#Foo: bar?: number + +"$/foo": post: { + description: "foo it" + responses: "200": content: "application/json": schema: #Foo +} \ No newline at end of file diff --git a/encoding/openapi/testdata/path-with-ref.json b/encoding/openapi/testdata/path-with-ref.json new file mode 100644 index 00000000000..ef86132f91f --- /dev/null +++ b/encoding/openapi/testdata/path-with-ref.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Foo API", + "version": "v1" + }, + "paths": { + "/foo": { + "post": { + "description": "foo it", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Foo" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Foo": { + "type": "object", + "properties": { + "bar": { + "type": "number" + } + } + } + } + } +} \ No newline at end of file From e3964603e8ea35c4317ad9e27fd00a39e4ecd509 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 19 Apr 2022 15:11:57 -0300 Subject: [PATCH 16/31] Added paths functionality to compose encoding for openAPI --- encoding/openapi/openapi.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go index 6eaebb8cae0..527f317575f 100644 --- a/encoding/openapi/openapi.go +++ b/encoding/openapi/openapi.go @@ -92,7 +92,12 @@ func Generate(inst *cue.Instance, c *Config) (*ast.File, error) { if err != nil { return nil, err } - top, err := c.compose(inst, all) + paths, err := paths(c, inst) + if err != nil { + return nil, err + } + + top, err := c.compose(inst, all, paths) if err != nil { return nil, err } @@ -108,7 +113,12 @@ func (g *Generator) All(inst *cue.Instance) (*OrderedMap, error) { if err != nil { return nil, err } - top, err := g.compose(inst, all) + paths, err := paths(g, inst) + if err != nil { + return nil, err + } + + top, err := g.compose(inst, all, paths) return (*OrderedMap)(top), err } @@ -125,7 +135,7 @@ func toCUE(name string, x interface{}) (v ast.Expr, err error) { } -func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.StructLit, err error) { +func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit, paths *ast.StructLit) (x *ast.StructLit, err error) { var errs errors.Error @@ -133,7 +143,7 @@ func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.Str var info *ast.StructLit for i, _ := inst.Value().Fields(cue.Definitions(true)); i.Next(); { - if i.IsDefinition() { + if i.IsDefinition() || strings.HasPrefix(i.Label(), "$") { continue } label := i.Label() @@ -210,7 +220,7 @@ func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.Str return ast.NewStruct( "openapi", ast.NewString(c.Version), "info", info, - "paths", ast.NewStruct(), + "paths", paths, "components", ast.NewStruct("schemas", schemas), ), errs } From 4b0fd26136b3f296ecbfd5af865057d64648121c Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 19 Apr 2022 15:14:27 -0300 Subject: [PATCH 17/31] Added main functions to build paths struct --- encoding/openapi/build.go | 105 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index b076a08783b..9463dbc353c 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -46,6 +46,8 @@ type buildContext struct { fieldFilter *regexp.Regexp evalDepth int // detect cycles when resolving references + paths *OrderedMap + schemas *OrderedMap // Track external schemas. @@ -68,9 +70,89 @@ type externalType struct { } type oaSchema = OrderedMap +type pathOperations = OrderedMap type typeFunc func(b *builder, a cue.Value) +func paths(g *Generator, inst *cue.Instance) (paths *ast.StructLit, err error) { + var fieldFilter *regexp.Regexp + if g.FieldFilter != "" { + fieldFilter, err = regexp.Compile(g.FieldFilter) + if err != nil { + return nil, errors.Newf(token.NoPos, "invalid field filter: %v", err) + } + + // verify that certain elements are still passed. + for _, f := range strings.Split( + "version,title,allOf,anyOf,not,enum,Schema/properties,Schema/items"+ + "nullable,type", ",") { + if fieldFilter.MatchString(f) { + return nil, errors.Newf(token.NoPos, "field filter may not exclude %q", f) + } + } + } + + c := buildContext{ + inst: inst, + instExt: inst, + refPrefix: "components/schemas", + expandRefs: g.ExpandReferences, + structural: g.ExpandReferences, + nameFunc: g.ReferenceFunc, + descFunc: g.DescriptionFunc, + paths: &OrderedMap{}, + schemas: &OrderedMap{}, + externalRefs: map[string]*externalType{}, + fieldFilter: fieldFilter, + } + + switch g.Version { + case "3.0.0": + c.exclusiveBool = true + case "3.1.0": + default: + return nil, errors.Newf(token.NoPos, "unsupported version %s", g.Version) + } + + defer func() { + switch x := recover().(type) { + case nil: + case *openapiError: + err = x + default: + panic(x) + } + }() + + i, err := inst.Value().Fields(cue.Definitions(true)) + if err != nil { + return nil, err + } + for i.Next() { + label := i.Label() + + if i.IsDefinition() || !strings.HasPrefix(label, "$") || c.isInternal(label) { + continue + } + + label = label[1:] + ref := c.makeRef(inst, []string{label}) + if ref == "" { + continue + } + c.paths.Set(ref, c.buildPath(i.Value())) + } + + a := c.paths.Elts + sort.Slice(a, func(i, j int) bool { + x, _, _ := ast.LabelName(a[i].(*ast.Field).Label) + y, _, _ := ast.LabelName(a[j].(*ast.Field).Label) + return x < y + }) + + return (*ast.StructLit)(c.paths), c.errs +} + func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err error) { var fieldFilter *regexp.Regexp if g.FieldFilter != "" { @@ -101,6 +183,7 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro structural: g.ExpandReferences, nameFunc: g.ReferenceFunc, descFunc: g.DescriptionFunc, + paths: &OrderedMap{}, schemas: &OrderedMap{}, externalRefs: map[string]*externalType{}, fieldFilter: fieldFilter, @@ -146,6 +229,7 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro if ref == "" { continue } + println(ref, label) c.schemas.Set(ref, c.build(label, i.Value())) } @@ -180,6 +264,10 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro return (*ast.StructLit)(c.schemas), c.errs } +func (c *buildContext) buildPath(v cue.Value) *ast.StructLit { + return newRootPathBuilder(c).path(v) +} + func (c *buildContext) build(name string, v cue.Value) *ast.StructLit { return newCoreBuilder(c).schema(nil, name, v) } @@ -212,6 +300,13 @@ func (b *builder) checkArgs(a []cue.Value, n int) { } } +func (pb *PathBuilder) path(v cue.Value) *ast.StructLit { + str, _ := v.Lookup("description").String() + desc_str := ast.NewString(str) + + return ast.NewStruct("description", desc_str) +} + func (b *builder) schema(core *builder, name string, v cue.Value) *ast.StructLit { oldPath := b.ctx.path b.ctx.path = append(b.ctx.path, name) @@ -1093,6 +1188,12 @@ func (b *builder) bytes(v cue.Value) { } } +type PathBuilder struct { + ctx *buildContext + description string + operations *pathOperations +} + type builder struct { ctx *buildContext typ string @@ -1112,6 +1213,10 @@ type builder struct { items *builder } +func newRootPathBuilder(c *buildContext) *PathBuilder { + return &PathBuilder{ctx: c} +} + func newRootBuilder(c *buildContext) *builder { return &builder{ctx: c} } From 9230263303d9f50d22c8d031076c0735fab0b35f Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 19 Apr 2022 16:47:32 -0300 Subject: [PATCH 18/31] Updated description creation in path --- encoding/openapi/build.go | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index 9463dbc353c..cac2fe29525 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -300,11 +300,21 @@ func (b *builder) checkArgs(a []cue.Value, n int) { } } +func (pb *PathBuilder) pathDescription(v cue.Value) { + description, err := v.String() + if err != nil { + description = "" + } + pb.description = ast.NewString(description) +} + func (pb *PathBuilder) path(v cue.Value) *ast.StructLit { - str, _ := v.Lookup("description").String() - desc_str := ast.NewString(str) - return ast.NewStruct("description", desc_str) + pb.pathDescription(v.Lookup("description")) + pb.securityList(v.Lookup("security")) + + return ast.NewStruct("description", pb.description, + "security", pb.security) } func (b *builder) schema(core *builder, name string, v cue.Value) *ast.StructLit { @@ -994,6 +1004,17 @@ func (b *builder) array(v cue.Value) { } } +func (pb *PathBuilder) securityList(v cue.Value) { + items := []ast.Expr{} + + for i, _ := v.List(); i.Next(); { + items = append(items, pb.decode(i.Value())) + } + + pb.security = ast.NewList(items...) + +} + func (b *builder) listCap(v cue.Value) { switch op, a := v.Expr(); op { case cue.LessThanOp: @@ -1190,8 +1211,9 @@ func (b *builder) bytes(v cue.Value) { type PathBuilder struct { ctx *buildContext - description string + description *ast.BasicLit operations *pathOperations + security *ast.ListLit } type builder struct { @@ -1422,6 +1444,11 @@ func (b *builder) decode(v cue.Value) ast.Expr { return v.Syntax(cue.Final()).(ast.Expr) } +func (pb *PathBuilder) decode(v cue.Value) ast.Expr { + v, _ = v.Default() + return v.Syntax(cue.Final()).(ast.Expr) +} + func (b *builder) big(v cue.Value) ast.Expr { v, _ = v.Default() return v.Syntax(cue.Final()).(ast.Expr) From a0dae2f339349b2af101ecd6fc4976c8cbeb844d Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Thu, 21 Apr 2022 17:59:35 -0300 Subject: [PATCH 19/31] Add parsing functionalities to path's operations objects --- encoding/openapi/build.go | 79 +++++++++++++++++++++++++++----- encoding/openapi/pathResponse.go | 72 +++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 encoding/openapi/pathResponse.go diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index cac2fe29525..b160fc062a4 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -183,7 +183,6 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro structural: g.ExpandReferences, nameFunc: g.ReferenceFunc, descFunc: g.DescriptionFunc, - paths: &OrderedMap{}, schemas: &OrderedMap{}, externalRefs: map[string]*externalType{}, fieldFilter: fieldFilter, @@ -265,7 +264,7 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro } func (c *buildContext) buildPath(v cue.Value) *ast.StructLit { - return newRootPathBuilder(c).path(v) + return newRootPathBuilder(c).buildPath(v) } func (c *buildContext) build(name string, v cue.Value) *ast.StructLit { @@ -305,16 +304,71 @@ func (pb *PathBuilder) pathDescription(v cue.Value) { if err != nil { description = "" } - pb.description = ast.NewString(description) + pb.path.Set("description", ast.NewString(description)) +} + +func (pb *PathBuilder) operation(v cue.Value) { + operations := &OrderedMap{} + for i, _ := v.Value().Fields(cue.Definitions(false)); i.Next(); { + // searching http status + label, err := strconv.Atoi(i.Label()) + if err != nil { + continue + } + + // add an error for wrong http status? + if label > 599 || label < 100 { + continue + } + + responseStruct := Response(i.Value(), pb.ctx) + operations.Set(strconv.Itoa(label), responseStruct) + + } + + label, _ := v.Label() + pb.path.Set(label, operations) } -func (pb *PathBuilder) path(v cue.Value) *ast.StructLit { +func isPathLabel(s string) bool { + pathLabels := map[string]bool{"description": true, + "security": true, + "get": true, + "put": true, + "post": true, + "delete": true, + "options": true, + "head": true, + "patch": true, + "trace": true} + _, ok := pathLabels[s] + return ok +} + +func (pb *PathBuilder) buildPath(v cue.Value) *ast.StructLit { + + for i, _ := v.Value().Fields(cue.Definitions(true)); i.Next(); { + label := i.Label() + + if !isPathLabel(label) { + continue + } - pb.pathDescription(v.Lookup("description")) - pb.securityList(v.Lookup("security")) + switch label { + case "description": + pb.pathDescription(v.Lookup("description")) + case "security": + pb.securityList(v.Lookup("security")) + case "get", "put", "post", "delete", "options", "head", "patch", "trace": + pb.operation(v.Lookup(label)) - return ast.NewStruct("description", pb.description, - "security", pb.security) + } + + } + + //return ast.NewStruct("description", pb.description, + // "security", pb.security) + return (*ast.StructLit)(pb.path) } func (b *builder) schema(core *builder, name string, v cue.Value) *ast.StructLit { @@ -1011,8 +1065,8 @@ func (pb *PathBuilder) securityList(v cue.Value) { items = append(items, pb.decode(i.Value())) } - pb.security = ast.NewList(items...) - + //pb.security = ast.NewList(items...) + pb.path.Set("security", ast.NewList(items...)) } func (b *builder) listCap(v cue.Value) { @@ -1214,6 +1268,8 @@ type PathBuilder struct { description *ast.BasicLit operations *pathOperations security *ast.ListLit + + path *OrderedMap } type builder struct { @@ -1236,7 +1292,8 @@ type builder struct { } func newRootPathBuilder(c *buildContext) *PathBuilder { - return &PathBuilder{ctx: c} + return &PathBuilder{ctx: c, + path: &OrderedMap{}} } func newRootBuilder(c *buildContext) *builder { diff --git a/encoding/openapi/pathResponse.go b/encoding/openapi/pathResponse.go new file mode 100644 index 00000000000..eb4653a8a1c --- /dev/null +++ b/encoding/openapi/pathResponse.go @@ -0,0 +1,72 @@ +package openapi + +import ( + "cuelang.org/go/cue" + "cuelang.org/go/cue/ast" +) + +type ResponseObjectBuilder struct { + ctx *buildContext + description string + mediaTypes *OrderedMap +} + +func newResponseBuilder(c *buildContext) *ResponseObjectBuilder { + return &ResponseObjectBuilder{ctx: c, mediaTypes: &OrderedMap{}} +} + +func isMediaType(s string) bool { + mediaTypes := map[string]bool{"application/json": true, + "application/xml": true, + "application/x-www-form-urlencoded": true, + "multipart/form-data": true, + "text/plain": true, + "text/html": true, + "application/pdf": true, + "image/png": true} + + _, ok := mediaTypes[s] + return ok +} + +func (rb *ResponseObjectBuilder) mediaType(v cue.Value) { + schema := &OrderedMap{} + schemaStruct := rb.ctx.build("schema", v.Lookup("schema")) + + schema.Set("schema", schemaStruct) + + label, _ := v.Label() + rb.mediaTypes.Set(label, schema) +} + +func (rb *ResponseObjectBuilder) buildResponse(v cue.Value) *ast.StructLit { + + response := &OrderedMap{} + + description, err := v.Lookup("description").String() + if err != nil { + description = "" + } + rb.description = description + + contentStruct := v.Lookup("content") + for i, _ := contentStruct.Value().Fields(cue.Definitions(false)); i.Next(); { + label := i.Label() + if !isMediaType(label) { + continue + } + rb.mediaType(i.Value()) + + } + + //rb.mediaTypes.Set("description", description) + + response.Set("description", description) + response.Set("content", rb.mediaTypes) + + return (*ast.StructLit)(response) +} + +func Response(v cue.Value, c *buildContext) *ast.StructLit { + return newResponseBuilder(c).buildResponse(v) +} From 25e9efbaff44eb6fc5b1a78fe31dd3c37778ca96 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 26 Apr 2022 15:23:33 -0300 Subject: [PATCH 20/31] Add error handling for PathBuilder struct Update pathBuilder struct --- encoding/openapi/build.go | 38 ++++++++++---------------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index b160fc062a4..696333510b8 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -228,7 +228,6 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro if ref == "" { continue } - println(ref, label) c.schemas.Set(ref, c.build(label, i.Value())) } @@ -316,9 +315,8 @@ func (pb *PathBuilder) operation(v cue.Value) { continue } - // add an error for wrong http status? if label > 599 || label < 100 { - continue + pb.failf(v, "wrong HTTP Status code %v", label) } responseStruct := Response(i.Value(), pb.ctx) @@ -330,19 +328,12 @@ func (pb *PathBuilder) operation(v cue.Value) { pb.path.Set(label, operations) } -func isPathLabel(s string) bool { - pathLabels := map[string]bool{"description": true, - "security": true, - "get": true, - "put": true, - "post": true, - "delete": true, - "options": true, - "head": true, - "patch": true, - "trace": true} - _, ok := pathLabels[s] - return ok +func (pb *PathBuilder) failf(v cue.Value, format string, args ...interface{}) { + panic(&openapiError{ + errors.NewMessage(format, args), + pb.ctx.path, + v.Pos(), + }) } func (pb *PathBuilder) buildPath(v cue.Value) *ast.StructLit { @@ -350,10 +341,6 @@ func (pb *PathBuilder) buildPath(v cue.Value) *ast.StructLit { for i, _ := v.Value().Fields(cue.Definitions(true)); i.Next(); { label := i.Label() - if !isPathLabel(label) { - continue - } - switch label { case "description": pb.pathDescription(v.Lookup("description")) @@ -361,13 +348,12 @@ func (pb *PathBuilder) buildPath(v cue.Value) *ast.StructLit { pb.securityList(v.Lookup("security")) case "get", "put", "post", "delete", "options", "head", "patch", "trace": pb.operation(v.Lookup(label)) - + default: + pb.failf(i.Value(), "unsupported field \"%v\" for path struct", label) } } - //return ast.NewStruct("description", pb.description, - // "security", pb.security) return (*ast.StructLit)(pb.path) } @@ -1264,11 +1250,7 @@ func (b *builder) bytes(v cue.Value) { } type PathBuilder struct { - ctx *buildContext - description *ast.BasicLit - operations *pathOperations - security *ast.ListLit - + ctx *buildContext path *OrderedMap } From 443bd8246ea21551cd442895a2dd12c5e763ff9c Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Wed, 27 Apr 2022 15:24:32 -0300 Subject: [PATCH 21/31] Updated path parsing prefix & added parsing for content struct in operations now the paths are checked with a prefix of the form $/ instead of $ --- encoding/openapi/build.go | 38 +++++++++++++++++++++++++++++++------ encoding/openapi/openapi.go | 2 +- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index 696333510b8..8b1d3c5aab9 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -131,7 +131,7 @@ func paths(g *Generator, inst *cue.Instance) (paths *ast.StructLit, err error) { for i.Next() { label := i.Label() - if i.IsDefinition() || !strings.HasPrefix(label, "$") || c.isInternal(label) { + if i.IsDefinition() || !strings.HasPrefix(label, "$/") || c.isInternal(label) { continue } @@ -306,13 +306,13 @@ func (pb *PathBuilder) pathDescription(v cue.Value) { pb.path.Set("description", ast.NewString(description)) } -func (pb *PathBuilder) operation(v cue.Value) { - operations := &OrderedMap{} +func (pb *PathBuilder) responses(v cue.Value) *ast.StructLit { + responses := &OrderedMap{} for i, _ := v.Value().Fields(cue.Definitions(false)); i.Next(); { // searching http status label, err := strconv.Atoi(i.Label()) if err != nil { - continue + pb.failf(v, "%v is no HTTP Status code", label) } if label > 599 || label < 100 { @@ -320,12 +320,38 @@ func (pb *PathBuilder) operation(v cue.Value) { } responseStruct := Response(i.Value(), pb.ctx) - operations.Set(strconv.Itoa(label), responseStruct) + responses.Set(strconv.Itoa(label), responseStruct) } + return (*ast.StructLit)(responses) +} + +func (pb *PathBuilder) operation(v cue.Value) { + /* + operations := &OrderedMap{} + for i, _ := v.Value().Fields(cue.Definitions(false)); i.Next(); { + // searching http status + label, err := strconv.Atoi(i.Label()) + if err != nil { + continue + } + + if label > 599 || label < 100 { + pb.failf(v, "wrong HTTP Status code %v", label) + } + + responseStruct := Response(i.Value(), pb.ctx) + operations.Set(strconv.Itoa(label), responseStruct) + + } + */ + operation := &OrderedMap{} + responses := pb.responses(v.Lookup("responses")) + operation.Set("responses", responses) + label, _ := v.Label() - pb.path.Set(label, operations) + pb.path.Set(label, operation) } func (pb *PathBuilder) failf(v cue.Value, format string, args ...interface{}) { diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go index 527f317575f..1bcc09c7d7b 100644 --- a/encoding/openapi/openapi.go +++ b/encoding/openapi/openapi.go @@ -143,7 +143,7 @@ func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit, paths *ast. var info *ast.StructLit for i, _ := inst.Value().Fields(cue.Definitions(true)); i.Next(); { - if i.IsDefinition() || strings.HasPrefix(i.Label(), "$") { + if i.IsDefinition() || strings.HasPrefix(i.Label(), "$/") { continue } label := i.Label() From e006996db07b7a0c4ee8870c28991a9fc02f8fd7 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Wed, 27 Apr 2022 15:27:04 -0300 Subject: [PATCH 22/31] Update tests for paths implementation --- encoding/openapi/openapi_test.go | 4 +++ encoding/openapi/testdata/simple-path.cue | 17 +++++++++++ encoding/openapi/testdata/simple-path.json | 33 ++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 encoding/openapi/testdata/simple-path.cue create mode 100644 encoding/openapi/testdata/simple-path.json diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go index ebfba56516f..385d070cfca 100644 --- a/encoding/openapi/openapi_test.go +++ b/encoding/openapi/openapi_test.go @@ -151,6 +151,10 @@ func TestParseDefinitions(t *testing.T) { in: "cycle.cue", config: &openapi.Config{Info: info, ExpandReferences: true}, err: "cycle", + }, { + in: "simple-path.cue", + out: "simple-path.json", + config: defaultConfig, }} for _, tc := range testCases { t.Run(tc.out, func(t *testing.T) { diff --git a/encoding/openapi/testdata/simple-path.cue b/encoding/openapi/testdata/simple-path.cue new file mode 100644 index 00000000000..8026446e23c --- /dev/null +++ b/encoding/openapi/testdata/simple-path.cue @@ -0,0 +1,17 @@ +"$/ping": { + security: ["token", "user"] + description: "Ping endpoint" + get: { + description: "Returns pong" + responses:{ + '200':{ + content: { + "text/plain":{ + schema: string + + } + } + } + } + } +} diff --git a/encoding/openapi/testdata/simple-path.json b/encoding/openapi/testdata/simple-path.json new file mode 100644 index 00000000000..259c95b6891 --- /dev/null +++ b/encoding/openapi/testdata/simple-path.json @@ -0,0 +1,33 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Generated by cue.", + "version": "no version" + }, + "paths": { + "/ping": { + "security": [ + "token", + "user" + ], + "description": "Ping endpoint", + "get": { + "responses": { + "200": { + "description": "", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": {} + } +} \ No newline at end of file From 4ca6f8c6a23d6c1b509faf31042ac7c2a2f8d290 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Wed, 27 Apr 2022 16:47:52 -0300 Subject: [PATCH 23/31] Added checking for empty content in responses --- encoding/openapi/pathResponse.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/encoding/openapi/pathResponse.go b/encoding/openapi/pathResponse.go index eb4653a8a1c..452b8cb2a08 100644 --- a/encoding/openapi/pathResponse.go +++ b/encoding/openapi/pathResponse.go @@ -62,7 +62,9 @@ func (rb *ResponseObjectBuilder) buildResponse(v cue.Value) *ast.StructLit { //rb.mediaTypes.Set("description", description) response.Set("description", description) - response.Set("content", rb.mediaTypes) + if rb.mediaTypes.len() != 0 { + response.Set("content", rb.mediaTypes) + } return (*ast.StructLit)(response) } From a512c922b3ebe5fe9f8e2483a0362d6386b6288c Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Wed, 27 Apr 2022 16:49:04 -0300 Subject: [PATCH 24/31] Add test for case of empty content in responses of paths --- encoding/openapi/openapi_test.go | 4 ++++ encoding/openapi/testdata/no-content-path.cue | 10 +++++++++ .../openapi/testdata/no-content-path.json | 21 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 encoding/openapi/testdata/no-content-path.cue create mode 100644 encoding/openapi/testdata/no-content-path.json diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go index 385d070cfca..27438810af8 100644 --- a/encoding/openapi/openapi_test.go +++ b/encoding/openapi/openapi_test.go @@ -155,6 +155,10 @@ func TestParseDefinitions(t *testing.T) { in: "simple-path.cue", out: "simple-path.json", config: defaultConfig, + }, { + in: "no-content-path.cue", + out: "no-content-path.json", + config: defaultConfig, }} for _, tc := range testCases { t.Run(tc.out, func(t *testing.T) { diff --git a/encoding/openapi/testdata/no-content-path.cue b/encoding/openapi/testdata/no-content-path.cue new file mode 100644 index 00000000000..837a4997a13 --- /dev/null +++ b/encoding/openapi/testdata/no-content-path.cue @@ -0,0 +1,10 @@ +"$/live": { + get: { + responses: { + "200": { + description: "live endpoint" + } + } + } +} + diff --git a/encoding/openapi/testdata/no-content-path.json b/encoding/openapi/testdata/no-content-path.json new file mode 100644 index 00000000000..449e353bd23 --- /dev/null +++ b/encoding/openapi/testdata/no-content-path.json @@ -0,0 +1,21 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Generated by cue.", + "version": "no version" + }, + "paths": { + "/live": { + "get": { + "responses": { + "200": { + "description": "live endpoint" + } + } + } + } + }, + "components": { + "schemas": {} + } +} \ No newline at end of file From 87fba4584f2597ece8bbb4023473771ad66ec2ae Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 3 May 2022 14:58:11 -0300 Subject: [PATCH 25/31] Add tests for checking multiples responses in a path, and failing in the case of a wrong http status code --- encoding/openapi/openapi_test.go | 36 ++++++++++++++++ .../testdata/multiple-responses-path.cue | 24 +++++++++++ .../testdata/multiple-responses-path.json | 43 +++++++++++++++++++ .../openapi/testdata/wrong-http-status.cue | 16 +++++++ 4 files changed, 119 insertions(+) create mode 100644 encoding/openapi/testdata/multiple-responses-path.cue create mode 100644 encoding/openapi/testdata/multiple-responses-path.json create mode 100644 encoding/openapi/testdata/wrong-http-status.cue diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go index 27438810af8..a2f15c95233 100644 --- a/encoding/openapi/openapi_test.go +++ b/encoding/openapi/openapi_test.go @@ -159,6 +159,10 @@ func TestParseDefinitions(t *testing.T) { in: "no-content-path.cue", out: "no-content-path.json", config: defaultConfig, + }, { + in: "multiple-responses-path.cue", + out: "multiple-responses-path.json", + config: defaultConfig, }} for _, tc := range testCases { t.Run(tc.out, func(t *testing.T) { @@ -264,3 +268,35 @@ func TestX(t *testing.T) { _ = json.Indent(out, b, "", " ") t.Error(out.String()) } + +func TestExpectedError(t *testing.T) { + defaultConfig := &openapi.Config{} + testCases := []struct { + in, out string + config *openapi.Config + err string + }{{ + in: "wrong-http-status.cue", + config: defaultConfig, + }} + + for _, tc := range testCases { + t.Run(tc.out, func(t *testing.T) { + filename := filepath.FromSlash(tc.in) + + inst := cue.Build(load.Instances([]string{filename}, &load.Config{ + Dir: "./testdata", + }))[0] + if inst.Err != nil { + t.Fatal(errors.Details(inst.Err, nil)) + } + + _, err := openapi.Gen(inst, tc.config) + if err == nil { + t.Fatal("expected error") + return + } + }) + } + +} diff --git a/encoding/openapi/testdata/multiple-responses-path.cue b/encoding/openapi/testdata/multiple-responses-path.cue new file mode 100644 index 00000000000..e912ec4dd72 --- /dev/null +++ b/encoding/openapi/testdata/multiple-responses-path.cue @@ -0,0 +1,24 @@ +"$/ping": { + security: ["token", "user"] + description: "Ping endpoint" + get: { + description: "Returns pong" + responses:{ + '200':{ + content: { + "text/plain":{ + schema: string + + } + } + } + '400': { + content: { + "text/plain": { + schema: string + } + } + } + } + } +} diff --git a/encoding/openapi/testdata/multiple-responses-path.json b/encoding/openapi/testdata/multiple-responses-path.json new file mode 100644 index 00000000000..bb78b0c1c8f --- /dev/null +++ b/encoding/openapi/testdata/multiple-responses-path.json @@ -0,0 +1,43 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Generated by cue.", + "version": "no version" + }, + "paths": { + "/ping": { + "security": [ + "token", + "user" + ], + "description": "Ping endpoint", + "get": { + "responses": { + "200": { + "description": "", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": {} + } +} \ No newline at end of file diff --git a/encoding/openapi/testdata/wrong-http-status.cue b/encoding/openapi/testdata/wrong-http-status.cue new file mode 100644 index 00000000000..e67a4b273b0 --- /dev/null +++ b/encoding/openapi/testdata/wrong-http-status.cue @@ -0,0 +1,16 @@ +"$/ping": { + security: ["token", "user"] + description: "Ping endpoint" + get: { + description: "Returns pong" + responses:{ + '666':{ + content: { + "application/json":{ + schema: {} + } + } + } + } + } +} From a1de92c737c56a54b43bd84ee1b2043d2ffee584 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 3 May 2022 15:27:36 -0300 Subject: [PATCH 26/31] Update content parsing now checking with a regex with words of the form (...)/(...) --- encoding/openapi/pathResponse.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/encoding/openapi/pathResponse.go b/encoding/openapi/pathResponse.go index 452b8cb2a08..5621ac8caf2 100644 --- a/encoding/openapi/pathResponse.go +++ b/encoding/openapi/pathResponse.go @@ -1,6 +1,8 @@ package openapi import ( + "regexp" + "cuelang.org/go/cue" "cuelang.org/go/cue/ast" ) @@ -52,7 +54,9 @@ func (rb *ResponseObjectBuilder) buildResponse(v cue.Value) *ast.StructLit { contentStruct := v.Lookup("content") for i, _ := contentStruct.Value().Fields(cue.Definitions(false)); i.Next(); { label := i.Label() - if !isMediaType(label) { + matched, _ := regexp.MatchString(`([^\s]+)[/]([^\s]+)`, label) + + if !matched { continue } rb.mediaType(i.Value()) From dba1a8f57fd0c9cc2b12fe43e0fcb9a8981a921d Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Mon, 16 May 2022 18:07:25 -0300 Subject: [PATCH 27/31] Add security and description parsing to operation object parsing --- encoding/openapi/build.go | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index 8b1d3c5aab9..7757ceb71e5 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -347,7 +347,28 @@ func (pb *PathBuilder) operation(v cue.Value) { } */ operation := &OrderedMap{} + var security *ast.ListLit + + if v.Lookup("description").Exists() { + description, err := v.Lookup("description").String() + if err != nil { + description = "" + } + operation.Set("description", description) + } + + if v.Lookup("security").Exists() { + security = pb.securityList(v.Lookup("security")) + } else if pb.security != nil { + security = pb.security + } + if security != nil { + operation.Set("security", security) + + } + responses := pb.responses(v.Lookup("responses")) + operation.Set("responses", responses) label, _ := v.Label() @@ -371,7 +392,7 @@ func (pb *PathBuilder) buildPath(v cue.Value) *ast.StructLit { case "description": pb.pathDescription(v.Lookup("description")) case "security": - pb.securityList(v.Lookup("security")) + pb.security = pb.securityList(v.Lookup("security")) case "get", "put", "post", "delete", "options", "head", "patch", "trace": pb.operation(v.Lookup(label)) default: @@ -1070,15 +1091,15 @@ func (b *builder) array(v cue.Value) { } } -func (pb *PathBuilder) securityList(v cue.Value) { +func (pb *PathBuilder) securityList(v cue.Value) *ast.ListLit { items := []ast.Expr{} for i, _ := v.List(); i.Next(); { items = append(items, pb.decode(i.Value())) } - //pb.security = ast.NewList(items...) - pb.path.Set("security", ast.NewList(items...)) + return ast.NewList(items...) + //pb.path.Set("security", ast.NewList(items...)) } func (b *builder) listCap(v cue.Value) { @@ -1278,6 +1299,8 @@ func (b *builder) bytes(v cue.Value) { type PathBuilder struct { ctx *buildContext path *OrderedMap + + security *ast.ListLit } type builder struct { From d4a60d3dff8d3e9c1d0ccf4fb51337c8c16fec4c Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 17 May 2022 14:27:55 -0300 Subject: [PATCH 28/31] Correct unit tests according to changes in security parsing and description parsing in operations --- .../openapi/testdata/multiple-responses-path.cue | 5 ++++- .../openapi/testdata/multiple-responses-path.json | 15 +++++++++++---- encoding/openapi/testdata/simple-path.cue | 5 ++++- encoding/openapi/testdata/simple-path.json | 15 +++++++++++---- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/encoding/openapi/testdata/multiple-responses-path.cue b/encoding/openapi/testdata/multiple-responses-path.cue index e912ec4dd72..21dfe4a28d1 100644 --- a/encoding/openapi/testdata/multiple-responses-path.cue +++ b/encoding/openapi/testdata/multiple-responses-path.cue @@ -1,5 +1,8 @@ "$/ping": { - security: ["token", "user"] + security: [{ + "type": ["http"], + "scheme": ["basic"] + }] description: "Ping endpoint" get: { description: "Returns pong" diff --git a/encoding/openapi/testdata/multiple-responses-path.json b/encoding/openapi/testdata/multiple-responses-path.json index bb78b0c1c8f..c06ad9104b3 100644 --- a/encoding/openapi/testdata/multiple-responses-path.json +++ b/encoding/openapi/testdata/multiple-responses-path.json @@ -6,12 +6,19 @@ }, "paths": { "/ping": { - "security": [ - "token", - "user" - ], "description": "Ping endpoint", "get": { + "description": "Returns pong", + "security": [ + { + "type": [ + "http" + ], + "scheme": [ + "basic" + ] + } + ], "responses": { "200": { "description": "", diff --git a/encoding/openapi/testdata/simple-path.cue b/encoding/openapi/testdata/simple-path.cue index 8026446e23c..75a79e2213c 100644 --- a/encoding/openapi/testdata/simple-path.cue +++ b/encoding/openapi/testdata/simple-path.cue @@ -1,5 +1,8 @@ "$/ping": { - security: ["token", "user"] + security: [{ + "type": ["http"], + "scheme": ["basic"] + }] description: "Ping endpoint" get: { description: "Returns pong" diff --git a/encoding/openapi/testdata/simple-path.json b/encoding/openapi/testdata/simple-path.json index 259c95b6891..452b828dae6 100644 --- a/encoding/openapi/testdata/simple-path.json +++ b/encoding/openapi/testdata/simple-path.json @@ -6,12 +6,19 @@ }, "paths": { "/ping": { - "security": [ - "token", - "user" - ], "description": "Ping endpoint", "get": { + "description": "Returns pong", + "security": [ + { + "type": [ + "http" + ], + "scheme": [ + "basic" + ] + } + ], "responses": { "200": { "description": "", From 1851a2f3799ef691b29b8e9eb288e9d7821baf74 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Thu, 19 May 2022 17:33:23 -0300 Subject: [PATCH 29/31] Remove dead code & change Regexp Match String with Compile --- encoding/openapi/build.go | 1 - encoding/openapi/pathResponse.go | 17 ++--------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index 7757ceb71e5..83494a8f89c 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -70,7 +70,6 @@ type externalType struct { } type oaSchema = OrderedMap -type pathOperations = OrderedMap type typeFunc func(b *builder, a cue.Value) diff --git a/encoding/openapi/pathResponse.go b/encoding/openapi/pathResponse.go index 5621ac8caf2..f3d9da7aedd 100644 --- a/encoding/openapi/pathResponse.go +++ b/encoding/openapi/pathResponse.go @@ -17,20 +17,6 @@ func newResponseBuilder(c *buildContext) *ResponseObjectBuilder { return &ResponseObjectBuilder{ctx: c, mediaTypes: &OrderedMap{}} } -func isMediaType(s string) bool { - mediaTypes := map[string]bool{"application/json": true, - "application/xml": true, - "application/x-www-form-urlencoded": true, - "multipart/form-data": true, - "text/plain": true, - "text/html": true, - "application/pdf": true, - "image/png": true} - - _, ok := mediaTypes[s] - return ok -} - func (rb *ResponseObjectBuilder) mediaType(v cue.Value) { schema := &OrderedMap{} schemaStruct := rb.ctx.build("schema", v.Lookup("schema")) @@ -52,9 +38,10 @@ func (rb *ResponseObjectBuilder) buildResponse(v cue.Value) *ast.StructLit { rb.description = description contentStruct := v.Lookup("content") + r, _ := regexp.Compile(`([^\s]+)[/]([^\s]+)`) for i, _ := contentStruct.Value().Fields(cue.Definitions(false)); i.Next(); { label := i.Label() - matched, _ := regexp.MatchString(`([^\s]+)[/]([^\s]+)`, label) + matched := r.MatchString(label) if !matched { continue From 1a8a27638e8eef825aae8b19f7a767042a4a6c44 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Thu, 19 May 2022 17:54:15 -0300 Subject: [PATCH 30/31] Add unit tests for more cases of openapi paths --- encoding/openapi/openapi_test.go | 15 ++++- .../testdata/path-multiple-operations.cue | 34 ++++++++++ .../testdata/path-multiple-operations.json | 65 +++++++++++++++++++ encoding/openapi/testdata/path-with-ref.cue | 11 ++++ encoding/openapi/testdata/path-with-ref.json | 38 +++++++++++ 5 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 encoding/openapi/testdata/path-multiple-operations.cue create mode 100644 encoding/openapi/testdata/path-multiple-operations.json create mode 100644 encoding/openapi/testdata/path-with-ref.cue create mode 100644 encoding/openapi/testdata/path-with-ref.json diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go index a2f15c95233..67f9745c43e 100644 --- a/encoding/openapi/openapi_test.go +++ b/encoding/openapi/openapi_test.go @@ -160,10 +160,19 @@ func TestParseDefinitions(t *testing.T) { out: "no-content-path.json", config: defaultConfig, }, { - in: "multiple-responses-path.cue", - out: "multiple-responses-path.json", + in: "path-with-ref.cue", + out: "path-with-ref.json", config: defaultConfig, - }} + }, { + in: "path-multiple-operations.cue", + out: "path-multiple-operations.json", + config: defaultConfig, + }, + { + in: "multiple-responses-path.cue", + out: "multiple-responses-path.json", + config: defaultConfig, + }} for _, tc := range testCases { t.Run(tc.out, func(t *testing.T) { filename := filepath.FromSlash(tc.in) diff --git a/encoding/openapi/testdata/path-multiple-operations.cue b/encoding/openapi/testdata/path-multiple-operations.cue new file mode 100644 index 00000000000..e93a81242a6 --- /dev/null +++ b/encoding/openapi/testdata/path-multiple-operations.cue @@ -0,0 +1,34 @@ +"$/ping": { + security: [{ + "type": ["http"], + "scheme": ["basic"] + }] + description: "Ping endpoint" + get: { + description: "Returns pong" + responses:{ + '200':{ + content: { + "text/plain":{ + schema: string + + } + } + } + } + } + post: { + description: "Received a pong" + responses:{ + '200':{ + content: { + "application/json":{ + schema: int + } + } + } + } + } + + +} diff --git a/encoding/openapi/testdata/path-multiple-operations.json b/encoding/openapi/testdata/path-multiple-operations.json new file mode 100644 index 00000000000..46b530e1264 --- /dev/null +++ b/encoding/openapi/testdata/path-multiple-operations.json @@ -0,0 +1,65 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Generated by cue.", + "version": "no version" + }, + "paths": { + "/ping": { + "description": "Ping endpoint", + "get": { + "description": "Returns pong", + "security": [ + { + "type": [ + "http" + ], + "scheme": [ + "basic" + ] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "description": "Received a pong", + "security": [ + { + "type": [ + "http" + ], + "scheme": [ + "basic" + ] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + } + } + }, + "components": { + "schemas": {} + } +} \ No newline at end of file diff --git a/encoding/openapi/testdata/path-with-ref.cue b/encoding/openapi/testdata/path-with-ref.cue new file mode 100644 index 00000000000..f843bbb03ce --- /dev/null +++ b/encoding/openapi/testdata/path-with-ref.cue @@ -0,0 +1,11 @@ +info: { + title: "Foo API" + version: "v1" +} + +#Foo: bar?: number + +"$/foo": post: { + description: "foo it" + responses: "200": content: "application/json": schema: #Foo +} \ No newline at end of file diff --git a/encoding/openapi/testdata/path-with-ref.json b/encoding/openapi/testdata/path-with-ref.json new file mode 100644 index 00000000000..ef86132f91f --- /dev/null +++ b/encoding/openapi/testdata/path-with-ref.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Foo API", + "version": "v1" + }, + "paths": { + "/foo": { + "post": { + "description": "foo it", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Foo" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Foo": { + "type": "object", + "properties": { + "bar": { + "type": "number" + } + } + } + } + } +} \ No newline at end of file From 08b449f9a0d47080fcdb97f08640ef5f1633ad61 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Fri, 20 May 2022 18:34:32 -0300 Subject: [PATCH 31/31] Remove unnecesary comments --- encoding/openapi/build.go | 19 ------------------- encoding/openapi/pathResponse.go | 2 -- 2 files changed, 21 deletions(-) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index 83494a8f89c..952e963b100 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -327,24 +327,6 @@ func (pb *PathBuilder) responses(v cue.Value) *ast.StructLit { } func (pb *PathBuilder) operation(v cue.Value) { - /* - operations := &OrderedMap{} - for i, _ := v.Value().Fields(cue.Definitions(false)); i.Next(); { - // searching http status - label, err := strconv.Atoi(i.Label()) - if err != nil { - continue - } - - if label > 599 || label < 100 { - pb.failf(v, "wrong HTTP Status code %v", label) - } - - responseStruct := Response(i.Value(), pb.ctx) - operations.Set(strconv.Itoa(label), responseStruct) - - } - */ operation := &OrderedMap{} var security *ast.ListLit @@ -1098,7 +1080,6 @@ func (pb *PathBuilder) securityList(v cue.Value) *ast.ListLit { } return ast.NewList(items...) - //pb.path.Set("security", ast.NewList(items...)) } func (b *builder) listCap(v cue.Value) { diff --git a/encoding/openapi/pathResponse.go b/encoding/openapi/pathResponse.go index f3d9da7aedd..8026dbdc320 100644 --- a/encoding/openapi/pathResponse.go +++ b/encoding/openapi/pathResponse.go @@ -50,8 +50,6 @@ func (rb *ResponseObjectBuilder) buildResponse(v cue.Value) *ast.StructLit { } - //rb.mediaTypes.Set("description", description) - response.Set("description", description) if rb.mediaTypes.len() != 0 { response.Set("content", rb.mediaTypes)