Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

encoding/opeanpi: paths parsing for OpenAPI spec #1727

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
852369c
Added paths functionality to compose encoding for openAPI
qequ Apr 19, 2022
7da9a34
Added main functions to build paths struct
qequ Apr 19, 2022
496044e
Updated description creation in path
qequ Apr 19, 2022
f5d5163
Add parsing functionalities to path's operations objects
qequ Apr 21, 2022
dbb3bca
Add error handling for PathBuilder struct
qequ Apr 26, 2022
e1ae134
Updated path parsing prefix & added parsing for content struct in ope…
qequ Apr 27, 2022
85a75c8
Update tests for paths implementation
qequ Apr 27, 2022
a9eb4a5
Added checking for empty content in responses
qequ Apr 27, 2022
46cff34
Add test for case of empty content in responses of paths
qequ Apr 27, 2022
99a1864
Add tests for checking multiples responses in a path, and failing in …
qequ May 3, 2022
4516632
Update content parsing
qequ May 3, 2022
ead9792
Add security and description parsing to operation object parsing
qequ May 16, 2022
38d5b69
Correct unit tests according to changes in security parsing and descr…
qequ May 17, 2022
bb0aa1f
Remove dead code & change Regexp Match String with Compile
qequ May 19, 2022
662e56b
Add unit tests for more cases of openapi paths
qequ May 19, 2022
e396460
Added paths functionality to compose encoding for openAPI
qequ Apr 19, 2022
4b0fd26
Added main functions to build paths struct
qequ Apr 19, 2022
9230263
Updated description creation in path
qequ Apr 19, 2022
a0dae2f
Add parsing functionalities to path's operations objects
qequ Apr 21, 2022
25e9efb
Add error handling for PathBuilder struct
qequ Apr 26, 2022
443bd82
Updated path parsing prefix & added parsing for content struct in ope…
qequ Apr 27, 2022
e006996
Update tests for paths implementation
qequ Apr 27, 2022
4ca6f8c
Added checking for empty content in responses
qequ Apr 27, 2022
a512c92
Add test for case of empty content in responses of paths
qequ Apr 27, 2022
87fba45
Add tests for checking multiples responses in a path, and failing in …
qequ May 3, 2022
a1de92c
Update content parsing
qequ May 3, 2022
dba1a8f
Add security and description parsing to operation object parsing
qequ May 16, 2022
d4a60d3
Correct unit tests according to changes in security parsing and descr…
qequ May 17, 2022
1851a2f
Remove dead code & change Regexp Match String with Compile
qequ May 19, 2022
1a8a276
Add unit tests for more cases of openapi paths
qequ May 19, 2022
c64290d
Merge branch 'pathsImplementation' of github.com:qequ/cue into pathsI…
qequ May 20, 2022
08b449f
Remove unnecesary comments
qequ May 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions encoding/openapi/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type buildContext struct {
fieldFilter *regexp.Regexp
evalDepth int // detect cycles when resolving references

paths *OrderedMap

schemas *OrderedMap

// Track external schemas.
Expand All @@ -71,6 +73,85 @@ type oaSchema = 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 != "" {
Expand Down Expand Up @@ -180,6 +261,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).buildPath(v)
}

func (c *buildContext) build(name string, v cue.Value) *ast.StructLit {
return newCoreBuilder(c).schema(nil, name, v)
}
Expand Down Expand Up @@ -212,6 +297,94 @@ 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.path.Set("description", ast.NewString(description))
}

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 {
pb.failf(v, "%v is no HTTP Status code", label)
}

if label > 599 || label < 100 {
pb.failf(v, "wrong HTTP Status code %v", label)
}

responseStruct := Response(i.Value(), pb.ctx)
responses.Set(strconv.Itoa(label), responseStruct)

}

return (*ast.StructLit)(responses)
}

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()
pb.path.Set(label, operation)
}

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 {

for i, _ := v.Value().Fields(cue.Definitions(true)); i.Next(); {
label := i.Label()

switch label {
case "description":
pb.pathDescription(v.Lookup("description"))
case "security":
pb.security = 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.StructLit)(pb.path)
}

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)
Expand Down Expand Up @@ -899,6 +1072,16 @@ func (b *builder) array(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()))
}

return ast.NewList(items...)
}

func (b *builder) listCap(v cue.Value) {
switch op, a := v.Expr(); op {
case cue.LessThanOp:
Expand Down Expand Up @@ -1093,6 +1276,13 @@ func (b *builder) bytes(v cue.Value) {
}
}

type PathBuilder struct {
ctx *buildContext
path *OrderedMap

security *ast.ListLit
}

type builder struct {
ctx *buildContext
typ string
Expand All @@ -1112,6 +1302,11 @@ type builder struct {
items *builder
}

func newRootPathBuilder(c *buildContext) *PathBuilder {
return &PathBuilder{ctx: c,
path: &OrderedMap{}}
}

func newRootBuilder(c *buildContext) *builder {
return &builder{ctx: c}
}
Expand Down Expand Up @@ -1317,6 +1512,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)
Expand Down
20 changes: 15 additions & 5 deletions encoding/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}

Expand All @@ -125,15 +135,15 @@ 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

var title, version string
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()
Expand Down Expand Up @@ -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
}
Expand Down
55 changes: 54 additions & 1 deletion encoding/openapi/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,28 @@ 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,
}, {
in: "no-content-path.cue",
out: "no-content-path.json",
config: defaultConfig,
}, {
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)
Expand Down Expand Up @@ -256,3 +277,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
}
})
}

}
Loading