microsoft/typespec
Publicmirrored from https://github.com/microsoft/typespecAvailable
packages/openapi3/test/test-openapi-output.ts
940lines · modecode
| 1 | import { expectDiagnostics } from "@cadl-lang/compiler/testing"; |
| 2 | import { deepStrictEqual, ok, strictEqual } from "assert"; |
| 3 | import { createOpenAPITestRunner, oapiForModel, openApiFor } from "./test-host.js"; |
| 4 | |
| 5 | describe("openapi3: definitions", () => { |
| 6 | it("defines models", async () => { |
| 7 | const res = await oapiForModel( |
| 8 | "Foo", |
| 9 | `model Foo { |
| 10 | x: int32; |
| 11 | };` |
| 12 | ); |
| 13 | |
| 14 | ok(res.isRef); |
| 15 | deepStrictEqual(res.schemas.Foo, { |
| 16 | type: "object", |
| 17 | properties: { |
| 18 | x: { type: "integer", format: "int32" }, |
| 19 | }, |
| 20 | required: ["x"], |
| 21 | }); |
| 22 | }); |
| 23 | |
| 24 | it("doesn't define anonymous or unconnected models", async () => { |
| 25 | const res = await oapiForModel( |
| 26 | "{ ... Foo }", |
| 27 | `model Foo { |
| 28 | x: int32; |
| 29 | };` |
| 30 | ); |
| 31 | |
| 32 | ok(!res.isRef); |
| 33 | strictEqual(Object.keys(res.schemas).length, 0); |
| 34 | deepStrictEqual(res.useSchema, { |
| 35 | type: "object", |
| 36 | properties: { |
| 37 | x: { type: "integer", format: "int32" }, |
| 38 | }, |
| 39 | required: ["x"], |
| 40 | "x-cadl-name": "(anonymous model)", |
| 41 | }); |
| 42 | }); |
| 43 | |
| 44 | it("defines templated models", async () => { |
| 45 | const res = await oapiForModel( |
| 46 | "Foo<int32>", |
| 47 | `model Foo<T> { |
| 48 | x: T; |
| 49 | };` |
| 50 | ); |
| 51 | |
| 52 | ok(res.isRef); |
| 53 | ok(res.schemas.Foo_int32, "expected definition named Foo_int32"); |
| 54 | deepStrictEqual(res.schemas.Foo_int32, { |
| 55 | type: "object", |
| 56 | properties: { |
| 57 | x: { type: "integer", format: "int32" }, |
| 58 | }, |
| 59 | required: ["x"], |
| 60 | }); |
| 61 | }); |
| 62 | |
| 63 | it("defines templated models when template param is in a namespace", async () => { |
| 64 | const res = await oapiForModel( |
| 65 | "Foo<Test.M>", |
| 66 | ` |
| 67 | namespace Test { |
| 68 | model M {} |
| 69 | } |
| 70 | model Foo<T> { |
| 71 | x: T; |
| 72 | };` |
| 73 | ); |
| 74 | |
| 75 | ok(res.isRef); |
| 76 | ok(res.schemas["Foo_Test.M"], "expected definition named Foo_Test.M"); |
| 77 | deepStrictEqual(res.schemas["Foo_Test.M"], { |
| 78 | type: "object", |
| 79 | properties: { |
| 80 | x: { $ref: "#/components/schemas/Test.M" }, |
| 81 | }, |
| 82 | required: ["x"], |
| 83 | }); |
| 84 | }); |
| 85 | |
| 86 | it("defines models extended from models", async () => { |
| 87 | const res = await oapiForModel( |
| 88 | "Bar", |
| 89 | ` |
| 90 | model Foo { |
| 91 | y: int32; |
| 92 | }; |
| 93 | model Bar extends Foo {}` |
| 94 | ); |
| 95 | |
| 96 | ok(res.isRef); |
| 97 | ok(res.schemas.Foo, "expected definition named Foo"); |
| 98 | ok(res.schemas.Bar, "expected definition named Bar"); |
| 99 | deepStrictEqual(res.schemas.Bar, { |
| 100 | type: "object", |
| 101 | properties: {}, |
| 102 | allOf: [{ $ref: "#/components/schemas/Foo" }], |
| 103 | }); |
| 104 | |
| 105 | deepStrictEqual(res.schemas.Foo, { |
| 106 | type: "object", |
| 107 | properties: { y: { type: "integer", format: "int32" } }, |
| 108 | required: ["y"], |
| 109 | }); |
| 110 | }); |
| 111 | |
| 112 | it("emits models extended from models when parent is emitted", async () => { |
| 113 | const res = await openApiFor( |
| 114 | ` |
| 115 | model Parent { |
| 116 | x?: int32; |
| 117 | }; |
| 118 | model Child extends Parent { |
| 119 | y?: int32; |
| 120 | } |
| 121 | namespace Test { |
| 122 | @route("/") op test(): Parent; |
| 123 | } |
| 124 | ` |
| 125 | ); |
| 126 | deepStrictEqual(res.components.schemas.Parent, { |
| 127 | type: "object", |
| 128 | properties: { x: { type: "integer", format: "int32" } }, |
| 129 | }); |
| 130 | deepStrictEqual(res.components.schemas.Child, { |
| 131 | type: "object", |
| 132 | allOf: [{ $ref: "#/components/schemas/Parent" }], |
| 133 | properties: { y: { type: "integer", format: "int32" } }, |
| 134 | }); |
| 135 | }); |
| 136 | |
| 137 | it("ignore uninstantiated template types", async () => { |
| 138 | const res = await openApiFor( |
| 139 | ` |
| 140 | model Parent { |
| 141 | x?: int32; |
| 142 | }; |
| 143 | model TParent<T> extends Parent { |
| 144 | t: T; |
| 145 | } |
| 146 | model Child extends TParent<string> { |
| 147 | y?: int32; |
| 148 | } |
| 149 | namespace Test { |
| 150 | @route("/") op test(): Parent; |
| 151 | } |
| 152 | ` |
| 153 | ); |
| 154 | ok( |
| 155 | !("TParent" in res.components.schemas), |
| 156 | "Parent templated type shouldn't be includd in OpenAPI" |
| 157 | ); |
| 158 | deepStrictEqual(res.components.schemas.Parent, { |
| 159 | type: "object", |
| 160 | properties: { x: { type: "integer", format: "int32" } }, |
| 161 | }); |
| 162 | deepStrictEqual(res.components.schemas.TParent_string, { |
| 163 | type: "object", |
| 164 | properties: { t: { type: "string" } }, |
| 165 | required: ["t"], |
| 166 | allOf: [{ $ref: "#/components/schemas/Parent" }], |
| 167 | }); |
| 168 | deepStrictEqual(res.components.schemas.Child, { |
| 169 | type: "object", |
| 170 | allOf: [{ $ref: "#/components/schemas/TParent_string" }], |
| 171 | properties: { y: { type: "integer", format: "int32" } }, |
| 172 | }); |
| 173 | }); |
| 174 | |
| 175 | it("shouldn't emit instantiated template child types that are only used in is", async () => { |
| 176 | const res = await openApiFor( |
| 177 | ` |
| 178 | model Parent { |
| 179 | x?: int32; |
| 180 | }; |
| 181 | model TParent<T> extends Parent { |
| 182 | t: T; |
| 183 | } |
| 184 | model Child is TParent<string> { |
| 185 | y?: int32; |
| 186 | } |
| 187 | namespace Test { |
| 188 | @route("/") op test(): Parent; |
| 189 | } |
| 190 | ` |
| 191 | ); |
| 192 | ok( |
| 193 | !("TParent_string" in res.components.schemas), |
| 194 | "Parent instantiated templated type shouldn't be includd in OpenAPI" |
| 195 | ); |
| 196 | }); |
| 197 | |
| 198 | it("defines models with properties extended from models", async () => { |
| 199 | const res = await oapiForModel( |
| 200 | "Bar", |
| 201 | ` |
| 202 | model Foo { |
| 203 | y: int32; |
| 204 | }; |
| 205 | model Bar extends Foo { |
| 206 | x: int32; |
| 207 | }` |
| 208 | ); |
| 209 | |
| 210 | ok(res.isRef); |
| 211 | ok(res.schemas.Foo, "expected definition named Foo"); |
| 212 | ok(res.schemas.Bar, "expected definition named Bar"); |
| 213 | deepStrictEqual(res.schemas.Bar, { |
| 214 | type: "object", |
| 215 | properties: { x: { type: "integer", format: "int32" } }, |
| 216 | allOf: [{ $ref: "#/components/schemas/Foo" }], |
| 217 | required: ["x"], |
| 218 | }); |
| 219 | |
| 220 | deepStrictEqual(res.schemas.Foo, { |
| 221 | type: "object", |
| 222 | properties: { y: { type: "integer", format: "int32" } }, |
| 223 | required: ["y"], |
| 224 | }); |
| 225 | }); |
| 226 | |
| 227 | it("defines models extended from templated models", async () => { |
| 228 | const res = await oapiForModel( |
| 229 | "Bar", |
| 230 | ` |
| 231 | model Foo<T> { |
| 232 | y: T; |
| 233 | }; |
| 234 | model Bar extends Foo<int32> {}` |
| 235 | ); |
| 236 | |
| 237 | ok(res.isRef); |
| 238 | ok(res.schemas["Foo_int32"] === undefined, "no definition named Foo_int32"); |
| 239 | ok(res.schemas.Bar, "expected definition named Bar"); |
| 240 | deepStrictEqual(res.schemas.Bar, { |
| 241 | type: "object", |
| 242 | properties: { y: { type: "integer", format: "int32" } }, |
| 243 | required: ["y"], |
| 244 | }); |
| 245 | }); |
| 246 | |
| 247 | it("defines models with properties extended from templated models", async () => { |
| 248 | const res = await oapiForModel( |
| 249 | "Bar", |
| 250 | ` |
| 251 | model Foo<T> { |
| 252 | y: T; |
| 253 | }; |
| 254 | model Bar extends Foo<int32> { |
| 255 | x: int32 |
| 256 | }` |
| 257 | ); |
| 258 | |
| 259 | ok(res.isRef); |
| 260 | ok(res.schemas.Foo_int32, "expected definition named Foo_int32"); |
| 261 | ok(res.schemas.Bar, "expected definition named Bar"); |
| 262 | deepStrictEqual(res.schemas.Bar, { |
| 263 | type: "object", |
| 264 | properties: { x: { type: "integer", format: "int32" } }, |
| 265 | allOf: [{ $ref: "#/components/schemas/Foo_int32" }], |
| 266 | required: ["x"], |
| 267 | }); |
| 268 | |
| 269 | deepStrictEqual(res.schemas.Foo_int32, { |
| 270 | type: "object", |
| 271 | properties: { y: { type: "integer", format: "int32" } }, |
| 272 | required: ["y"], |
| 273 | }); |
| 274 | }); |
| 275 | |
| 276 | it("defines templated models with properties extended from templated models", async () => { |
| 277 | const res = await oapiForModel( |
| 278 | "Bar<int32>", |
| 279 | ` |
| 280 | model Foo<T> { |
| 281 | y: T; |
| 282 | }; |
| 283 | model Bar<T> extends Foo<T> { |
| 284 | x: T |
| 285 | }` |
| 286 | ); |
| 287 | |
| 288 | ok(res.isRef); |
| 289 | ok(res.schemas.Foo_int32, "expected definition named Foo_int32"); |
| 290 | ok(res.schemas.Bar_int32, "expected definition named Bar_int32"); |
| 291 | deepStrictEqual(res.schemas.Bar_int32, { |
| 292 | type: "object", |
| 293 | properties: { x: { type: "integer", format: "int32" } }, |
| 294 | allOf: [{ $ref: "#/components/schemas/Foo_int32" }], |
| 295 | required: ["x"], |
| 296 | }); |
| 297 | |
| 298 | deepStrictEqual(res.schemas.Foo_int32, { |
| 299 | type: "object", |
| 300 | properties: { y: { type: "integer", format: "int32" } }, |
| 301 | required: ["y"], |
| 302 | }); |
| 303 | }); |
| 304 | |
| 305 | it("defines models with no properties extended", async () => { |
| 306 | const res = await oapiForModel( |
| 307 | "Bar", |
| 308 | ` |
| 309 | model Foo {}; |
| 310 | model Bar extends Foo {};` |
| 311 | ); |
| 312 | |
| 313 | ok(res.isRef); |
| 314 | ok(res.schemas.Foo, "expected definition named Foo"); |
| 315 | ok(res.schemas.Bar, "expected definition named Bar"); |
| 316 | deepStrictEqual(res.schemas.Bar, { |
| 317 | type: "object", |
| 318 | properties: {}, |
| 319 | allOf: [{ $ref: "#/components/schemas/Foo" }], |
| 320 | }); |
| 321 | |
| 322 | deepStrictEqual(res.schemas.Foo, { |
| 323 | type: "object", |
| 324 | properties: {}, |
| 325 | }); |
| 326 | }); |
| 327 | |
| 328 | it("defines models with no properties extended twice", async () => { |
| 329 | const res = await oapiForModel( |
| 330 | "Baz", |
| 331 | ` |
| 332 | model Foo { x: int32 }; |
| 333 | model Bar extends Foo {}; |
| 334 | model Baz extends Bar {};` |
| 335 | ); |
| 336 | |
| 337 | ok(res.isRef); |
| 338 | ok(res.schemas.Foo, "expected definition named Foo"); |
| 339 | ok(res.schemas.Bar, "expected definition named Bar"); |
| 340 | ok(res.schemas.Baz, "expected definition named Baz"); |
| 341 | deepStrictEqual(res.schemas.Baz, { |
| 342 | type: "object", |
| 343 | properties: {}, |
| 344 | allOf: [{ $ref: "#/components/schemas/Bar" }], |
| 345 | }); |
| 346 | |
| 347 | deepStrictEqual(res.schemas.Bar, { |
| 348 | type: "object", |
| 349 | properties: {}, |
| 350 | allOf: [{ $ref: "#/components/schemas/Foo" }], |
| 351 | }); |
| 352 | |
| 353 | deepStrictEqual(res.schemas.Foo, { |
| 354 | type: "object", |
| 355 | properties: { |
| 356 | x: { |
| 357 | format: "int32", |
| 358 | type: "integer", |
| 359 | }, |
| 360 | }, |
| 361 | required: ["x"], |
| 362 | }); |
| 363 | }); |
| 364 | |
| 365 | it("defines enum types", async () => { |
| 366 | const res = await oapiForModel( |
| 367 | "Pet", |
| 368 | ` |
| 369 | enum PetType { |
| 370 | Dog, Cat |
| 371 | } |
| 372 | model Pet { type: PetType }; |
| 373 | ` |
| 374 | ); |
| 375 | ok(res.isRef); |
| 376 | strictEqual(res.schemas.Pet.properties.type.$ref, "#/components/schemas/PetType"); |
| 377 | deepStrictEqual(res.schemas.PetType.enum, ["Dog", "Cat"]); |
| 378 | }); |
| 379 | |
| 380 | it("defines enum types with custom values", async () => { |
| 381 | const res = await oapiForModel( |
| 382 | "Pet", |
| 383 | ` |
| 384 | enum PetType { |
| 385 | Dog: 0, Cat: 1 |
| 386 | } |
| 387 | model Pet { type: PetType }; |
| 388 | ` |
| 389 | ); |
| 390 | ok(res.isRef); |
| 391 | strictEqual(res.schemas.Pet.properties.type.$ref, "#/components/schemas/PetType"); |
| 392 | deepStrictEqual(res.schemas.PetType.enum, [0, 1]); |
| 393 | }); |
| 394 | |
| 395 | it("defines known values", async () => { |
| 396 | const res = await oapiForModel( |
| 397 | "Pet", |
| 398 | ` |
| 399 | enum KnownPetType { |
| 400 | Dog, Cat |
| 401 | } |
| 402 | |
| 403 | @knownValues(KnownPetType) |
| 404 | model PetType is string {} |
| 405 | model Pet { type: PetType }; |
| 406 | ` |
| 407 | ); |
| 408 | ok(res.isRef); |
| 409 | strictEqual(res.schemas.Pet.properties.type.$ref, "#/components/schemas/PetType"); |
| 410 | deepStrictEqual(res.schemas.PetType, { |
| 411 | oneOf: [{ type: "string" }, { type: "string", enum: ["Dog", "Cat"] }], |
| 412 | }); |
| 413 | }); |
| 414 | |
| 415 | it("defines nullable properties", async () => { |
| 416 | const res = await oapiForModel( |
| 417 | "Pet", |
| 418 | ` |
| 419 | model Pet { |
| 420 | name: string | null; |
| 421 | }; |
| 422 | ` |
| 423 | ); |
| 424 | ok(res.isRef); |
| 425 | deepStrictEqual(res.schemas.Pet, { |
| 426 | type: "object", |
| 427 | properties: { |
| 428 | name: { |
| 429 | type: "string", |
| 430 | nullable: true, |
| 431 | "x-cadl-name": "Cadl.string | Cadl.null", |
| 432 | }, |
| 433 | }, |
| 434 | required: ["name"], |
| 435 | }); |
| 436 | }); |
| 437 | |
| 438 | it("defines nullable array", async () => { |
| 439 | const res = await oapiForModel( |
| 440 | "Pet", |
| 441 | ` |
| 442 | model Pet { |
| 443 | name: int32[] | null; |
| 444 | }; |
| 445 | ` |
| 446 | ); |
| 447 | ok(res.isRef); |
| 448 | deepStrictEqual(res.schemas.Pet, { |
| 449 | type: "object", |
| 450 | properties: { |
| 451 | name: { |
| 452 | type: "array", |
| 453 | items: { |
| 454 | type: "integer", |
| 455 | format: "int32", |
| 456 | }, |
| 457 | nullable: true, |
| 458 | "x-cadl-name": "Cadl.int32[] | Cadl.null", |
| 459 | }, |
| 460 | }, |
| 461 | required: ["name"], |
| 462 | }); |
| 463 | }); |
| 464 | |
| 465 | it("defines enums with a nullable variant", async () => { |
| 466 | const res = await oapiForModel( |
| 467 | "Pet", |
| 468 | ` |
| 469 | model Pet { |
| 470 | type: "cat" | "dog" | null; |
| 471 | }; |
| 472 | ` |
| 473 | ); |
| 474 | ok(res.isRef); |
| 475 | deepStrictEqual(res.schemas.Pet, { |
| 476 | type: "object", |
| 477 | properties: { |
| 478 | type: { |
| 479 | type: "string", |
| 480 | enum: ["cat", "dog"], |
| 481 | nullable: true, |
| 482 | "x-cadl-name": "cat | dog | Cadl.null", |
| 483 | }, |
| 484 | }, |
| 485 | required: ["type"], |
| 486 | }); |
| 487 | }); |
| 488 | |
| 489 | it("throws diagnostics for empty enum definitions", async () => { |
| 490 | const runner = await createOpenAPITestRunner(); |
| 491 | |
| 492 | const diagnostics = await runner.diagnose(` |
| 493 | enum PetType { |
| 494 | } |
| 495 | model Pet { type: PetType }; |
| 496 | @route("/") |
| 497 | namespace root { |
| 498 | op read(): Pet; |
| 499 | } |
| 500 | `); |
| 501 | |
| 502 | expectDiagnostics(diagnostics, { |
| 503 | code: "@cadl-lang/openapi3/union-unsupported", |
| 504 | message: |
| 505 | "Empty unions are not supported for OpenAPI v3 - enums must have at least one value.", |
| 506 | }); |
| 507 | }); |
| 508 | |
| 509 | it("defines request bodies as unions of models", async () => { |
| 510 | const openApi = await openApiFor(` |
| 511 | model Cat { |
| 512 | meow: int32; |
| 513 | } |
| 514 | model Dog { |
| 515 | bark: string; |
| 516 | } |
| 517 | @route("/") |
| 518 | namespace root { |
| 519 | @post op create(@body body: Cat | Dog): { ...Response<200> }; |
| 520 | } |
| 521 | `); |
| 522 | ok(openApi.components.schemas.Cat, "expected definition named Cat"); |
| 523 | ok(openApi.components.schemas.Dog, "expected definition named Dog"); |
| 524 | deepStrictEqual(openApi.paths["/"].post.requestBody.content["application/json"].schema, { |
| 525 | "x-cadl-name": "Cat | Dog", |
| 526 | anyOf: [{ $ref: "#/components/schemas/Cat" }, { $ref: "#/components/schemas/Dog" }], |
| 527 | }); |
| 528 | }); |
| 529 | |
| 530 | it("defines request bodies as unions of model and non-model types", async () => { |
| 531 | const openApi = await openApiFor(` |
| 532 | model Cat { |
| 533 | meow: int32; |
| 534 | } |
| 535 | @route("/") |
| 536 | namespace root { |
| 537 | @post op create(@body body: Cat | string): { ...Response<200> }; |
| 538 | } |
| 539 | `); |
| 540 | ok(openApi.components.schemas.Cat, "expected definition named Cat"); |
| 541 | deepStrictEqual(openApi.paths["/"].post.requestBody.content["application/json"].schema, { |
| 542 | "x-cadl-name": "Cat | Cadl.string", |
| 543 | anyOf: [{ $ref: "#/components/schemas/Cat" }, { type: "string" }], |
| 544 | }); |
| 545 | }); |
| 546 | |
| 547 | it("defines request bodies aliased to a union of models", async () => { |
| 548 | const openApi = await openApiFor(` |
| 549 | model Cat { |
| 550 | meow: int32; |
| 551 | } |
| 552 | model Dog { |
| 553 | bark: string; |
| 554 | } |
| 555 | alias Pet = Cat | Dog; |
| 556 | @route("/") |
| 557 | namespace root { |
| 558 | @post op create(@body body: Pet): { ...Response<200> }; |
| 559 | } |
| 560 | `); |
| 561 | ok(openApi.components.schemas.Cat, "expected definition named Cat"); |
| 562 | ok(openApi.components.schemas.Dog, "expected definition named Dog"); |
| 563 | deepStrictEqual(openApi.paths["/"].post.requestBody.content["application/json"].schema, { |
| 564 | "x-cadl-name": "Cat | Dog", |
| 565 | anyOf: [{ $ref: "#/components/schemas/Cat" }, { $ref: "#/components/schemas/Dog" }], |
| 566 | }); |
| 567 | }); |
| 568 | |
| 569 | it("defines response bodies as unions of models", async () => { |
| 570 | const openApi = await openApiFor(` |
| 571 | model Cat { |
| 572 | meow: int32; |
| 573 | } |
| 574 | model Dog { |
| 575 | bark: string; |
| 576 | } |
| 577 | @route("/") |
| 578 | namespace root { |
| 579 | op read(): { @body body: Cat | Dog }; |
| 580 | } |
| 581 | `); |
| 582 | ok(openApi.components.schemas.Cat, "expected definition named Cat"); |
| 583 | ok(openApi.components.schemas.Dog, "expected definition named Dog"); |
| 584 | deepStrictEqual(openApi.paths["/"].get.responses["200"].content["application/json"].schema, { |
| 585 | "x-cadl-name": "Cat | Dog", |
| 586 | anyOf: [{ $ref: "#/components/schemas/Cat" }, { $ref: "#/components/schemas/Dog" }], |
| 587 | }); |
| 588 | }); |
| 589 | |
| 590 | it("defines response bodies as unions of model and non-model types", async () => { |
| 591 | const openApi = await openApiFor(` |
| 592 | model Cat { |
| 593 | meow: int32; |
| 594 | } |
| 595 | @route("/") |
| 596 | namespace root { |
| 597 | op read(): { @body body: Cat | string }; |
| 598 | } |
| 599 | `); |
| 600 | ok(openApi.components.schemas.Cat, "expected definition named Cat"); |
| 601 | deepStrictEqual(openApi.paths["/"].get.responses["200"].content["application/json"].schema, { |
| 602 | "x-cadl-name": "Cat | Cadl.string", |
| 603 | anyOf: [{ $ref: "#/components/schemas/Cat" }, { type: "string" }], |
| 604 | }); |
| 605 | }); |
| 606 | |
| 607 | it("defines response bodies aliased to a union from models", async () => { |
| 608 | const openApi = await openApiFor(` |
| 609 | model Cat { |
| 610 | meow: int32; |
| 611 | } |
| 612 | model Dog { |
| 613 | bark: string; |
| 614 | } |
| 615 | alias Pet = Cat | Dog; |
| 616 | @route("/") |
| 617 | namespace root { |
| 618 | op read(): { @body body: Pet }; |
| 619 | } |
| 620 | `); |
| 621 | ok(openApi.components.schemas.Cat, "expected definition named Cat"); |
| 622 | ok(openApi.components.schemas.Dog, "expected definition named Dog"); |
| 623 | deepStrictEqual(openApi.paths["/"].get.responses["200"].content["application/json"].schema, { |
| 624 | "x-cadl-name": "Cat | Dog", |
| 625 | anyOf: [{ $ref: "#/components/schemas/Cat" }, { $ref: "#/components/schemas/Dog" }], |
| 626 | }); |
| 627 | }); |
| 628 | |
| 629 | it("defines response bodies unioned in OkResponse as unions of models", async () => { |
| 630 | const openApi = await openApiFor(` |
| 631 | model Cat { |
| 632 | meow: int32; |
| 633 | } |
| 634 | model Dog { |
| 635 | bark: string; |
| 636 | } |
| 637 | @route("/") |
| 638 | namespace root { |
| 639 | op read(): OkResponse<Cat | Dog>; |
| 640 | } |
| 641 | `); |
| 642 | ok(openApi.components.schemas.Cat, "expected definition named Cat"); |
| 643 | ok(openApi.components.schemas.Dog, "expected definition named Dog"); |
| 644 | deepStrictEqual(openApi.paths["/"].get.responses["200"].content["application/json"].schema, { |
| 645 | "x-cadl-name": "Cat | Dog", |
| 646 | anyOf: [{ $ref: "#/components/schemas/Cat" }, { $ref: "#/components/schemas/Dog" }], |
| 647 | }); |
| 648 | }); |
| 649 | |
| 650 | it("defines unions with named variants similarly to unnamed unions (it ignores variant names)", async () => { |
| 651 | const openApi = await openApiFor(` |
| 652 | model Cat { |
| 653 | meow: int32; |
| 654 | } |
| 655 | model Dog { |
| 656 | bark: string; |
| 657 | } |
| 658 | union Pet { cat: Cat, dog: Dog } |
| 659 | @route("/") |
| 660 | namespace root { |
| 661 | op read(): { @body body: Pet }; |
| 662 | } |
| 663 | `); |
| 664 | ok(openApi.components.schemas.Cat, "expected definition named Cat"); |
| 665 | ok(openApi.components.schemas.Dog, "expected definition named Dog"); |
| 666 | ok(openApi.components.schemas.Pet, "expected definition named Pet"); |
| 667 | deepStrictEqual(openApi.components.schemas.Pet, { |
| 668 | anyOf: [{ $ref: "#/components/schemas/Cat" }, { $ref: "#/components/schemas/Dog" }], |
| 669 | }); |
| 670 | deepStrictEqual(openApi.paths["/"].get.responses["200"].content["application/json"].schema, { |
| 671 | $ref: "#/components/schemas/Pet", |
| 672 | }); |
| 673 | }); |
| 674 | |
| 675 | it("defines oneOf schema for unions with @oneOf decorator", async () => { |
| 676 | const openApi = await openApiFor(` |
| 677 | model Cat { |
| 678 | meow: int32; |
| 679 | } |
| 680 | model Dog { |
| 681 | bark: string; |
| 682 | } |
| 683 | @oneOf |
| 684 | union Pet { cat: Cat, dog: Dog } |
| 685 | @route("/") |
| 686 | namespace root { |
| 687 | op read(): { @body body: Pet }; |
| 688 | } |
| 689 | `); |
| 690 | ok(openApi.components.schemas.Cat, "expected definition named Cat"); |
| 691 | ok(openApi.components.schemas.Dog, "expected definition named Dog"); |
| 692 | ok(openApi.components.schemas.Pet, "expected definition named Pet"); |
| 693 | deepStrictEqual(openApi.components.schemas.Pet, { |
| 694 | oneOf: [{ $ref: "#/components/schemas/Cat" }, { $ref: "#/components/schemas/Dog" }], |
| 695 | }); |
| 696 | deepStrictEqual(openApi.paths["/"].get.responses["200"].content["application/json"].schema, { |
| 697 | $ref: "#/components/schemas/Pet", |
| 698 | }); |
| 699 | }); |
| 700 | }); |
| 701 | |
| 702 | describe("openapi3: primitives", () => { |
| 703 | const cases = [ |
| 704 | ["int8", { type: "integer", format: "int8" }], |
| 705 | ["int16", { type: "integer", format: "int16" }], |
| 706 | ["int32", { type: "integer", format: "int32" }], |
| 707 | ["int64", { type: "integer", format: "int64" }], |
| 708 | ["safeint", { type: "integer", format: "int64" }], |
| 709 | ["uint8", { type: "integer", format: "uint8" }], |
| 710 | ["uint16", { type: "integer", format: "uint16" }], |
| 711 | ["uint32", { type: "integer", format: "uint32" }], |
| 712 | ["uint64", { type: "integer", format: "uint64" }], |
| 713 | ["float32", { type: "number", format: "float" }], |
| 714 | ["float64", { type: "number", format: "double" }], |
| 715 | ["string", { type: "string" }], |
| 716 | ["boolean", { type: "boolean" }], |
| 717 | ["plainDate", { type: "string", format: "date" }], |
| 718 | ["zonedDateTime", { type: "string", format: "date-time" }], |
| 719 | ["plainTime", { type: "string", format: "time" }], |
| 720 | ["duration", { type: "string", format: "duration" }], |
| 721 | ["bytes", { type: "string", format: "byte" }], |
| 722 | ]; |
| 723 | |
| 724 | for (const test of cases) { |
| 725 | it("knows schema for " + test[0], async () => { |
| 726 | const res = await oapiForModel( |
| 727 | "Pet", |
| 728 | ` |
| 729 | model Pet { name: ${test[0]} }; |
| 730 | ` |
| 731 | ); |
| 732 | |
| 733 | const schema = res.schemas.Pet.properties.name; |
| 734 | deepStrictEqual(schema, test[1]); |
| 735 | }); |
| 736 | } |
| 737 | }); |
| 738 | |
| 739 | describe("openapi3: literals", () => { |
| 740 | const cases = [ |
| 741 | ["1", { type: "number", enum: [1] }], |
| 742 | ['"hello"', { type: "string", enum: ["hello"] }], |
| 743 | ["false", { type: "boolean", enum: [false] }], |
| 744 | ["true", { type: "boolean", enum: [true] }], |
| 745 | ]; |
| 746 | |
| 747 | for (const test of cases) { |
| 748 | it("knows schema for " + test[0], async () => { |
| 749 | const res = await oapiForModel( |
| 750 | "Pet", |
| 751 | ` |
| 752 | model Pet { name: ${test[0]} }; |
| 753 | ` |
| 754 | ); |
| 755 | |
| 756 | const schema = res.schemas.Pet.properties.name; |
| 757 | deepStrictEqual(schema, test[1]); |
| 758 | }); |
| 759 | } |
| 760 | }); |
| 761 | |
| 762 | describe("openapi3: operations", () => { |
| 763 | it("define operations with param with defaults", async () => { |
| 764 | const res = await openApiFor( |
| 765 | ` |
| 766 | @route("/") |
| 767 | namespace root { |
| 768 | @get() |
| 769 | op read(@query queryWithDefault?: string = "defaultValue"): string; |
| 770 | } |
| 771 | ` |
| 772 | ); |
| 773 | |
| 774 | deepStrictEqual(res.paths["/"].get.parameters[0].schema.default, "defaultValue"); |
| 775 | }); |
| 776 | |
| 777 | it("define operations with param with decorators", async () => { |
| 778 | const res = await openApiFor( |
| 779 | ` |
| 780 | @route("/thing") |
| 781 | namespace root { |
| 782 | @get |
| 783 | @route("{name}") |
| 784 | op getThing( |
| 785 | @pattern("^[a-zA-Z0-9-]{3,24}$") |
| 786 | @path name: string, |
| 787 | |
| 788 | @minValue(1) |
| 789 | @maxValue(10) |
| 790 | @query count: int32 |
| 791 | ): string; |
| 792 | } |
| 793 | ` |
| 794 | ); |
| 795 | |
| 796 | const getThing = res.paths["/thing/{name}"].get; |
| 797 | ok(getThing); |
| 798 | ok(getThing.parameters[0].schema); |
| 799 | ok(getThing.parameters[0].schema.pattern); |
| 800 | strictEqual(getThing.parameters[0].schema.pattern, "^[a-zA-Z0-9-]{3,24}$"); |
| 801 | |
| 802 | ok(getThing.parameters[1].schema); |
| 803 | ok(getThing.parameters[1].schema.minimum); |
| 804 | ok(getThing.parameters[1].schema.maximum); |
| 805 | strictEqual(getThing.parameters[1].schema.minimum, 1); |
| 806 | strictEqual(getThing.parameters[1].schema.maximum, 10); |
| 807 | }); |
| 808 | }); |
| 809 | |
| 810 | describe("openapi3: request", () => { |
| 811 | describe("binary request", () => { |
| 812 | it("bytes request should default to application/json byte", async () => { |
| 813 | const res = await openApiFor( |
| 814 | ` |
| 815 | @route("/") |
| 816 | namespace root { |
| 817 | @post op read(@body body: bytes): {}; |
| 818 | } |
| 819 | ` |
| 820 | ); |
| 821 | |
| 822 | const requestBody = res.paths["/"].post.requestBody; |
| 823 | ok(requestBody); |
| 824 | strictEqual(requestBody.content["application/json"].schema.type, "string"); |
| 825 | strictEqual(requestBody.content["application/json"].schema.format, "byte"); |
| 826 | }); |
| 827 | |
| 828 | it("bytes request should respect @header contentType and use binary format when not json or text", async () => { |
| 829 | const res = await openApiFor( |
| 830 | ` |
| 831 | @route("/") |
| 832 | namespace root { |
| 833 | @post op read(@header contentType: "image/png", @body body: bytes): {}; |
| 834 | } |
| 835 | ` |
| 836 | ); |
| 837 | |
| 838 | const requestBody = res.paths["/"].post.requestBody; |
| 839 | ok(requestBody); |
| 840 | strictEqual(requestBody.content["image/png"].schema.type, "string"); |
| 841 | strictEqual(requestBody.content["image/png"].schema.format, "binary"); |
| 842 | }); |
| 843 | }); |
| 844 | }); |
| 845 | |
| 846 | describe("openapi3: extension decorator", () => { |
| 847 | it("adds an arbitrary extension to a model", async () => { |
| 848 | const oapi = await openApiFor( |
| 849 | ` |
| 850 | @extension("x-model-extension", "foobar") |
| 851 | model Pet { |
| 852 | name: string; |
| 853 | } |
| 854 | @route("/") |
| 855 | namespace root { |
| 856 | @get() |
| 857 | op read(): Pet; |
| 858 | } |
| 859 | ` |
| 860 | ); |
| 861 | ok(oapi.components.schemas.Pet); |
| 862 | strictEqual(oapi.components.schemas.Pet["x-model-extension"], "foobar"); |
| 863 | }); |
| 864 | |
| 865 | it("adds an arbitrary extension to an operation", async () => { |
| 866 | const oapi = await openApiFor( |
| 867 | ` |
| 868 | model Pet { |
| 869 | name: string; |
| 870 | } |
| 871 | @route("/") |
| 872 | namespace root { |
| 873 | @get() |
| 874 | @extension("x-operation-extension", "barbaz") |
| 875 | op list(): Pet[]; |
| 876 | } |
| 877 | ` |
| 878 | ); |
| 879 | ok(oapi.paths["/"].get); |
| 880 | strictEqual(oapi.paths["/"].get["x-operation-extension"], "barbaz"); |
| 881 | }); |
| 882 | |
| 883 | it("adds an arbitrary extension to a parameter", async () => { |
| 884 | const oapi = await openApiFor( |
| 885 | ` |
| 886 | model Pet { |
| 887 | name: string; |
| 888 | } |
| 889 | model PetId { |
| 890 | @path |
| 891 | @extension("x-parameter-extension", "foobaz") |
| 892 | petId: string; |
| 893 | } |
| 894 | @route("/Pets") |
| 895 | namespace root { |
| 896 | @get() |
| 897 | op get(... PetId): Pet; |
| 898 | } |
| 899 | ` |
| 900 | ); |
| 901 | ok(oapi.paths["/Pets/{petId}"].get); |
| 902 | strictEqual( |
| 903 | oapi.paths["/Pets/{petId}"].get.parameters[0]["$ref"], |
| 904 | "#/components/parameters/PetId" |
| 905 | ); |
| 906 | strictEqual(oapi.components.parameters.PetId.name, "petId"); |
| 907 | strictEqual(oapi.components.parameters.PetId["x-parameter-extension"], "foobaz"); |
| 908 | }); |
| 909 | |
| 910 | it("check format and pattern decorator on model", async () => { |
| 911 | const oapi = await openApiFor( |
| 912 | ` |
| 913 | model Pet extends PetId { |
| 914 | @pattern("^[a-zA-Z0-9-]{3,24}$") |
| 915 | name: string; |
| 916 | } |
| 917 | model PetId { |
| 918 | @path |
| 919 | @pattern("^[a-zA-Z0-9-]{3,24}$") |
| 920 | @format("UUID") |
| 921 | petId: string; |
| 922 | } |
| 923 | @route("/Pets") |
| 924 | namespace root { |
| 925 | @get() |
| 926 | op get(... PetId): Pet; |
| 927 | } |
| 928 | ` |
| 929 | ); |
| 930 | ok(oapi.paths["/Pets/{petId}"].get); |
| 931 | strictEqual( |
| 932 | oapi.paths["/Pets/{petId}"].get.parameters[0]["$ref"], |
| 933 | "#/components/parameters/PetId" |
| 934 | ); |
| 935 | strictEqual(oapi.components.parameters.PetId.name, "petId"); |
| 936 | strictEqual(oapi.components.schemas.Pet.properties.name.pattern, "^[a-zA-Z0-9-]{3,24}$"); |
| 937 | strictEqual(oapi.components.parameters.PetId.schema.format, "UUID"); |
| 938 | strictEqual(oapi.components.parameters.PetId.schema.pattern, "^[a-zA-Z0-9-]{3,24}$"); |
| 939 | }); |
| 940 | }); |
| 941 | |