Skip to content

Commit

Permalink
docs/concept: how CUE works with OpenAPI
Browse files Browse the repository at this point in the history
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
Dispatch-Trailer: {"type":"trybot","CL":1192024,"patchset":6,"ref":"refs/changes/24/1192024/6","targetBranch":"master"}
  • Loading branch information
jpluscplusm authored and cueckoo committed May 8, 2024
1 parent 24ebac4 commit 6fefd1e
Show file tree
Hide file tree
Showing 4 changed files with 559 additions and 0 deletions.
236 changes: 236 additions & 0 deletions content/docs/concept/how-cue-works-with-openapi/en.md
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()
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 --
{{<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 content/docs/concept/how-cue-works-with-openapi/gen_cache.cue
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": "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": {
...
"""
}]
}
}
}
}
}
}
}
}
3 changes: 3 additions & 0 deletions content/docs/concept/how-cue-works-with-openapi/page.cue
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: _

0 comments on commit 6fefd1e

Please sign in to comment.