Add emitter for OpenAPI 3.0 (#745)
This commit is contained in:
Родитель
d28a30a7a9
Коммит
0b024d674c
|
@ -400,14 +400,14 @@ The first step in using a library is to install it via `npm`. You can get `npm`
|
|||
|
||||
If you haven't already intiialized your Cadl project's package.json file, now would be a good time to do so. The package.json file lets you track the dependencies your project depends on, and is a best practice to check in along with any Cadl files you create. Run `npm init` create your package.json file.
|
||||
|
||||
Then, in your Cadl project directory, type `npm install libraryName` to install a library. For example, to install the official Cadl REST API bindings and OpenAPI generator, you would type `npm install @cadl-lang/rest @azure-tools/cadl-autorest`.
|
||||
Then, in your Cadl project directory, type `npm install libraryName` to install a library. For example, to install the official Cadl REST API bindings and OpenAPI generator, you would type `npm install @cadl-lang/rest @cadl-lang/openapi3`.
|
||||
|
||||
Lastly, you need to import the libraries into your Cadl program. By convention, all external dependencies are imported in your `main.cadl` file, but can be in any Cadl file imported into your program. Importing the two libraries we installed above would look like this:
|
||||
|
||||
```
|
||||
// in main.cadl
|
||||
import "@cadl-lang/rest";
|
||||
import "@azure-tools/cadl-autorest";
|
||||
import "@cadl-lang/openapi3";
|
||||
```
|
||||
|
||||
#### Creating libraries
|
||||
|
@ -420,9 +420,9 @@ The package.json file for an Cadl library requires one additional field: `cadlMa
|
|||
|
||||
With the language building blocks we've covered so far we're ready to author our first REST API. Cadl has an official REST API "binding" called `@cadl-lang/rest`. It's a set of Cadl declarations and decorators that describe REST APIs and can be used by code generators to generate OpenAPI descriptions, implementation code, and the like.
|
||||
|
||||
Cadl also has an official OpenAPI emitter called `@azure-tools/cadl-autorest` that consumes the REST API bindings and emits standard OpenAPI descriptions. This can then be fed in to any OpenAPI code generation pipeline.
|
||||
Cadl also has an official OpenAPI emitter called `@cadl-lang/openapi3` that consumes the REST API bindings and emits standard OpenAPI descriptions. This can then be fed in to any OpenAPI code generation pipeline.
|
||||
|
||||
The following examples assume you have imported both `@azure-tools/cadl-autorest` and `@cadl-lang/rest` somewhere in your Cadl program (though importing them in `main.cadl` is the standard convention).
|
||||
The following examples assume you have imported both `@cadl-lang/openapi3` and `@cadl-lang/rest` somewhere in your Cadl program (though importing them in `main.cadl` is the standard convention).
|
||||
|
||||
#### Service definition and metadata
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1 @@
|
|||
# Change Log - @cadl-lang/cadl-openapi3
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "@cadl-lang/openapi3",
|
||||
"version": "0.1.0",
|
||||
"author": "Microsoft Corporation",
|
||||
"description": "Cadl library for emitting OpenAPI 3.0 from the Cadl REST protocol binding",
|
||||
"homepage": "https://github.com/Azure/adl",
|
||||
"readme": "https://github.com/Azure/adl/blob/master/README.md",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Azure/adl.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Azure/adl/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"cadl"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/src/openapi.js",
|
||||
"cadlMain": "dist/src/openapi.js",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p .",
|
||||
"watch": "tsc -p . --watch",
|
||||
"test": "mocha --timeout 5000 --require source-map-support/register --ignore 'dist/test/manual/**/*.js' 'dist/test/**/*.js'",
|
||||
"test-official": "mocha --forbid-only --timeout 5000 --require source-map-support/register --ignore 'dist/test/manual/**/*.js' 'dist/test/**/*.js'"
|
||||
},
|
||||
"files": [
|
||||
"lib/*.cadl",
|
||||
"dist/**",
|
||||
"!dist/test/**"
|
||||
],
|
||||
"dependencies": {
|
||||
"@cadl-lang/compiler": "0.19.0",
|
||||
"@cadl-lang/rest": "0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "~7.0.2",
|
||||
"@types/node": "~14.0.27",
|
||||
"mocha": "~8.3.2",
|
||||
"typescript": "~4.3.2"
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,410 @@
|
|||
import { deepStrictEqual, ok, strictEqual } from "assert";
|
||||
import { openApiFor } from "./testHost.js";
|
||||
|
||||
describe("openapi3: definitions", () => {
|
||||
it("defines models", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Foo",
|
||||
`model Foo {
|
||||
x: int32;
|
||||
};`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
deepStrictEqual(res.schemas.Foo, {
|
||||
type: "object",
|
||||
properties: {
|
||||
x: { type: "integer", format: "int32" },
|
||||
},
|
||||
required: ["x"],
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't define anonymous or unconnected models", async () => {
|
||||
const res = await oapiForModel(
|
||||
"{ ... Foo }",
|
||||
`model Foo {
|
||||
x: int32;
|
||||
};`
|
||||
);
|
||||
|
||||
ok(!res.isRef);
|
||||
strictEqual(Object.keys(res.schemas).length, 0);
|
||||
deepStrictEqual(res.useSchema, {
|
||||
type: "object",
|
||||
properties: {
|
||||
x: { type: "integer", format: "int32" },
|
||||
},
|
||||
required: ["x"],
|
||||
"x-cadl-name": "(anonymous model)",
|
||||
});
|
||||
});
|
||||
|
||||
it("defines templated models", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Foo<int32>",
|
||||
`model Foo<T> {
|
||||
x: T;
|
||||
};`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
ok(res.schemas.Foo_int32, "expected definition named Foo_int32");
|
||||
deepStrictEqual(res.schemas.Foo_int32, {
|
||||
type: "object",
|
||||
properties: {
|
||||
x: { type: "integer", format: "int32" },
|
||||
},
|
||||
required: ["x"],
|
||||
});
|
||||
});
|
||||
|
||||
it("defines templated models when template param is in a namespace", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Foo<Test.M>",
|
||||
`
|
||||
namespace Test {
|
||||
model M {}
|
||||
}
|
||||
model Foo<T> {
|
||||
x: T;
|
||||
};`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
ok(res.schemas["Foo_Test.M"], "expected definition named Foo_Test.M");
|
||||
deepStrictEqual(res.schemas["Foo_Test.M"], {
|
||||
type: "object",
|
||||
properties: {
|
||||
x: { $ref: "#/components/schemas/Test.M" },
|
||||
},
|
||||
required: ["x"],
|
||||
});
|
||||
});
|
||||
|
||||
it("defines models extended from models", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Bar",
|
||||
`
|
||||
model Foo {
|
||||
y: int32;
|
||||
};
|
||||
model Bar extends Foo {}`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
ok(res.schemas.Foo, "expected definition named Foo");
|
||||
ok(res.schemas.Bar, "expected definition named Bar");
|
||||
deepStrictEqual(res.schemas.Bar, {
|
||||
type: "object",
|
||||
properties: {},
|
||||
allOf: [{ $ref: "#/components/schemas/Foo" }],
|
||||
});
|
||||
|
||||
deepStrictEqual(res.schemas.Foo, {
|
||||
type: "object",
|
||||
properties: { y: { type: "integer", format: "int32" } },
|
||||
required: ["y"],
|
||||
});
|
||||
});
|
||||
|
||||
it("defines models with properties extended from models", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Bar",
|
||||
`
|
||||
model Foo {
|
||||
y: int32;
|
||||
};
|
||||
model Bar extends Foo {
|
||||
x: int32;
|
||||
}`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
ok(res.schemas.Foo, "expected definition named Foo");
|
||||
ok(res.schemas.Bar, "expected definition named Bar");
|
||||
deepStrictEqual(res.schemas.Bar, {
|
||||
type: "object",
|
||||
properties: { x: { type: "integer", format: "int32" } },
|
||||
allOf: [{ $ref: "#/components/schemas/Foo" }],
|
||||
required: ["x"],
|
||||
});
|
||||
|
||||
deepStrictEqual(res.schemas.Foo, {
|
||||
type: "object",
|
||||
properties: { y: { type: "integer", format: "int32" } },
|
||||
required: ["y"],
|
||||
});
|
||||
});
|
||||
|
||||
it("defines models extended from templated models", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Bar",
|
||||
`
|
||||
model Foo<T> {
|
||||
y: T;
|
||||
};
|
||||
model Bar extends Foo<int32> {}`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
ok(res.schemas["Foo_int32"] === undefined, "no definition named Foo_int32");
|
||||
ok(res.schemas.Bar, "expected definition named Bar");
|
||||
deepStrictEqual(res.schemas.Bar, {
|
||||
type: "object",
|
||||
properties: { y: { type: "integer", format: "int32" } },
|
||||
required: ["y"],
|
||||
});
|
||||
});
|
||||
|
||||
it("defines models with properties extended from templated models", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Bar",
|
||||
`
|
||||
model Foo<T> {
|
||||
y: T;
|
||||
};
|
||||
model Bar extends Foo<int32> {
|
||||
x: int32
|
||||
}`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
ok(res.schemas.Foo_int32, "expected definition named Foo_int32");
|
||||
ok(res.schemas.Bar, "expected definition named Bar");
|
||||
deepStrictEqual(res.schemas.Bar, {
|
||||
type: "object",
|
||||
properties: { x: { type: "integer", format: "int32" } },
|
||||
allOf: [{ $ref: "#/components/schemas/Foo_int32" }],
|
||||
required: ["x"],
|
||||
});
|
||||
|
||||
deepStrictEqual(res.schemas.Foo_int32, {
|
||||
type: "object",
|
||||
properties: { y: { type: "integer", format: "int32" } },
|
||||
required: ["y"],
|
||||
});
|
||||
});
|
||||
|
||||
it("defines templated models with properties extended from templated models", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Bar<int32>",
|
||||
`
|
||||
model Foo<T> {
|
||||
y: T;
|
||||
};
|
||||
model Bar<T> extends Foo<T> {
|
||||
x: T
|
||||
}`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
ok(res.schemas.Foo_int32, "expected definition named Foo_int32");
|
||||
ok(res.schemas.Bar_int32, "expected definition named Bar_int32");
|
||||
deepStrictEqual(res.schemas.Bar_int32, {
|
||||
type: "object",
|
||||
properties: { x: { type: "integer", format: "int32" } },
|
||||
allOf: [{ $ref: "#/components/schemas/Foo_int32" }],
|
||||
required: ["x"],
|
||||
});
|
||||
|
||||
deepStrictEqual(res.schemas.Foo_int32, {
|
||||
type: "object",
|
||||
properties: { y: { type: "integer", format: "int32" } },
|
||||
required: ["y"],
|
||||
});
|
||||
});
|
||||
|
||||
it("defines models with no properties extended", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Bar",
|
||||
`
|
||||
model Foo {};
|
||||
model Bar extends Foo {};`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
ok(res.schemas.Foo, "expected definition named Foo");
|
||||
ok(res.schemas.Bar, "expected definition named Bar");
|
||||
deepStrictEqual(res.schemas.Bar, {
|
||||
type: "object",
|
||||
properties: {},
|
||||
allOf: [{ $ref: "#/components/schemas/Foo" }],
|
||||
});
|
||||
|
||||
deepStrictEqual(res.schemas.Foo, {
|
||||
type: "object",
|
||||
properties: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("defines models with no properties extended twice", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Baz",
|
||||
`
|
||||
model Foo { x: int32 };
|
||||
model Bar extends Foo {};
|
||||
model Baz extends Bar {};`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
ok(res.schemas.Foo, "expected definition named Foo");
|
||||
ok(res.schemas.Bar, "expected definition named Bar");
|
||||
ok(res.schemas.Baz, "expected definition named Baz");
|
||||
deepStrictEqual(res.schemas.Baz, {
|
||||
type: "object",
|
||||
properties: {},
|
||||
allOf: [{ $ref: "#/components/schemas/Bar" }],
|
||||
});
|
||||
|
||||
deepStrictEqual(res.schemas.Bar, {
|
||||
type: "object",
|
||||
properties: {},
|
||||
allOf: [{ $ref: "#/components/schemas/Foo" }],
|
||||
});
|
||||
|
||||
deepStrictEqual(res.schemas.Foo, {
|
||||
type: "object",
|
||||
properties: {
|
||||
x: {
|
||||
format: "int32",
|
||||
type: "integer",
|
||||
},
|
||||
},
|
||||
required: ["x"],
|
||||
});
|
||||
});
|
||||
|
||||
it("defines models extended from primitives", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Pet",
|
||||
`
|
||||
model shortString extends string {}
|
||||
model Pet { name: shortString };
|
||||
`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
ok(res.schemas.shortString, "expected definition named shortString");
|
||||
ok(res.schemas.Pet, "expected definition named Pet");
|
||||
deepStrictEqual(res.schemas.shortString, {
|
||||
type: "string",
|
||||
});
|
||||
});
|
||||
|
||||
it("defines models extended from primitives with attrs", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Pet",
|
||||
`
|
||||
@maxLength(10) @minLength(10)
|
||||
model shortString extends string {}
|
||||
model Pet { name: shortString };
|
||||
`
|
||||
);
|
||||
|
||||
ok(res.isRef);
|
||||
ok(res.schemas.shortString, "expected definition named shortString");
|
||||
ok(res.schemas.Pet, "expected definition named Pet");
|
||||
deepStrictEqual(res.schemas.shortString, {
|
||||
type: "string",
|
||||
minLength: 10,
|
||||
maxLength: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it("defines models extended from primitives with new attrs", async () => {
|
||||
const res = await oapiForModel(
|
||||
"Pet",
|
||||
`
|
||||
@maxLength(10)
|
||||
model shortString extends string {}
|
||||
@minLength(1)
|
||||
model shortButNotEmptyString extends shortString {};
|
||||
model Pet { name: shortButNotEmptyString, breed: shortString };
|
||||
`
|
||||
);
|
||||
ok(res.isRef);
|
||||
ok(res.schemas.shortString, "expected definition named shortString");
|
||||
ok(res.schemas.shortButNotEmptyString, "expected definition named shortButNotEmptyString");
|
||||
ok(res.schemas.Pet, "expected definition named Pet");
|
||||
|
||||
deepStrictEqual(res.schemas.shortString, {
|
||||
type: "string",
|
||||
maxLength: 10,
|
||||
});
|
||||
deepStrictEqual(res.schemas.shortButNotEmptyString, {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
maxLength: 10,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("openapi3: primitives", () => {
|
||||
const cases = [
|
||||
["int32", { type: "integer", format: "int32" }],
|
||||
["int64", { type: "integer", format: "int64" }],
|
||||
["float32", { type: "number", format: "float" }],
|
||||
["float64", { type: "number", format: "double" }],
|
||||
["string", { type: "string" }],
|
||||
["boolean", { type: "boolean" }],
|
||||
["plainDate", { type: "string", format: "date" }],
|
||||
["zonedDateTime", { type: "string", format: "date-time" }],
|
||||
["plainTime", { type: "string", format: "time" }],
|
||||
];
|
||||
|
||||
for (const test of cases) {
|
||||
it("knows schema for " + test[0], async () => {
|
||||
const res = await oapiForModel(
|
||||
"Pet",
|
||||
`
|
||||
model Pet { name: ${test[0]} };
|
||||
`
|
||||
);
|
||||
|
||||
const schema = res.schemas.Pet.properties.name;
|
||||
deepStrictEqual(schema, test[1]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("openapi3: literals", () => {
|
||||
const cases = [
|
||||
["1", { type: "number", enum: [1] }],
|
||||
['"hello"', { type: "string", enum: ["hello"] }],
|
||||
["false", { type: "boolean", enum: [false] }],
|
||||
["true", { type: "boolean", enum: [true] }],
|
||||
];
|
||||
|
||||
for (const test of cases) {
|
||||
it("knows schema for " + test[0], async () => {
|
||||
const res = await oapiForModel(
|
||||
"Pet",
|
||||
`
|
||||
model Pet { name: ${test[0]} };
|
||||
`
|
||||
);
|
||||
|
||||
const schema = res.schemas.Pet.properties.name;
|
||||
deepStrictEqual(schema, test[1]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function oapiForModel(name: string, modelDef: string) {
|
||||
const oapi = await openApiFor(`
|
||||
${modelDef};
|
||||
@resource("/")
|
||||
namespace root {
|
||||
op read(): ${name};
|
||||
}
|
||||
`);
|
||||
|
||||
const useSchema = oapi.paths["/"].get.responses[200].content["application/json"].schema;
|
||||
|
||||
return {
|
||||
isRef: !!useSchema.$ref,
|
||||
useSchema,
|
||||
schemas: oapi.components.schemas || {},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { createTestHost } from "@cadl-lang/compiler/dist/test/test-host.js";
|
||||
import { resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
export async function createOpenAPITestHost() {
|
||||
const host = await createTestHost();
|
||||
const root = resolve(fileURLToPath(import.meta.url), "../../../");
|
||||
|
||||
// load rest
|
||||
await host.addRealCadlFile(
|
||||
"./node_modules/rest/package.json",
|
||||
resolve(root, "../rest/package.json")
|
||||
);
|
||||
await host.addRealCadlFile(
|
||||
"./node_modules/rest/lib/rest.cadl",
|
||||
resolve(root, "../rest/lib/rest.cadl")
|
||||
);
|
||||
await host.addRealJsFile(
|
||||
"./node_modules/rest/dist/rest.js",
|
||||
resolve(root, "../rest/dist/rest.js")
|
||||
);
|
||||
|
||||
// load openapi
|
||||
await host.addRealCadlFile(
|
||||
"./node_modules/openapi3/package.json",
|
||||
resolve(root, "../openapi3/package.json")
|
||||
);
|
||||
await host.addRealJsFile(
|
||||
"./node_modules/openapi3/dist/src/openapi.js",
|
||||
resolve(root, "../openapi3/dist/src/openapi.js")
|
||||
);
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
export async function openApiFor(code: string) {
|
||||
const host = await createOpenAPITestHost();
|
||||
const outPath = resolve("/openapi.json");
|
||||
host.addCadlFile("./main.cadl", `import "rest"; import "openapi3";${code}`);
|
||||
await host.compile("./main.cadl", { noEmit: false, swaggerOutputFile: outPath });
|
||||
return JSON.parse(host.fs.get(outPath)!);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"references": [{ "path": "../compiler/tsconfig.json" }, { "path": "../rest/tsconfig.json" }],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"types": ["node", "mocha"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
}
|
Загрузка…
Ссылка в новой задаче