diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index 01c07a03c..04bbe7f54 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -183,6 +183,7 @@ func (c *buildContext) isInternal(sel cue.Selector) bool { func (b *builder) failf(v cue.Value, format string, args ...interface{}) { panic(&openapiError{ errors.NewMessagef(format, args...), + v.Err(), cue.MakePath(b.ctx.path...), v.Pos(), }) @@ -692,6 +693,8 @@ func (b *builder) object(v cue.Value) { // TODO: extract format from specific type. default: + // TODO: consider // TODO(pkg): wrapping may cause issues in the + // builtin package. Seems fine for now though. b.failf(v, "unsupported op %v for object type (%v)", op, v) return } diff --git a/encoding/openapi/errors.go b/encoding/openapi/errors.go index 33d8358fe..2f48a8037 100644 --- a/encoding/openapi/errors.go +++ b/encoding/openapi/errors.go @@ -18,6 +18,8 @@ import ( "cuelang.org/go/cue" "cuelang.org/go/cue/errors" "cuelang.org/go/cue/token" + "cuelang.org/go/internal/core/adt" + "cuelang.org/go/internal/pkg" ) var _ errors.Error = &openapiError{} @@ -25,10 +27,20 @@ var _ errors.Error = &openapiError{} // implements cue/Error type openapiError struct { errors.Message + err error path cue.Path pos token.Pos } +// Bottom implements [pkg.Bottomer]. By doing so we ensure that logic that +// checks for an incomplete error can do so, even if wrapped in an openapiError. +func (e *openapiError) Bottom() *adt.Bottom { + if x, ok := e.err.(pkg.Bottomer); ok { + return x.Bottom() + } + return nil +} + func (e *openapiError) Position() token.Pos { return e.pos } diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go index 9e594a11e..3e68c4c8e 100644 --- a/encoding/openapi/openapi.go +++ b/encoding/openapi/openapi.go @@ -126,6 +126,8 @@ func toCUE(name string, x interface{}) (v ast.Expr, err error) { v, err = cuejson.Extract(name, b) } if err != nil { + // TODO(pkg): wrapping may cause issues in the builtin package. Seems + // fine for now though. return nil, errors.Wrapf(err, token.NoPos, "openapi: could not encode %s", name) } diff --git a/pkg/encoding/openapi/openapi.cue b/pkg/encoding/openapi/openapi.cue new file mode 100644 index 000000000..4ca0f5704 --- /dev/null +++ b/pkg/encoding/openapi/openapi.cue @@ -0,0 +1,56 @@ +// Copyright 2023 CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openapi + +// #Config represents options for generating OpenAPI. +#Config: { + // version is fixed to 3.0.0 for now. + version!: "3.0.0" + + info?: #Info + + // selfContained causes all non-expanded external references to be included + // in this document. + selfContained: bool | *false + + // expandReferences replaces references with actual objects when generating + // OpenAPI Schema. It is an error for an CUE value to refer to itself + // if this option is used. + expandReferences: bool | *false +} + +// #Info represents metadata about the API. +#Info: { + title!: string + version!: string + summary?: string + description?: string + termsOfService?: string + contact?: #Contact + license?: #License +} + +// #Contact represents contact information for the exposed API. +#Contact: { + name?: string + url?: string + email?: string +} + +// #License represents license information for the exposed API. +#License: { + name!: string + url?: string +} diff --git a/pkg/encoding/openapi/openapi.go b/pkg/encoding/openapi/openapi.go new file mode 100644 index 000000000..692ad9e7b --- /dev/null +++ b/pkg/encoding/openapi/openapi.go @@ -0,0 +1,94 @@ +// Copyright 2023 CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package openapi provides OpenAPI encoding and decoding functionality. +// +// This is an EXPERIMENTAL API. +package openapi + +import ( + "cuelang.org/go/cue" + "cuelang.org/go/encoding/openapi" + "cuelang.org/go/internal/core/adt" + "cuelang.org/go/internal/pkg" + "cuelang.org/go/internal/value" +) + +var ( + selfContainedPath = cue.ParsePath("selfContained") + expandReferencesPath = cue.ParsePath("expandReferences") + infoPath = cue.ParsePath("info") +) + +// Marshal returns the OpenAPI encoding of schema for the given OpenAPI version. +// The optional config value can be used to make further adjustments. +// +// Experimental: this API may change. +// +// schema can have the following fields: +// +// #Config: { +// // version holds the OpenAPI version to use when marshaling. +// // Currently only "3.0.0" is supported. +// version!: "3.0.0" // currently "3.0.0" only + +// // selfContained causes all non-expanded external references +// // to be included// +// selfContained?: bool +// +// // expandReferences replaces references with actual objects when generating +// // OpenAPI Schema. It is an error for an CUE value to refer to itself +// // if this option is used. +// expandReferences?: bool +// +// // info specifies the info section of the OpenAPI document. To be a valid +// // OpenAPI document, it must include at least the title and version fields. +// info?: { +// title: string +// description: string +// version: string +// } +// } +func MarshalSchema(config cue.Value, schema pkg.Schema) (string, error) { + // TODO: implement a proper struct for schema. + + ctx := value.OpContext(schema) + return marshalSchema(ctx, config, schema) +} + +func marshalSchema(_ *adt.OpContext, config cue.Value, schema pkg.Schema) (string, error) { + selfContained, _ := config.LookupPath(selfContainedPath).Bool() + expandReferences, _ := config.LookupPath(expandReferencesPath).Bool() + + version, err := config.LookupPath(cue.ParsePath("version")).String() + if err != nil { + return "", err + } + + c := &openapi.Config{ + Version: version, + SelfContained: selfContained, + ExpandReferences: expandReferences, + } + + if info := config.LookupPath(infoPath); info.Exists() { + c.Info = info + } + + b, err := openapi.Gen(schema, c) + if err != nil { + return "", err + } + return string(b), err +} diff --git a/pkg/encoding/openapi/openapi_test.go b/pkg/encoding/openapi/openapi_test.go new file mode 100644 index 000000000..651c438a0 --- /dev/null +++ b/pkg/encoding/openapi/openapi_test.go @@ -0,0 +1,25 @@ +// Copyright 2023 CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openapi_test + +import ( + "testing" + + "cuelang.org/go/pkg/internal/builtintest" +) + +func TestBuiltin(t *testing.T) { + builtintest.Run("openapi", t) +} diff --git a/pkg/encoding/openapi/pkg.go b/pkg/encoding/openapi/pkg.go new file mode 100644 index 000000000..32a74eac4 --- /dev/null +++ b/pkg/encoding/openapi/pkg.go @@ -0,0 +1,58 @@ +// Code generated by cuelang.org/go/pkg/gen. DO NOT EDIT. + +package openapi + +import ( + "cuelang.org/go/internal/core/adt" + "cuelang.org/go/internal/pkg" +) + +func init() { + pkg.Register("encoding/openapi", p) +} + +var _ = adt.TopKind // in case the adt package isn't used + +var p = &pkg.Package{ + Native: []*pkg.Builtin{{ + Name: "MarshalSchema", + Params: []pkg.Param{ + {Kind: adt.TopKind}, + {Kind: adt.TopKind}, + }, + Result: adt.StringKind, + NonConcrete: true, + Func: func(c *pkg.CallCtxt) { + config, schema := c.Value(0), c.Schema(1) + if c.Do() { + c.Ret, c.Err = marshalSchema(c.OpContext(), config, schema) + } + }, + }}, + CUE: `{ + #Config: { + version!: "3.0.0" + info?: #Info + selfContained: bool | *false + expandReferences: bool | *false + } + #Info: { + title!: string + version!: string + summary?: string + description?: string + termsOfService?: string + contact?: #Contact + license?: #License + } + #Contact: { + name?: string + url?: string + email?: string + } + #License: { + name!: string + url?: string + } +}`, +} diff --git a/pkg/encoding/openapi/testdata/gen.txtar b/pkg/encoding/openapi/testdata/gen.txtar new file mode 100644 index 000000000..c63c48337 --- /dev/null +++ b/pkg/encoding/openapi/testdata/gen.txtar @@ -0,0 +1,1083 @@ +-- in.cue -- +import ( + "encoding/json" + "encoding/openapi" +) + +// Basic schema marshaling test +basic: { + config: openapi.#Config & { + version: "3.0.0" + info: { + title: "Test API" + version: "1.0.0" + } + } + schema: { + // A User is a person identified by their name and age. + #User: { + name: string + age: int + } + } + result: json.Indent(openapi.MarshalSchema(config, schema), "", " ") +} + +// Self-contained schema test +selfContained: { + config: openapi.#Config & { + version: "3.0.0" + info: { + title: "Test API" + version: "1.0.0" + } + selfContained: true + } + schema: { + #Person: { + name: string + address: #Address + } + #Address: { + street: string + city: string + } + } + result: json.Indent(openapi.MarshalSchema(config, schema), "", " ") +} + +// Expand references test +expandReferences: { + config: openapi.#Config & { + version: "3.0.0" + info: { + title: "Test API" + version: "1.0.0" + } + expandReferences: true + } + schema: { + #Product: { + id: int + name: string + category: #Category + subcategory: #Category + } + #Category: { + name: string + id: int + } + } + result: json.Indent(openapi.MarshalSchema(config, schema), "", " ") +} + +// Test with constraints and validation +constraints: { + config: openapi.#Config & { + version: "3.0.0" + info: { + title: "Validation API" + version: "1.0.0" + } + } + schema: { + #User: { + name: string & len(_) > 0 + age: int & >=0 & <=120 + email: string & =~"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" + } + } + result: json.Indent(openapi.MarshalSchema(config, schema), "", " ") +} + +// Test with optional and required fields +optional: { + config: openapi.#Config & { + version: "3.0.0" + info: { + title: "Optional Fields API" + version: "1.0.0" + } + } + schema: { + #User: { + name!: string + age?: int + email?: string + metadata?: {...} + } + } + result: json.Indent(openapi.MarshalSchema(config, schema), "", " ") +} + +// Test with arrays and objects +collections: { + config: openapi.#Config & { + version: "3.0.0" + info: { + title: "Collections API" + version: "1.0.0" + } + } + schema: { + #UserList: { + users: [...#User] + total: int + } + #User: { + id: int + name: string + tags: [...string] + } + } + result: json.Indent(openapi.MarshalSchema(config, schema), "", " ") +} + +// Test with enums and unions +enums: { + config: openapi.#Config & { + version: "3.0.0" + info: { + title: "Enums API" + version: "1.0.0" + } + } + schema: { + #Status: "active" | "inactive" | "pending" + #Priority: 1 | 2 | 3 + #Task: { + id: int + status: #Status + priority: #Priority + } + } + result: json.Indent(openapi.MarshalSchema(config, schema), "", " ") +} + +// Test error handling - invalid version +invalidVersion: { + config: openapi.#Config & { + version: "invalid.version" + info: { + title: "Test API" + version: "1.0.0" + } + } + schema: { + #User: { + name: string + } + } + result: json.Indent(openapi.MarshalSchema(config, schema), "", " ") +} + +// Test minimal config +minimal: { + config: openapi.#Config & { + version: "3.0.0" + info: { + title: "Minimal API" + version: "1.0" + } + } + schema: { + #Simple: { + value: string + } + } + result: json.Indent(openapi.MarshalSchema(config, schema), "", " ") +} + +// Test with nested structures +nested: { + config: openapi.#Config & { + version: "3.0.0" + info: { + title: "Nested API" + version: "1.0.0" + } + } + schema: { + #Company: { + name: string + departments: [...#Department] + } + #Department: { + name: string + manager: #Employee + employees: [...#Employee] + } + #Employee: { + id: int + name: string + position: string + } + } + result: json.Indent(openapi.MarshalSchema(config, schema), "", " ") +} +-- out/openapi-v3 -- +Errors: +invalidVersion.config.version: conflicting values "3.0.0" and "invalid.version": + ./in.cue:158:10 + ./in.cue:159:12 + encoding/openapi:3:21 + +Result: +import ( + "encoding/json" + "encoding/openapi" +) + +// Basic schema marshaling test +basic: { + config: { + version: "3.0.0" + info: { + title: "Test API" + version: "1.0.0" + summary?: string + description?: string + termsOfService?: string + contact?: { + name?: string + url?: string + email?: string + } + license?: { + name!: string + url?: string + } + } + selfContained: *false | bool + expandReferences: *false | bool + } + schema: { + // A User is a person identified by their name and age. + #User: { + name: string + age: int + } + } + result: """ + { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "User": { + "description": "A User is a person identified by their name and age.", + "type": "object", + "required": [ + "name", + "age" + ], + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + } + } + } + } + } + """ +} + +// Self-contained schema test +selfContained: { + config: { + version: "3.0.0" + info: { + title: "Test API" + version: "1.0.0" + summary?: string + description?: string + termsOfService?: string + contact?: { + name?: string + url?: string + email?: string + } + license?: { + name!: string + url?: string + } + } + selfContained: true + expandReferences: *false | bool + } + schema: { + #Person: { + name: string + address: { + street: string + city: string + } + } + #Address: { + street: string + city: string + } + } + result: """ + { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "Address": { + "type": "object", + "required": [ + "street", + "city" + ], + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + } + } + }, + "Person": { + "type": "object", + "required": [ + "name", + "address" + ], + "properties": { + "name": { + "type": "string" + }, + "address": { + "$ref": "#/components/schemas/selfContained.schema.Address" + } + } + }, + "selfContained.schema.Address": { + "type": "object", + "required": [ + "street", + "city" + ], + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + } + } + } + } + } + } + """ +} + +// Expand references test +expandReferences: { + config: { + version: "3.0.0" + info: { + title: "Test API" + version: "1.0.0" + summary?: string + description?: string + termsOfService?: string + contact?: { + name?: string + url?: string + email?: string + } + license?: { + name!: string + url?: string + } + } + selfContained: *false | bool + expandReferences: true + } + schema: { + #Product: { + id: int + name: string + category: { + name: string + id: int + } + subcategory: { + name: string + id: int + } + } + #Category: { + name: string + id: int + } + } + result: """ + { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "Category": { + "type": "object", + "required": [ + "name", + "id" + ], + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, + "Product": { + "type": "object", + "required": [ + "id", + "name", + "category", + "subcategory" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "category": { + "type": "object", + "required": [ + "name", + "id" + ], + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, + "subcategory": { + "type": "object", + "required": [ + "name", + "id" + ], + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + } + } + } + } + } + } + """ +} + +// Test with constraints and validation +constraints: { + config: { + version: "3.0.0" + info: { + title: "Validation API" + version: "1.0.0" + summary?: string + description?: string + termsOfService?: string + contact?: { + name?: string + url?: string + email?: string + } + license?: { + name!: string + url?: string + } + } + selfContained: *false | bool + expandReferences: *false | bool + } + schema: { + #User: { + name: string & len(_) > 0 + age: uint & <=120 + email: =~"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" + } + } + result: json.Indent(openapi.MarshalSchema(config, schema), "", " ") +} + +// Test with optional and required fields +optional: { + config: { + version: "3.0.0" + info: { + title: "Optional Fields API" + version: "1.0.0" + summary?: string + description?: string + termsOfService?: string + contact?: { + name?: string + url?: string + email?: string + } + license?: { + name!: string + url?: string + } + } + selfContained: *false | bool + expandReferences: *false | bool + } + schema: { + #User: { + name!: string + age?: int + email?: string + metadata?: {} + } + } + result: """ + { + "openapi": "3.0.0", + "info": { + "title": "Optional Fields API", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "User": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "metadata": { + "type": "object" + } + } + } + } + } + } + """ +} + +// Test with arrays and objects +collections: { + config: { + version: "3.0.0" + info: { + title: "Collections API" + version: "1.0.0" + summary?: string + description?: string + termsOfService?: string + contact?: { + name?: string + url?: string + email?: string + } + license?: { + name!: string + url?: string + } + } + selfContained: *false | bool + expandReferences: *false | bool + } + schema: { + #UserList: { + users: [...{ + id: int + name: string + tags: [...string] + }] + total: int + } + #User: { + id: int + name: string + tags: [...string] + } + } + result: """ + { + "openapi": "3.0.0", + "info": { + "title": "Collections API", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "User": { + "type": "object", + "required": [ + "id", + "name", + "tags" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "UserList": { + "type": "object", + "required": [ + "users", + "total" + ], + "properties": { + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/collections.schema.User" + } + }, + "total": { + "type": "integer" + } + } + }, + "collections.schema.User": { + "type": "object", + "required": [ + "id", + "name", + "tags" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + """ +} + +// Test with enums and unions +enums: { + config: { + version: "3.0.0" + info: { + title: "Enums API" + version: "1.0.0" + summary?: string + description?: string + termsOfService?: string + contact?: { + name?: string + url?: string + email?: string + } + license?: { + name!: string + url?: string + } + } + selfContained: *false | bool + expandReferences: *false | bool + } + schema: { + #Status: "active" | "inactive" | "pending" + #Priority: 1 | 2 | 3 + #Task: { + id: int + status: "active" | "inactive" | "pending" + priority: 1 | 2 | 3 + } + } + result: """ + { + "openapi": "3.0.0", + "info": { + "title": "Enums API", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "Priority": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ] + }, + "Status": { + "type": "string", + "enum": [ + "active", + "inactive", + "pending" + ] + }, + "Task": { + "type": "object", + "required": [ + "id", + "status", + "priority" + ], + "properties": { + "id": { + "type": "integer" + }, + "status": { + "$ref": "#/components/schemas/enums.schema.Status" + }, + "priority": { + "$ref": "#/components/schemas/enums.schema.Priority" + } + } + }, + "enums.schema.Priority": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ] + }, + "enums.schema.Status": { + "type": "string", + "enum": [ + "active", + "inactive", + "pending" + ] + } + } + } + } + """ +} + +// Test error handling - invalid version +invalidVersion: { + config: { + version: _|_ // invalidVersion.config.version: conflicting values "3.0.0" and "invalid.version" + info: { + title: "Test API" + version: "1.0.0" + summary?: string + description?: string + termsOfService?: string + contact?: { + name?: string + url?: string + email?: string + } + license?: { + name!: string + url?: string + } + } + selfContained: *false | bool + expandReferences: *false | bool + } + schema: { + #User: { + name: string + } + } + result: _|_ // invalidVersion.config.version: conflicting values "3.0.0" and "invalid.version" +} + +// Test minimal config +minimal: { + config: { + version: "3.0.0" + info: { + title: "Minimal API" + version: "1.0" + summary?: string + description?: string + termsOfService?: string + contact?: { + name?: string + url?: string + email?: string + } + license?: { + name!: string + url?: string + } + } + selfContained: *false | bool + expandReferences: *false | bool + } + schema: { + #Simple: { + value: string + } + } + result: """ + { + "openapi": "3.0.0", + "info": { + "title": "Minimal API", + "version": "1.0" + }, + "paths": {}, + "components": { + "schemas": { + "Simple": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + } + } + } + } + } + } + """ +} + +// Test with nested structures +nested: { + config: { + version: "3.0.0" + info: { + title: "Nested API" + version: "1.0.0" + summary?: string + description?: string + termsOfService?: string + contact?: { + name?: string + url?: string + email?: string + } + license?: { + name!: string + url?: string + } + } + selfContained: *false | bool + expandReferences: *false | bool + } + schema: { + #Company: { + name: string + departments: [...{ + name: string + manager: { + id: int + name: string + position: string + } + employees: [...{ + id: int + name: string + position: string + }] + }] + } + #Department: { + name: string + manager: { + id: int + name: string + position: string + } + employees: [...{ + id: int + name: string + position: string + }] + } + #Employee: { + id: int + name: string + position: string + } + } + result: """ + { + "openapi": "3.0.0", + "info": { + "title": "Nested API", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "Company": { + "type": "object", + "required": [ + "name", + "departments" + ], + "properties": { + "name": { + "type": "string" + }, + "departments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/nested.schema.Department" + } + } + } + }, + "Department": { + "type": "object", + "required": [ + "name", + "manager", + "employees" + ], + "properties": { + "name": { + "type": "string" + }, + "manager": { + "$ref": "#/components/schemas/nested.schema.Employee" + }, + "employees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/nested.schema.Employee" + } + } + } + }, + "Employee": { + "type": "object", + "required": [ + "id", + "name", + "position" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "position": { + "type": "string" + } + } + }, + "nested.schema.Department": { + "type": "object", + "required": [ + "name", + "manager", + "employees" + ], + "properties": { + "name": { + "type": "string" + }, + "manager": { + "$ref": "#/components/schemas/nested.schema.Employee" + }, + "employees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/nested.schema.Employee" + } + } + } + }, + "nested.schema.Employee": { + "type": "object", + "required": [ + "id", + "name", + "position" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "position": { + "type": "string" + } + } + } + } + } + } + """ +} diff --git a/pkg/register.go b/pkg/register.go index 484507b58..592fcf9ab 100644 --- a/pkg/register.go +++ b/pkg/register.go @@ -13,6 +13,7 @@ import ( _ "cuelang.org/go/pkg/encoding/csv" _ "cuelang.org/go/pkg/encoding/hex" _ "cuelang.org/go/pkg/encoding/json" + _ "cuelang.org/go/pkg/encoding/openapi" _ "cuelang.org/go/pkg/encoding/toml" _ "cuelang.org/go/pkg/encoding/yaml" _ "cuelang.org/go/pkg/html"