diff --git a/content/docs/concept/how-cue-works-with-openapi/en.md b/content/docs/concept/how-cue-works-with-openapi/en.md new file mode 100644 index 0000000000..7c97192ca5 --- /dev/null +++ b/content/docs/concept/how-cue-works-with-openapi/en.md @@ -0,0 +1,236 @@ +--- +title: How CUE works with OpenAPI +tags: +- encodings +- cue command +authors: +- jpluscplusm +- myitcv +toc_hide: true +--- + +{{{with _script_ "en" "HIDDEN_ set up caches"}}} +export GOMODCACHE=/caches/gomodcache +export GOCACHE=/caches/gobuild +{{{end}}} + +CUE works with the +[OpenAPI 3.0.0 standard](https://github.com/OAI/OpenAPI-Specification/tree/3.0.0) +for the description of REST APIs by supporting the use and import of OpenAPI +`components.schemas` data schemas, and the export of CUE definitions into the +same namespace. + +CUE is usually more clear and concise than the equivalent OpenAPI. However, +given that they meet different needs for different types of users, CUE's +ability to round-trip between CUE and OpenAPI's data schema subset acts as a +useful bridge between their two worlds. + +## Reading and writing OpenAPI with the `cue` command + +The `cue` command can convert CUE schemas into OpenAPI's `components.schemas`. +CUE files can be converted into OpenAPI so long as they only specify +definitions and metadata (`info`, `$version`, etc) at their top-level. + +Let's start with a trivial CUE schema that we want to convert to OpenAPI: + +{{{with upload "en" "schema.cue"}}} +-- schema.cue -- +// A schema for the pet API. +package api + +$version: "v1.2.3" +// A Pet is a pet that we handle. +#Pet: { + // A pet has a name. + name!: string + // We only handle certain kinds of pets. + kind!: #Kind + // Centenarian pets are not handled. + age?: uint & <100 // TODO: increase limit if the tortoise proposal is accepted. + ... +} + +// Kind encodes the different pets we handle. +#Kind: "cat" | "dog" | "goldfish" +{{{end}}} + +The [`cue def`]({{< relref "docs/reference/cli/cue-def" >}}) command normalizes +the schema, and optionally converts it into another format: + +{{{with script "en" "cue def schema.cue"}}} +cue def schema.cue -o api.pet.yaml --out openapi+yaml +{{{end}}} + +{{{with _script_ "en" "HIDDEN_ move api.pet.yaml sideways"}}} +mv api.pet.yaml .api.pet.yaml +{{{end}}} + +The OpenAPI `info.title` field can be extracted from the top-level CUE comment, +or can be specified directly. +The same goes for OpenAPI's `info.version` field, which is extracted from CUE's +top-level `$version` field if not specified directly. + +Be aware of just how *long* an equivalent OpenAPI definition can become - not +all formats possess CUE's succinctness and economy of expression! +The `cue def` command creates this file: + +{{{with upload "en" "api.pet.yaml"}}} +-- api.pet.yaml -- +openapi: 3.0.0 +info: + title: A schema for the pet API. + version: v1.2.3 +paths: {} +components: + schemas: + Kind: + description: Kind encodes the different pets we handle. + type: string + enum: + - cat + - dog + - goldfish + Pet: + description: A Pet is a pet that we handle. + type: object + required: + - name + - kind + properties: + name: + description: A pet has a name. + type: string + kind: + $ref: '#/components/schemas/Kind' + age: + description: Centenarian pets are not handled. + type: integer + minimum: 0 + maximum: 100 + exclusiveMaximum: true +{{{end}}} + +{{{with _script_ "en" "HIDDEN_ diff api.pet.yaml"}}} +diff -wu api.pet.yaml .api.pet.yaml +rm .api.pet.yaml # tidy up +{{{end}}} + +Because CUE is more expressive than OpenAPI, it isn't possible to generate a +precise OpenAPI equivalent for *every* CUE schema. CUE does the best conversion +it can, limited by what OpenAPI's data schemas can represent. + +[`cue import`]({{< relref "docs/reference/cli/cue-import" >}}) can perform the +reverse operation, taking the OpenAPI definition emitted above and converting +it back to CUE: + +{{{with script "en" "import openapi yaml"}}} +cue import -p api api.pet.yaml +{{{end}}} + +{{{with _script_ "en" "HIDDEN_ move api.pet.cue sideways"}}} +mv api.pet.cue .api.pet.cue +{{{end}}} + +This produces the following CUE, which is as close to the original `schema.cue` +as OpenAPI's capabilities currently permit: + +{{{with upload "en" "api.pet.cue"}}} +-- api.pet.cue -- +// A schema for the pet API. +package api + +info: { + title: *"A schema for the pet API." | string + version: *"v1.2.3" | string +} +// Kind encodes the different pets we handle. +#Kind: "cat" | "dog" | "goldfish" + +// A Pet is a pet that we handle. +#Pet: { + // A pet has a name. + name: string + kind: #Kind + + // Centenarian pets are not handled. + age?: int & >=0 & <100 + ... +} +{{{end}}} + +{{{with _script_ "en" "HIDDEN_ diff api.pet.cue"}}} +diff -wu api.pet.cue .api.pet.cue +rm .api.pet.cue # tidy up +{{{end}}} + +## Using CUE's Go API + +CUE can also generate OpenAPI through its Go API. + +Generating an OpenAPI definition can be as simple as this: + +{{{with _script_ "en" "go mod init"}}} +go mod init mod.example +{{{end}}} + +{{{with upload "en" "go emit openapi main"}}} +-- main.go -- +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + + "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/cue/load" + "cuelang.org/go/encoding/openapi" +) + +func main() { + ctx := cuecontext.New() + bis := load.Instances([]string{"schema.cue"}, nil) + v := ctx.BuildInstance(bis[0]) + + // Generate the OpenAPI schema from the value loaded from schema.cue + b, err := openapi.Gen(v, nil) + if err != nil { + log.Fatal(err) + } + + // Render as indented JSON + var out bytes.Buffer + if err = json.Indent(&out, b, "", " "); err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", out.Bytes()) +} +{{{end}}} + +{{{with _script_ "en" "go mod tidy"}}} +#ellipsis 0 +go get cuelang.org/go@${CUELANG_CUE_LATEST} +#ellipsis 0 +go mod tidy +{{{end}}} + +{{{with script "en" "go run"}}} +#ellipsis 10 +go run . +{{{end}}} + +The [`encoding/openapi`](https://pkg.go.dev/cuelang.org/go/encoding/openapi) +package provides options to make a definition self-contained, to filter +constraints, and so on. The *expanding references* option enables the +"Structural OpenAPI" form required by CRDs targeting Kubernetes version 1.15 +and later. + +## Related content + +- CUE supports OpenAPI's `components.schemas` namespace, and general `info` metadata -- + {{}} tracks the support of other namespaces defined by the OpenAPI standard +- The [OpenAPI 3.0.0 specification](https://github.com/OAI/OpenAPI-Specification/tree/3.0.0) +- {{< linkto/related/reference "cli/cue-def" >}} +- {{< linkto/related/reference "cli/cue-import" >}} +- The [`encoding/openapi`](https://pkg.go.dev/cuelang.org/go/encoding/openapi) package diff --git a/content/docs/concept/how-cue-works-with-openapi/gen_cache.cue b/content/docs/concept/how-cue-works-with-openapi/gen_cache.cue new file mode 100644 index 0000000000..5dc5fb834f --- /dev/null +++ b/content/docs/concept/how-cue-works-with-openapi/gen_cache.cue @@ -0,0 +1,118 @@ +package site +{ + content: { + docs: { + concept: { + "how-cue-works-with-openapi": { + page: { + cache: { + upload: { + "schema.cue": "IhWYgY2d4XJ14dVZmRAuFEOvvNEGp0VQ4d4KmUiGPOI=" + "api.pet.yaml": "a59sa6kiZ8MIhIuFnjuABOwBKtJb6/0GvCczVlyVRrc=" + "api.pet.cue": "pfphGZPbu+Bc4/4G9UHYevOhe+M796iWteICO8ORgbg=" + "go emit openapi main": "jhu5SQXRFuKEuJidmqemyFh57/DWy/VIXCmfWazF7IQ=" + } + multi_step: { + hash: "J0UNMMV9HUS9M1DM0IFBABVSFGGVSUB7HGB29SLKL1ACHV001V1G====" + scriptHash: "MM7VG55V7882CDD5KQ5RA7LT8H0DUEA4KEINTQBIDTMNFM6E3IG0====" + steps: [{ + doc: "" + cmd: "export GOMODCACHE=/caches/gomodcache" + exitCode: 0 + output: "" + }, { + doc: "" + cmd: "export GOCACHE=/caches/gobuild" + exitCode: 0 + output: "" + }, { + doc: "" + cmd: "cue def schema.cue -o api.pet.yaml --out openapi+yaml" + exitCode: 0 + output: "" + }, { + doc: "" + cmd: "mv api.pet.yaml .api.pet.yaml" + exitCode: 0 + output: "" + }, { + doc: "" + cmd: "diff -wu api.pet.yaml .api.pet.yaml" + exitCode: 0 + output: "" + }, { + doc: "" + cmd: "rm .api.pet.yaml # tidy up" + exitCode: 0 + output: "" + }, { + doc: "" + cmd: "cue import -p api api.pet.yaml" + exitCode: 0 + output: "" + }, { + doc: "" + cmd: "mv api.pet.cue .api.pet.cue" + exitCode: 0 + output: "" + }, { + doc: "" + cmd: "diff -wu api.pet.cue .api.pet.cue" + exitCode: 0 + output: "" + }, { + doc: "" + cmd: "rm .api.pet.cue # tidy up" + exitCode: 0 + output: "" + }, { + doc: "" + cmd: "go mod init mod.example" + exitCode: 0 + output: """ + go: creating new go.mod: module mod.example + + """ + }, { + doc: "#ellipsis 0" + cmd: "go get cuelang.org/go@v0.8.2" + exitCode: 0 + output: """ + ... + + """ + }, { + doc: "#ellipsis 0" + cmd: "go mod tidy" + exitCode: 0 + output: """ + ... + + """ + }, { + doc: "#ellipsis 10" + cmd: "go run ." + exitCode: 0 + output: """ + { + "openapi": "3.0.0", + "info": { + "title": "A schema for the pet API.", + "version": "v1.2.3" + }, + "paths": {}, + "components": { + "schemas": { + "Kind": { + ... + + """ + }] + } + } + } + } + } + } + } +} diff --git a/content/docs/concept/how-cue-works-with-openapi/page.cue b/content/docs/concept/how-cue-works-with-openapi/page.cue new file mode 100644 index 0000000000..2e5c4a7965 --- /dev/null +++ b/content/docs/concept/how-cue-works-with-openapi/page.cue @@ -0,0 +1,3 @@ +package site + +content: docs: concept: "how-cue-works-with-openapi": page: _ diff --git a/hugo/content/en/docs/concept/how-cue-works-with-openapi/index.md b/hugo/content/en/docs/concept/how-cue-works-with-openapi/index.md new file mode 100644 index 0000000000..6cb15f9f64 --- /dev/null +++ b/hugo/content/en/docs/concept/how-cue-works-with-openapi/index.md @@ -0,0 +1,202 @@ +--- +title: How CUE works with OpenAPI +tags: +- encodings +- cue command +authors: +- jpluscplusm +- myitcv +toc_hide: true +--- + +CUE works with the +[OpenAPI 3.0.0 standard](https://github.com/OAI/OpenAPI-Specification/tree/3.0.0) +for the description of REST APIs by supporting the use and import of OpenAPI +`components.schemas` data schemas, and the export of CUE definitions into the +same namespace. + +CUE is usually more clear and concise than the equivalent OpenAPI. However, +given that they meet different needs for different types of users, CUE's +ability to round-trip between CUE and OpenAPI's data schema subset acts as a +useful bridge between their two worlds. + +## Reading and writing OpenAPI with the `cue` command + +The `cue` command can convert CUE schemas into OpenAPI's `components.schemas`. +CUE files can be converted into OpenAPI so long as they only specify +definitions and metadata (`info`, `$version`, etc) at their top-level. + +Let's start with a trivial CUE schema that we want to convert to OpenAPI: + +```cue { title="schema.cue" } +// A schema for the pet API. +package api + +$version: "v1.2.3" +// A Pet is a pet that we handle. +#Pet: { + // A pet has a name. + name!: string + // We only handle certain kinds of pets. + kind!: #Kind + // Centenarian pets are not handled. + age?: uint & <100 // TODO: increase limit if the tortoise proposal is accepted. + ... +} + +// Kind encodes the different pets we handle. +#Kind: "cat" | "dog" | "goldfish" +``` + +The [`cue def`]({{< relref "docs/reference/cli/cue-def" >}}) command normalizes +the schema, and optionally converts it into another format: + +```text { title="TERMINAL" codeToCopy="Y3VlIGRlZiBzY2hlbWEuY3VlIC1vIGFwaS5wZXQueWFtbCAtLW91dCBvcGVuYXBpK3lhbWw=" } +$ cue def schema.cue -o api.pet.yaml --out openapi+yaml +``` +The OpenAPI `info.title` field can be extracted from the top-level CUE comment, +or can be specified directly. +The same goes for OpenAPI's `info.version` field, which is extracted from CUE's +top-level `$version` field if not specified directly. + +Be aware of just how *long* an equivalent OpenAPI definition can become - not +all formats possess CUE's succinctness and economy of expression! +The `cue def` command creates this file: + +```yaml { title="api.pet.yaml" } +openapi: 3.0.0 +info: + title: A schema for the pet API. + version: v1.2.3 +paths: {} +components: + schemas: + Kind: + description: Kind encodes the different pets we handle. + type: string + enum: + - cat + - dog + - goldfish + Pet: + description: A Pet is a pet that we handle. + type: object + required: + - name + - kind + properties: + name: + description: A pet has a name. + type: string + kind: + $ref: '#/components/schemas/Kind' + age: + description: Centenarian pets are not handled. + type: integer + minimum: 0 + maximum: 100 + exclusiveMaximum: true +``` +Because CUE is more expressive than OpenAPI, it isn't possible to generate a +precise OpenAPI equivalent for *every* CUE schema. CUE does the best conversion +it can, limited by what OpenAPI's data schemas can represent. + +[`cue import`]({{< relref "docs/reference/cli/cue-import" >}}) can perform the +reverse operation, taking the OpenAPI definition emitted above and converting +it back to CUE: + +```text { title="TERMINAL" codeToCopy="Y3VlIGltcG9ydCAtcCBhcGkgYXBpLnBldC55YW1s" } +$ cue import -p api api.pet.yaml +``` +This produces the following CUE, which is as close to the original `schema.cue` +as OpenAPI's capabilities currently permit: + +```cue { title="api.pet.cue" } +// A schema for the pet API. +package api + +info: { + title: *"A schema for the pet API." | string + version: *"v1.2.3" | string +} +// Kind encodes the different pets we handle. +#Kind: "cat" | "dog" | "goldfish" + +// A Pet is a pet that we handle. +#Pet: { + // A pet has a name. + name: string + kind: #Kind + + // Centenarian pets are not handled. + age?: int & >=0 & <100 + ... +} +``` +## Using CUE's Go API + +CUE can also generate OpenAPI through its Go API. + +Generating an OpenAPI definition can be as simple as this: +```go { title="main.go" } +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + + "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/cue/load" + "cuelang.org/go/encoding/openapi" +) + +func main() { + ctx := cuecontext.New() + bis := load.Instances([]string{"schema.cue"}, nil) + v := ctx.BuildInstance(bis[0]) + + // Generate the OpenAPI schema from the value loaded from schema.cue + b, err := openapi.Gen(v, nil) + if err != nil { + log.Fatal(err) + } + + // Render as indented JSON + var out bytes.Buffer + if err = json.Indent(&out, b, "", " "); err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", out.Bytes()) +} +``` +```text { title="TERMINAL" codeToCopy="Z28gcnVuIC4=" } +$ go run . +{ + "openapi": "3.0.0", + "info": { + "title": "A schema for the pet API.", + "version": "v1.2.3" + }, + "paths": {}, + "components": { + "schemas": { + "Kind": { +... +``` + +The [`encoding/openapi`](https://pkg.go.dev/cuelang.org/go/encoding/openapi) +package provides options to make a definition self-contained, to filter +constraints, and so on. The *expanding references* option enables the +"Structural OpenAPI" form required by CRDs targeting Kubernetes version 1.15 +and later. + +## Related content + +- CUE supports OpenAPI's `components.schemas` namespace, and general `info` metadata -- + {{}} tracks the support of other namespaces defined by the OpenAPI standard +- The [OpenAPI 3.0.0 specification](https://github.com/OAI/OpenAPI-Specification/tree/3.0.0) +- {{< linkto/related/reference "cli/cue-def" >}} +- {{< linkto/related/reference "cli/cue-import" >}} +- The [`encoding/openapi`](https://pkg.go.dev/cuelang.org/go/encoding/openapi) package