From 66ea772b451293aac01f1bd2c610dfcf35af9fa3 Mon Sep 17 00:00:00 2001 From: Marcel van Lohuizen Date: Wed, 3 Sep 2025 15:07:08 +0200 Subject: [PATCH] pkg/encoding/openapi: support marshalSchema builtin for openAPI This was used in the cmd serve demo of OCS 2023. There were some issues with error wrapping in openapi when interacting with tasks, but these seem to be resolved. I've added TODOs where this used to be an issue. Signed-off-by: Marcel van Lohuizen Change-Id: I9e135178dabd599e357341e54f7452c8a528948f Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1221920 Reviewed-by: Roger Peppe TryBot-Result: CUEcueckoo Unity-Result: CUE porcuepine --- encoding/openapi/build.go | 3 + encoding/openapi/errors.go | 12 + encoding/openapi/openapi.go | 2 + pkg/encoding/openapi/openapi.cue | 56 ++ pkg/encoding/openapi/openapi.go | 94 ++ pkg/encoding/openapi/openapi_test.go | 25 + pkg/encoding/openapi/pkg.go | 58 ++ pkg/encoding/openapi/testdata/gen.txtar | 1083 +++++++++++++++++++++++ pkg/register.go | 1 + 9 files changed, 1334 insertions(+) create mode 100644 pkg/encoding/openapi/openapi.cue create mode 100644 pkg/encoding/openapi/openapi.go create mode 100644 pkg/encoding/openapi/openapi_test.go create mode 100644 pkg/encoding/openapi/pkg.go create mode 100644 pkg/encoding/openapi/testdata/gen.txtar 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" -- 2.51.2