Skip to content

Commit 4151efa

Browse files
chrisradekChristopher Radek
and
Christopher Radek
authored
add openapi 3.1 support (#5372)
This PR adds support for Open API 3.1 in the `@typespec/openapi3` package. The changes in this PR includes: #4946 - adds `output-spec-versions` emitter option #5035 - adds initial open api 3.1 schema emitter #5245 - add support for tuples #5246 - add support for enums backed by multiple types #5310 - playground support for array of constants emitter options #5407 - add support for json-schema decorators #5549 - add support for binary types in Open API 3.1 --------- Co-authored-by: Christopher Radek <Christopher.Radek@microsoft.com>
1 parent 2bdac72 commit 4151efa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+4940
-2657
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@typespec/openapi3"
5+
---
6+
7+
Adds support for @typespec/json-schema decorators with Open API 3.0 and 3.1 emitters.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@typespec/openapi3"
5+
---
6+
7+
Adds support for emitting Open API 3.1 models using the `openapi-versions` emitter configuration option.
8+
Open API 3.0 is emitted by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@typespec/playground"
5+
---
6+
7+
Add support for displaying array-based emitter options

packages/openapi3/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ Example Multiple service with versioning
7474
- `openapi.Org1.Service2.v1.0.yaml`
7575
- `openapi.Org1.Service2.v1.1.yaml`
7676

77+
### `openapi-versions`
78+
79+
**Type:** `array`
80+
7781
### `new-line`
7882

7983
**Type:** `"crlf" | "lf"`

packages/openapi3/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,14 @@
6565
"peerDependencies": {
6666
"@typespec/compiler": "workspace:~",
6767
"@typespec/http": "workspace:~",
68+
"@typespec/json-schema": "workspace:~",
6869
"@typespec/openapi": "workspace:~",
6970
"@typespec/versioning": "workspace:~"
7071
},
7172
"peerDependenciesMeta": {
73+
"@typespec/json-schema": {
74+
"optional": true
75+
},
7276
"@typespec/xml": {
7377
"optional": true
7478
}
@@ -78,6 +82,7 @@
7882
"@types/yargs": "~17.0.33",
7983
"@typespec/compiler": "workspace:~",
8084
"@typespec/http": "workspace:~",
85+
"@typespec/json-schema": "workspace:~",
8186
"@typespec/library-linter": "workspace:~",
8287
"@typespec/openapi": "workspace:~",
8388
"@typespec/rest": "workspace:~",
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Program, Type } from "@typespec/compiler";
2+
import { getExtensions } from "@typespec/openapi";
3+
4+
export function attachExtensions(program: Program, type: Type, emitObject: any) {
5+
// Attach any OpenAPI extensions
6+
const extensions = getExtensions(program, type);
7+
if (extensions) {
8+
for (const key of extensions.keys()) {
9+
emitObject[key] = extensions.get(key);
10+
}
11+
}
12+
}

packages/openapi3/src/encoding.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ import { type ModelProperty, Program, type Scalar, getEncode } from "@typespec/c
22
import { ObjectBuilder } from "@typespec/compiler/emitter-framework";
33
import type { ResolvedOpenAPI3EmitterOptions } from "./openapi.js";
44
import { getSchemaForStdScalars } from "./std-scalar-schemas.js";
5-
import type { OpenAPI3Schema } from "./types.js";
5+
import type { OpenAPI3Schema, OpenAPISchema3_1 } from "./types.js";
66

77
export function applyEncoding(
88
program: Program,
99
typespecType: Scalar | ModelProperty,
10-
target: OpenAPI3Schema,
10+
target: OpenAPI3Schema | OpenAPISchema3_1,
11+
getEncodedFieldName: (typespecType: Scalar | ModelProperty) => string,
1112
options: ResolvedOpenAPI3EmitterOptions,
12-
): OpenAPI3Schema {
13+
): OpenAPI3Schema & OpenAPISchema3_1 {
1314
const encodeData = getEncode(program, typespecType);
1415
if (encodeData) {
1516
const newTarget = new ObjectBuilder(target);
1617
const newType = getSchemaForStdScalars(encodeData.type as any, options);
1718
newTarget.type = newType.type;
1819
// If the target already has a format it takes priority. (e.g. int32)
19-
newTarget.format = mergeFormatAndEncoding(
20-
newTarget.format,
20+
const encodedFieldName = getEncodedFieldName(typespecType);
21+
newTarget[encodedFieldName] = mergeFormatAndEncoding(
22+
newTarget[encodedFieldName],
2123
encodeData.encoding,
2224
newType.format,
2325
);

packages/openapi3/src/json-schema.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type JsonSchemaModule = typeof import("@typespec/json-schema");
2+
3+
export async function resolveJsonSchemaModule(): Promise<JsonSchemaModule | undefined> {
4+
try {
5+
return await import("@typespec/json-schema");
6+
} catch {
7+
return undefined;
8+
}
9+
}

packages/openapi3/src/lib.ts

+23
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/compiler";
22

33
export type FileType = "yaml" | "json";
4+
export type OpenAPIVersion = "3.0.0" | "3.1.0";
45
export interface OpenAPI3EmitterOptions {
56
/**
67
* If the content should be serialized as YAML or JSON.
@@ -36,6 +37,16 @@ export interface OpenAPI3EmitterOptions {
3637
*/
3738
"output-file"?: string;
3839

40+
/**
41+
* The Open API specification versions to emit.
42+
* If more than one version is specified, then the output file
43+
* will be created inside a directory matching each specification version.
44+
*
45+
* @default ["v3.0"]
46+
* @internal
47+
*/
48+
"openapi-versions"?: OpenAPIVersion[];
49+
3950
/**
4051
* Set the newline character for emitting files.
4152
* @default lf
@@ -104,6 +115,18 @@ const EmitterOptionsSchema: JSONSchemaType<OpenAPI3EmitterOptions> = {
104115
" - `openapi.Org1.Service2.v1.1.yaml` ",
105116
].join("\n"),
106117
},
118+
"openapi-versions": {
119+
type: "array",
120+
items: {
121+
type: "string",
122+
enum: ["3.0.0", "3.1.0"],
123+
nullable: true,
124+
description: "The versions of OpenAPI to emit. Defaults to `[3.0.0]`",
125+
},
126+
nullable: true,
127+
uniqueItems: true,
128+
minItems: 1,
129+
},
107130
"new-line": {
108131
type: "string",
109132
enum: ["crlf", "lf"],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { applyEncoding as baseApplyEncoding } from "./encoding.js";
2+
import { OpenApiSpecSpecificProps } from "./openapi-spec-mappings.js";
3+
import { OpenAPI3Schema } from "./types.js";
4+
5+
function getEncodingFieldName() {
6+
// In Open API 3.0, format is always used for encoding.
7+
return "format";
8+
}
9+
10+
export const applyEncoding: OpenApiSpecSpecificProps["applyEncoding"] = (
11+
program,
12+
typespecType,
13+
target,
14+
options,
15+
) => {
16+
return baseApplyEncoding(program, typespecType, target, getEncodingFieldName, options);
17+
};
18+
19+
export const getRawBinarySchema = (): OpenAPI3Schema => {
20+
return { type: "string", format: "binary" };
21+
};
22+
23+
export const isRawBinarySchema = (schema: OpenAPI3Schema): boolean => {
24+
return schema.type === "string" && schema.format === "binary";
25+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ModelProperty, Scalar } from "@typespec/compiler";
2+
import { applyEncoding as baseApplyEncoding } from "./encoding.js";
3+
import { OpenApiSpecSpecificProps } from "./openapi-spec-mappings.js";
4+
import { OpenAPISchema3_1 } from "./types.js";
5+
import { isScalarExtendsBytes } from "./util.js";
6+
7+
function getEncodingFieldName(typespecType: Scalar | ModelProperty) {
8+
// In Open API 3.1, `contentEncoding` is used for encoded binary data instead of `format`.
9+
const typeIsBytes = isScalarExtendsBytes(
10+
typespecType.kind === "ModelProperty" ? typespecType.type : typespecType,
11+
);
12+
if (typeIsBytes) {
13+
return "contentEncoding";
14+
}
15+
return "format";
16+
}
17+
18+
export const applyEncoding: OpenApiSpecSpecificProps["applyEncoding"] = (
19+
program,
20+
typespecType,
21+
target,
22+
options,
23+
) => {
24+
return baseApplyEncoding(program, typespecType, target, getEncodingFieldName, options);
25+
};
26+
27+
export const getRawBinarySchema = (contentType?: string): OpenAPISchema3_1 => {
28+
if (contentType) {
29+
return { contentMediaType: contentType };
30+
}
31+
return {};
32+
};
33+
34+
export const isRawBinarySchema = (schema: OpenAPISchema3_1): boolean => {
35+
return schema.type === undefined;
36+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { EmitContext, ModelProperty, Namespace, Program, Scalar } from "@typespec/compiler";
2+
import { AssetEmitter } from "@typespec/compiler/emitter-framework";
3+
import { MetadataInfo } from "@typespec/http";
4+
import { getExternalDocs, resolveInfo } from "@typespec/openapi";
5+
import { JsonSchemaModule } from "./json-schema.js";
6+
import { OpenAPI3EmitterOptions, OpenAPIVersion } from "./lib.js";
7+
import {
8+
applyEncoding as applyEncoding3_0,
9+
getRawBinarySchema as getRawBinarySchema3_0,
10+
isRawBinarySchema as isRawBinarySchema3_0,
11+
} from "./openapi-helpers-3-0.js";
12+
import {
13+
applyEncoding as applyEncoding3_1,
14+
getRawBinarySchema as getRawBinarySchema3_1,
15+
isRawBinarySchema as isRawBinarySchema3_1,
16+
} from "./openapi-helpers-3-1.js";
17+
import { ResolvedOpenAPI3EmitterOptions } from "./openapi.js";
18+
import { createSchemaEmitter3_0 } from "./schema-emitter-3-0.js";
19+
import { createSchemaEmitter3_1 } from "./schema-emitter-3-1.js";
20+
import { OpenAPI3Schema, OpenAPISchema3_1, SupportedOpenAPIDocuments } from "./types.js";
21+
import { VisibilityUsageTracker } from "./visibility-usage.js";
22+
import { XmlModule } from "./xml-module.js";
23+
24+
export type CreateSchemaEmitter = (props: {
25+
program: Program;
26+
context: EmitContext<OpenAPI3EmitterOptions>;
27+
metadataInfo: MetadataInfo;
28+
visibilityUsage: VisibilityUsageTracker;
29+
options: ResolvedOpenAPI3EmitterOptions;
30+
optionalDependencies: { jsonSchemaModule?: JsonSchemaModule; xmlModule?: XmlModule };
31+
}) => AssetEmitter<OpenAPI3Schema | OpenAPISchema3_1, OpenAPI3EmitterOptions>;
32+
33+
export interface OpenApiSpecSpecificProps {
34+
applyEncoding(
35+
program: Program,
36+
typespecType: Scalar | ModelProperty,
37+
target: OpenAPI3Schema,
38+
options: ResolvedOpenAPI3EmitterOptions,
39+
): OpenAPI3Schema & OpenAPISchema3_1;
40+
createRootDoc: (
41+
program: Program,
42+
serviceType: Namespace,
43+
serviceVersion?: string,
44+
) => SupportedOpenAPIDocuments;
45+
46+
createSchemaEmitter: CreateSchemaEmitter;
47+
/**
48+
* Returns the binary description for an unencoded binary type
49+
* @see https://spec.openapis.org/oas/v3.1.1.html#migrating-binary-descriptions-from-oas-3-0
50+
*/
51+
getRawBinarySchema(contentType?: string): OpenAPI3Schema | OpenAPISchema3_1;
52+
53+
isRawBinarySchema(schema: OpenAPI3Schema | OpenAPISchema3_1): boolean;
54+
}
55+
56+
export function getOpenApiSpecProps(specVersion: OpenAPIVersion): OpenApiSpecSpecificProps {
57+
switch (specVersion) {
58+
case "3.0.0":
59+
return {
60+
applyEncoding: applyEncoding3_0,
61+
createRootDoc(program, serviceType, serviceVersion) {
62+
return createRoot(program, serviceType, specVersion, serviceVersion);
63+
},
64+
createSchemaEmitter: createSchemaEmitter3_0,
65+
getRawBinarySchema: getRawBinarySchema3_0,
66+
isRawBinarySchema: isRawBinarySchema3_0,
67+
};
68+
case "3.1.0":
69+
return {
70+
applyEncoding: applyEncoding3_1,
71+
createRootDoc(program, serviceType, serviceVersion) {
72+
return createRoot(program, serviceType, specVersion, serviceVersion);
73+
},
74+
createSchemaEmitter: createSchemaEmitter3_1,
75+
getRawBinarySchema: getRawBinarySchema3_1,
76+
isRawBinarySchema: isRawBinarySchema3_1,
77+
};
78+
}
79+
}
80+
81+
function createRoot(
82+
program: Program,
83+
serviceType: Namespace,
84+
specVersion: OpenAPIVersion,
85+
serviceVersion?: string,
86+
): SupportedOpenAPIDocuments {
87+
const info = resolveInfo(program, serviceType);
88+
89+
return {
90+
openapi: specVersion,
91+
info: {
92+
title: "(title)",
93+
...info,
94+
version: serviceVersion ?? info?.version ?? "0.0.0",
95+
},
96+
externalDocs: getExternalDocs(program, serviceType),
97+
tags: [],
98+
paths: {},
99+
security: undefined,
100+
components: {
101+
parameters: {},
102+
requestBodies: {},
103+
responses: {},
104+
schemas: {},
105+
examples: {},
106+
securitySchemes: {},
107+
},
108+
};
109+
}

0 commit comments

Comments
 (0)