-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs/concept: how CUE works with OpenAPI
This adds a concept guide demonstrating how CUE works with OpenAPI. Much of the comparative discussion from https://cuelang.org/docs/concept/schema-definition-use-case/#json-schema--openapi is omitted, keeping the document focused on the pragmatic and possible. cue-lang/cue#3133 was opened as part of this change, which asks the project to support full round-tripping from OpenAPI to CUE and back again; or to decide and document which OpenAPI elements aren't supported. For cue-lang/docs-and-content#72 Preview-Path: /docs/concept/how-cue-works-with-openapi/ Signed-off-by: Jonathan Matthews <github@hello.jonathanmatthews.com> Change-Id: I9f9968d381d516f0e189616871919ddb063f11d3
- Loading branch information
1 parent
24ebac4
commit 5996aa1
Showing
4 changed files
with
559 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
insts := load.Instances([]string{"schema.cue"}, nil) | ||
v := ctx.BuildInstance(insts[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 metadata such as the `info` field -- | ||
{{<issue 3133/>}} 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 |
118 changes: 118 additions & 0 deletions
118
content/docs/concept/how-cue-works-with-openapi/gen_cache.cue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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": "iJ3M385hdYxCdbNbUSo2fUp8UxBgDb0Kra0vI/FTaFE=" | ||
} | ||
multi_step: { | ||
hash: "3AL269A7I4O2UGG9L7R6CMJ7NQ924KQM0LG8COQNQG9S8BUJ8VEG====" | ||
scriptHash: "5JNL3Q47AL0NUVFJA6OLQ28R6PVKQCQTRNKUO3BRGGFHPLF77K4G====" | ||
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": { | ||
... | ||
""" | ||
}] | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package site | ||
|
||
content: docs: concept: "how-cue-works-with-openapi": page: _ |
Oops, something went wrong.