|
| 1 | +--- |
| 2 | +title: How CUE works with OpenAPI |
| 3 | +tags: |
| 4 | +- encodings |
| 5 | +- cue command |
| 6 | +authors: |
| 7 | +- jpluscplusm |
| 8 | +- myitcv |
| 9 | +toc_hide: true |
| 10 | +--- |
| 11 | + |
| 12 | +{{{with _script_ "en" "HIDDEN_ set up caches"}}} |
| 13 | +export GOMODCACHE=/caches/gomodcache |
| 14 | +export GOCACHE=/caches/gobuild |
| 15 | +{{{end}}} |
| 16 | + |
| 17 | +CUE works with the |
| 18 | +[OpenAPI 3.0.0 standard](https://github.com/OAI/OpenAPI-Specification/tree/3.0.0) |
| 19 | +for the description of REST APIs by supporting the use and import of OpenAPI |
| 20 | +`components.schemas` data schemas, and the export of CUE definitions into the |
| 21 | +same namespace. |
| 22 | + |
| 23 | +CUE is usually more clear and concise than the equivalent OpenAPI. However, |
| 24 | +given that they meet different needs for different types of users, CUE's |
| 25 | +ability to round-trip between CUE and OpenAPI's data schema subset acts as a |
| 26 | +useful bridge between their two worlds. |
| 27 | + |
| 28 | +## Reading and writing OpenAPI with the `cue` command |
| 29 | + |
| 30 | +The `cue` command can convert CUE schemas into OpenAPI's `components.schemas`. |
| 31 | +CUE files can be converted into OpenAPI so long as they only specify |
| 32 | +definitions and metadata (`info`, `$version`, etc) at their top-level. |
| 33 | + |
| 34 | +Let's start with a trivial CUE schema that we want to convert to OpenAPI: |
| 35 | + |
| 36 | +{{{with upload "en" "schema.cue"}}} |
| 37 | +-- schema.cue -- |
| 38 | +// A schema for the pet API. |
| 39 | +package api |
| 40 | + |
| 41 | +$version: "v1.2.3" |
| 42 | +// A Pet is a pet that we handle. |
| 43 | +#Pet: { |
| 44 | + // A pet has a name. |
| 45 | + name!: string |
| 46 | + // We only handle certain kinds of pets. |
| 47 | + kind!: #Kind |
| 48 | + // Centenarian pets are not handled. |
| 49 | + age?: uint & <100 // TODO: increase limit if the tortoise proposal is accepted. |
| 50 | + ... |
| 51 | +} |
| 52 | + |
| 53 | +// Kind encodes the different pets we handle. |
| 54 | +#Kind: "cat" | "dog" | "goldfish" |
| 55 | +{{{end}}} |
| 56 | + |
| 57 | +The [`cue def`]({{< relref "docs/reference/cli/cue-def" >}}) command normalizes |
| 58 | +the schema, and optionally converts it into another format: |
| 59 | + |
| 60 | +{{{with script "en" "cue def schema.cue"}}} |
| 61 | +cue def schema.cue -o api.pet.yaml --out openapi+yaml |
| 62 | +{{{end}}} |
| 63 | + |
| 64 | +{{{with _script_ "en" "HIDDEN_ move api.pet.yaml sideways"}}} |
| 65 | +mv api.pet.yaml .api.pet.yaml |
| 66 | +{{{end}}} |
| 67 | + |
| 68 | +The OpenAPI `info.title` field can be extracted from the top-level CUE comment, |
| 69 | +or can be specified directly. |
| 70 | +The same goes for OpenAPI's `info.version` field, which is extracted from CUE's |
| 71 | +top-level `$version` field if not specified directly. |
| 72 | + |
| 73 | +Be aware of just how *long* an equivalent OpenAPI definition can become - not |
| 74 | +all formats possess CUE's succinctness and economy of expression! |
| 75 | +The `cue def` command creates this file: |
| 76 | + |
| 77 | +{{{with upload "en" "api.pet.yaml"}}} |
| 78 | +-- api.pet.yaml -- |
| 79 | +openapi: 3.0.0 |
| 80 | +info: |
| 81 | + title: A schema for the pet API. |
| 82 | + version: v1.2.3 |
| 83 | +paths: {} |
| 84 | +components: |
| 85 | + schemas: |
| 86 | + Kind: |
| 87 | + description: Kind encodes the different pets we handle. |
| 88 | + type: string |
| 89 | + enum: |
| 90 | + - cat |
| 91 | + - dog |
| 92 | + - goldfish |
| 93 | + Pet: |
| 94 | + description: A Pet is a pet that we handle. |
| 95 | + type: object |
| 96 | + required: |
| 97 | + - name |
| 98 | + - kind |
| 99 | + properties: |
| 100 | + name: |
| 101 | + description: A pet has a name. |
| 102 | + type: string |
| 103 | + kind: |
| 104 | + $ref: '#/components/schemas/Kind' |
| 105 | + age: |
| 106 | + description: Centenarian pets are not handled. |
| 107 | + type: integer |
| 108 | + minimum: 0 |
| 109 | + maximum: 100 |
| 110 | + exclusiveMaximum: true |
| 111 | +{{{end}}} |
| 112 | + |
| 113 | +{{{with _script_ "en" "HIDDEN_ diff api.pet.yaml"}}} |
| 114 | +diff -wu api.pet.yaml .api.pet.yaml |
| 115 | +rm .api.pet.yaml # tidy up |
| 116 | +{{{end}}} |
| 117 | + |
| 118 | +Because CUE is more expressive than OpenAPI, it isn't possible to generate a |
| 119 | +precise OpenAPI equivalent for *every* CUE schema. CUE does the best conversion |
| 120 | +it can, limited by what OpenAPI's data schemas can represent. |
| 121 | + |
| 122 | +[`cue import`]({{< relref "docs/reference/cli/cue-import" >}}) can perform the |
| 123 | +reverse operation, taking the OpenAPI definition emitted above and converting |
| 124 | +it back to CUE: |
| 125 | + |
| 126 | +{{{with script "en" "import openapi yaml"}}} |
| 127 | +cue import -p api api.pet.yaml |
| 128 | +{{{end}}} |
| 129 | + |
| 130 | +{{{with _script_ "en" "HIDDEN_ move api.pet.cue sideways"}}} |
| 131 | +mv api.pet.cue .api.pet.cue |
| 132 | +{{{end}}} |
| 133 | + |
| 134 | +This produces the following CUE, which is as close to the original `schema.cue` |
| 135 | +as OpenAPI's capabilities currently permit: |
| 136 | + |
| 137 | +{{{with upload "en" "api.pet.cue"}}} |
| 138 | +-- api.pet.cue -- |
| 139 | +// A schema for the pet API. |
| 140 | +package api |
| 141 | + |
| 142 | +info: { |
| 143 | + title: *"A schema for the pet API." | string |
| 144 | + version: *"v1.2.3" | string |
| 145 | +} |
| 146 | +// Kind encodes the different pets we handle. |
| 147 | +#Kind: "cat" | "dog" | "goldfish" |
| 148 | + |
| 149 | +// A Pet is a pet that we handle. |
| 150 | +#Pet: { |
| 151 | + // A pet has a name. |
| 152 | + name: string |
| 153 | + kind: #Kind |
| 154 | + |
| 155 | + // Centenarian pets are not handled. |
| 156 | + age?: int & >=0 & <100 |
| 157 | + ... |
| 158 | +} |
| 159 | +{{{end}}} |
| 160 | + |
| 161 | +{{{with _script_ "en" "HIDDEN_ diff api.pet.cue"}}} |
| 162 | +diff -wu api.pet.cue .api.pet.cue |
| 163 | +rm .api.pet.cue # tidy up |
| 164 | +{{{end}}} |
| 165 | + |
| 166 | +## Using CUE's Go API |
| 167 | + |
| 168 | +CUE can also generate OpenAPI through its Go API. |
| 169 | + |
| 170 | +Generating an OpenAPI definition can be as simple as this: |
| 171 | + |
| 172 | +{{{with _script_ "en" "go mod init"}}} |
| 173 | +go mod init mod.example |
| 174 | +{{{end}}} |
| 175 | + |
| 176 | +{{{with upload "en" "go emit openapi main"}}} |
| 177 | +-- main.go -- |
| 178 | +package main |
| 179 | + |
| 180 | +import ( |
| 181 | + "bytes" |
| 182 | + "encoding/json" |
| 183 | + "fmt" |
| 184 | + "log" |
| 185 | + |
| 186 | + "cuelang.org/go/cue/cuecontext" |
| 187 | + "cuelang.org/go/cue/load" |
| 188 | + "cuelang.org/go/encoding/openapi" |
| 189 | +) |
| 190 | + |
| 191 | +func main() { |
| 192 | + ctx := cuecontext.New() |
| 193 | + insts := load.Instances([]string{"schema.cue"}, nil) |
| 194 | + v := ctx.BuildInstance(insts[0]) |
| 195 | + |
| 196 | + // Generate the OpenAPI schema from the value loaded from schema.cue |
| 197 | + b, err := openapi.Gen(v, nil) |
| 198 | + if err != nil { |
| 199 | + log.Fatal(err) |
| 200 | + } |
| 201 | + |
| 202 | + // Render as indented JSON |
| 203 | + var out bytes.Buffer |
| 204 | + if err := json.Indent(&out, b, "", " "); err != nil { |
| 205 | + log.Fatal(err) |
| 206 | + } |
| 207 | + fmt.Printf("%s\n", out.Bytes()) |
| 208 | +} |
| 209 | +{{{end}}} |
| 210 | + |
| 211 | +{{{with _script_ "en" "go mod tidy"}}} |
| 212 | +#ellipsis 0 |
| 213 | +go get cuelang.org/go@${CUELANG_CUE_LATEST} |
| 214 | +#ellipsis 0 |
| 215 | +go mod tidy |
| 216 | +{{{end}}} |
| 217 | + |
| 218 | +{{{with script "en" "go run"}}} |
| 219 | +#ellipsis 10 |
| 220 | +go run . |
| 221 | +{{{end}}} |
| 222 | + |
| 223 | +The [`encoding/openapi`](https://pkg.go.dev/cuelang.org/go/encoding/openapi) |
| 224 | +package provides options to make a definition self-contained, to filter |
| 225 | +constraints, and so on. The *expanding references* option enables the |
| 226 | +"Structural OpenAPI" form required by CRDs targeting Kubernetes version 1.15 |
| 227 | +and later. |
| 228 | + |
| 229 | +## Related content |
| 230 | + |
| 231 | +- CUE supports OpenAPI's `components.schemas` namespace, and metadata such as the `info` field -- |
| 232 | + {{<issue 3133/>}} tracks the support of other namespaces defined by the OpenAPI standard |
| 233 | +- The [OpenAPI 3.0.0 specification](https://github.com/OAI/OpenAPI-Specification/tree/3.0.0) |
| 234 | +- {{< linkto/related/reference "cli/cue-def" >}} |
| 235 | +- {{< linkto/related/reference "cli/cue-import" >}} |
| 236 | +- The [`encoding/openapi`](https://pkg.go.dev/cuelang.org/go/encoding/openapi) package |
0 commit comments