@zudojs/openapi 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +327 -21
- package/dist/index.d.ts +26 -21
- package/dist/index.js +31 -13
- package/dist/openApiComponents/index.d.ts +2 -2
- package/dist/openApiComponents/index.js +1 -1
- package/dist/openApiConstants/index.d.ts +1 -1
- package/dist/openApiConstants/index.js +1 -1
- package/dist/openApiConstants/openApiConstants.core.d.ts +15 -3
- package/dist/openApiConstants/openApiConstants.core.js +22 -3
- package/dist/openApiDocument/index.d.ts +1 -1
- package/dist/openApiDocument/index.js +1 -1
- package/dist/openApiDocument/openApiDocument.builder.d.ts +39 -52
- package/dist/openApiDocument/openApiDocument.builder.js +73 -72
- package/dist/openApiErrors/index.d.ts +2 -2
- package/dist/openApiErrors/index.js +2 -1
- package/dist/openApiErrors/openApiError.base.d.ts +9 -0
- package/dist/openApiErrors/openApiError.base.js +7 -0
- package/dist/openApiErrors/openApiError.types.d.ts +34 -14
- package/dist/openApiErrors/openApiError.types.js +83 -35
- package/dist/openApiHttp/index.d.ts +2 -2
- package/dist/openApiHttp/index.js +2 -2
- package/dist/openApiHttp/openApiHttpAdapter.core.d.ts +127 -25
- package/dist/openApiHttp/openApiHttpAdapter.core.js +212 -16
- package/dist/openApiRegistry/openApiRegistry.core.d.ts +38 -5
- package/dist/openApiRegistry/openApiRegistry.core.js +167 -66
- package/dist/openApiRegistry/openApiRegistry.type.d.ts +18 -13
- package/dist/openApiRegistry/openApiRegistry.type.js +1 -2
- package/dist/openApiRouting/index.d.ts +2 -2
- package/dist/openApiRouting/index.js +1 -1
- package/dist/openApiRouting/routeConverter.core.d.ts +25 -7
- package/dist/openApiRouting/routeConverter.core.js +104 -58
- package/dist/openApiRouting/routeMetadata.type.d.ts +40 -17
- package/dist/openApiRouting/routeMetadata.type.js +9 -0
- package/dist/openApiRouting/routeScanner.core.d.ts +17 -47
- package/dist/openApiRouting/routeScanner.core.js +40 -55
- package/dist/openApiSchema/index.d.ts +4 -4
- package/dist/openApiSchema/index.js +2 -2
- package/dist/openApiSchema/references.core.d.ts +18 -1
- package/dist/openApiSchema/references.core.js +21 -1
- package/dist/openApiSchema/schemaConverter.core.d.ts +51 -3
- package/dist/openApiSchema/schemaConverter.core.js +475 -118
- package/dist/openApiSchema/schemaRegistry.core.d.ts +27 -10
- package/dist/openApiSchema/schemaRegistry.core.js +33 -14
- package/dist/openApiSerialization/openApiSerializer.core.d.ts +10 -4
- package/dist/openApiSerialization/openApiSerializer.core.js +135 -9
- package/dist/openApiTypes/index.d.ts +1 -1
- package/dist/openApiTypes/openApiDocument.type.d.ts +16 -0
- package/dist/openApiTypes/openApiSchema.type.d.ts +24 -1
- package/dist/openApiTypes/openApiSchema.type.js +3 -0
- package/dist/openApiUi/index.d.ts +8 -0
- package/dist/openApiUi/index.js +8 -0
- package/dist/openApiUi/openApiUi.brand.d.ts +26 -0
- package/dist/openApiUi/openApiUi.brand.js +44 -0
- package/dist/openApiUi/openApiUi.core.d.ts +47 -0
- package/dist/openApiUi/openApiUi.core.js +143 -0
- package/dist/openApiValidation/index.d.ts +2 -2
- package/dist/openApiValidation/index.js +1 -1
- package/dist/openApiValidation/openApiValidator.core.d.ts +33 -11
- package/dist/openApiValidation/openApiValidator.core.js +274 -66
- package/package.json +25 -18
- package/dist/.tsbuildinfo +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/openApiComponents/index.d.ts.map +0 -1
- package/dist/openApiComponents/index.js.map +0 -1
- package/dist/openApiConstants/index.d.ts.map +0 -1
- package/dist/openApiConstants/index.js.map +0 -1
- package/dist/openApiConstants/openApiConstants.core.d.ts.map +0 -1
- package/dist/openApiConstants/openApiConstants.core.js.map +0 -1
- package/dist/openApiDocument/index.d.ts.map +0 -1
- package/dist/openApiDocument/index.js.map +0 -1
- package/dist/openApiDocument/openApiDocument.builder.d.ts.map +0 -1
- package/dist/openApiDocument/openApiDocument.builder.js.map +0 -1
- package/dist/openApiErrors/index.d.ts.map +0 -1
- package/dist/openApiErrors/index.js.map +0 -1
- package/dist/openApiErrors/openApiError.base.d.ts.map +0 -1
- package/dist/openApiErrors/openApiError.base.js.map +0 -1
- package/dist/openApiErrors/openApiError.core.d.ts.map +0 -1
- package/dist/openApiErrors/openApiError.core.js.map +0 -1
- package/dist/openApiErrors/openApiError.types.d.ts.map +0 -1
- package/dist/openApiErrors/openApiError.types.js.map +0 -1
- package/dist/openApiHttp/index.d.ts.map +0 -1
- package/dist/openApiHttp/index.js.map +0 -1
- package/dist/openApiHttp/openApiHttpAdapter.core.d.ts.map +0 -1
- package/dist/openApiHttp/openApiHttpAdapter.core.js.map +0 -1
- package/dist/openApiRegistry/index.d.ts.map +0 -1
- package/dist/openApiRegistry/index.js.map +0 -1
- package/dist/openApiRegistry/openApiRegistry.core.d.ts.map +0 -1
- package/dist/openApiRegistry/openApiRegistry.core.js.map +0 -1
- package/dist/openApiRegistry/openApiRegistry.type.d.ts.map +0 -1
- package/dist/openApiRegistry/openApiRegistry.type.js.map +0 -1
- package/dist/openApiRouting/index.d.ts.map +0 -1
- package/dist/openApiRouting/index.js.map +0 -1
- package/dist/openApiRouting/routeConverter.core.d.ts.map +0 -1
- package/dist/openApiRouting/routeConverter.core.js.map +0 -1
- package/dist/openApiRouting/routeMetadata.type.d.ts.map +0 -1
- package/dist/openApiRouting/routeMetadata.type.js.map +0 -1
- package/dist/openApiRouting/routeScanner.core.d.ts.map +0 -1
- package/dist/openApiRouting/routeScanner.core.js.map +0 -1
- package/dist/openApiSchema/index.d.ts.map +0 -1
- package/dist/openApiSchema/index.js.map +0 -1
- package/dist/openApiSchema/references.core.d.ts.map +0 -1
- package/dist/openApiSchema/references.core.js.map +0 -1
- package/dist/openApiSchema/schemaConverter.core.d.ts.map +0 -1
- package/dist/openApiSchema/schemaConverter.core.js.map +0 -1
- package/dist/openApiSchema/schemaRegistry.core.d.ts.map +0 -1
- package/dist/openApiSchema/schemaRegistry.core.js.map +0 -1
- package/dist/openApiSerialization/index.d.ts.map +0 -1
- package/dist/openApiSerialization/index.js.map +0 -1
- package/dist/openApiSerialization/openApiSerializer.core.d.ts.map +0 -1
- package/dist/openApiSerialization/openApiSerializer.core.js.map +0 -1
- package/dist/openApiTypes/index.d.ts.map +0 -1
- package/dist/openApiTypes/index.js.map +0 -1
- package/dist/openApiTypes/openApiComponent.type.d.ts.map +0 -1
- package/dist/openApiTypes/openApiComponent.type.js.map +0 -1
- package/dist/openApiTypes/openApiDocument.type.d.ts.map +0 -1
- package/dist/openApiTypes/openApiDocument.type.js.map +0 -1
- package/dist/openApiTypes/openApiPath.type.d.ts.map +0 -1
- package/dist/openApiTypes/openApiPath.type.js.map +0 -1
- package/dist/openApiTypes/openApiSchema.type.d.ts.map +0 -1
- package/dist/openApiTypes/openApiSchema.type.js.map +0 -1
- package/dist/openApiTypes/openApiSecurity.type.d.ts.map +0 -1
- package/dist/openApiTypes/openApiSecurity.type.js.map +0 -1
- package/dist/openApiTypes/openApiTypes.core.d.ts.map +0 -1
- package/dist/openApiTypes/openApiTypes.core.js.map +0 -1
- package/dist/openApiValidation/index.d.ts.map +0 -1
- package/dist/openApiValidation/index.js.map +0 -1
- package/dist/openApiValidation/openApiValidator.core.d.ts.map +0 -1
- package/dist/openApiValidation/openApiValidator.core.js.map +0 -1
|
@@ -1,141 +1,498 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { DEFAULT_OPENAPI_VERSION } from "../openApiConstants/openApiConstants.core.js";
|
|
2
|
+
/** Maps `@zudojs/schema` string formats onto OpenAPI `format` values. */
|
|
3
|
+
const STRING_FORMATS = {
|
|
4
|
+
email: "email",
|
|
5
|
+
url: "uri",
|
|
6
|
+
uuid: "uuid",
|
|
7
|
+
"uuid-v4": "uuid",
|
|
8
|
+
datetime: "date-time",
|
|
9
|
+
date: "date",
|
|
10
|
+
time: "time",
|
|
11
|
+
ipv4: "ipv4",
|
|
12
|
+
ipv6: "ipv6",
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Assigns a property that may be named `__proto__`.
|
|
16
|
+
*
|
|
17
|
+
* `properties["__proto__"] = schema` on a plain object literal sets the
|
|
18
|
+
* object's prototype instead of adding a member: the property vanishes from
|
|
19
|
+
* the generated document with no error anywhere. A schema field genuinely
|
|
20
|
+
* called `__proto__` is unusual; one supplied by an attacker to make a
|
|
21
|
+
* constraint disappear from the published contract is exactly the point.
|
|
22
|
+
*/
|
|
23
|
+
function defineProperty(target, key, value) {
|
|
24
|
+
Object.defineProperty(target, key, {
|
|
25
|
+
value,
|
|
26
|
+
enumerable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function isSchemaLike(value) {
|
|
32
|
+
return (typeof value === "object" &&
|
|
33
|
+
value !== null &&
|
|
34
|
+
typeof value._type === "string");
|
|
35
|
+
}
|
|
36
|
+
function isOptionalSchema(value) {
|
|
37
|
+
return isSchemaLike(value) && value._type === "optional";
|
|
6
38
|
}
|
|
7
39
|
function extractMeta(schema) {
|
|
8
|
-
const
|
|
9
|
-
if (!
|
|
40
|
+
const meta = schema._metadata;
|
|
41
|
+
if (!meta)
|
|
10
42
|
return {};
|
|
11
43
|
return {
|
|
12
|
-
...(
|
|
13
|
-
...(
|
|
14
|
-
...(
|
|
15
|
-
...(
|
|
44
|
+
...(meta.description ? { description: meta.description } : {}),
|
|
45
|
+
...(meta.example !== undefined ? { example: meta.example } : {}),
|
|
46
|
+
...(meta.deprecated ? { deprecated: true } : {}),
|
|
47
|
+
...(meta.title ? { title: meta.title } : {}),
|
|
16
48
|
};
|
|
17
49
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
function config(schema) {
|
|
51
|
+
const raw = schema["_config"];
|
|
52
|
+
return typeof raw === "object" && raw !== null
|
|
53
|
+
? raw
|
|
54
|
+
: {};
|
|
55
|
+
}
|
|
56
|
+
function num(value) {
|
|
57
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
58
|
+
? value
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
/** True when the document being produced follows OpenAPI 3.1 or later. */
|
|
62
|
+
export function isVersion31(version) {
|
|
63
|
+
return !version.startsWith("3.0");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Applies nullability in the way the target version expresses it.
|
|
67
|
+
*
|
|
68
|
+
* 3.1 dropped `nullable` in favour of a type union, so emitting `nullable`
|
|
69
|
+
* into a 3.1 document produces a schema validators quietly ignore.
|
|
70
|
+
*/
|
|
71
|
+
function applyNullable(schema, version) {
|
|
72
|
+
if (!isVersion31(version)) {
|
|
73
|
+
return { ...schema, nullable: true };
|
|
74
|
+
}
|
|
75
|
+
if (schema.type === undefined) {
|
|
76
|
+
// No type to widen — express it as a union with the null type.
|
|
77
|
+
return { ...schema, anyOf: [...(schema.anyOf ?? []), { type: "null" }] };
|
|
78
|
+
}
|
|
79
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
80
|
+
if (types.includes("null"))
|
|
81
|
+
return schema;
|
|
82
|
+
return { ...schema, type: [...types, "null"] };
|
|
83
|
+
}
|
|
84
|
+
function convertString(schema, state) {
|
|
85
|
+
const c = config(schema);
|
|
86
|
+
const format = typeof c["format"] === "string" ? c["format"] : undefined;
|
|
87
|
+
const pattern = c["pattern"];
|
|
88
|
+
const exact = num(c["length"]);
|
|
89
|
+
if (format !== undefined && STRING_FORMATS[format] === undefined) {
|
|
90
|
+
state.warnings.push(`String format "${format}" has no OpenAPI equivalent; the constraint ` +
|
|
91
|
+
`was dropped from the schema.`);
|
|
92
|
+
}
|
|
93
|
+
if (pattern instanceof RegExp) {
|
|
94
|
+
// `pattern` in JSON Schema carries no flags. An `i` regex silently
|
|
95
|
+
// becomes case-sensitive in the document, so the published contract is
|
|
96
|
+
// stricter than the code that validates against it — the kind of drift
|
|
97
|
+
// that only shows up as a rejected request in production.
|
|
98
|
+
const meaningful = pattern.flags.replace(/[gy]/g, "");
|
|
99
|
+
if (meaningful.length > 0) {
|
|
100
|
+
state.warnings.push(`Pattern /${pattern.source}/${pattern.flags} has flags OpenAPI ` +
|
|
101
|
+
`cannot express; the emitted pattern is case- and mode-sensitive.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (pattern !== undefined && typeof pattern !== "string") {
|
|
105
|
+
state.warnings.push("A `pattern` constraint was not a RegExp and could not be emitted.");
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
type: "string",
|
|
109
|
+
...(format && STRING_FORMATS[format]
|
|
110
|
+
? { format: STRING_FORMATS[format] }
|
|
111
|
+
: {}),
|
|
112
|
+
...(exact !== undefined
|
|
113
|
+
? { minLength: exact, maxLength: exact }
|
|
114
|
+
: {
|
|
115
|
+
...(num(c["min"]) !== undefined ? { minLength: num(c["min"]) } : {}),
|
|
116
|
+
...(num(c["max"]) !== undefined ? { maxLength: num(c["max"]) } : {}),
|
|
117
|
+
}),
|
|
118
|
+
...(pattern instanceof RegExp
|
|
119
|
+
? { pattern: pattern.source }
|
|
120
|
+
: typeof pattern === "string"
|
|
121
|
+
? { pattern }
|
|
122
|
+
: {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Renders an exclusive bound the way the target version spells it.
|
|
127
|
+
*
|
|
128
|
+
* 3.1 (JSON Schema 2020-12) gives `exclusiveMinimum` the numeric bound. 3.0
|
|
129
|
+
* defines it as a *boolean* modifier on `minimum`, so emitting the number
|
|
130
|
+
* into a 3.0 document produces a keyword of the wrong type: a strict
|
|
131
|
+
* validator rejects the document and a lenient one ignores the bound — either
|
|
132
|
+
* way the constraint is gone.
|
|
133
|
+
*/
|
|
134
|
+
function exclusiveBound(kind, value, version) {
|
|
135
|
+
if (isVersion31(version)) {
|
|
136
|
+
return kind === "minimum"
|
|
137
|
+
? { exclusiveMinimum: value }
|
|
138
|
+
: { exclusiveMaximum: value };
|
|
139
|
+
}
|
|
140
|
+
return kind === "minimum"
|
|
141
|
+
? { minimum: value, exclusiveMinimum: true }
|
|
142
|
+
: { maximum: value, exclusiveMaximum: true };
|
|
143
|
+
}
|
|
144
|
+
function convertNumber(schema, state) {
|
|
145
|
+
const c = config(schema);
|
|
146
|
+
const gt = num(c["gt"]);
|
|
147
|
+
const lt = num(c["lt"]);
|
|
148
|
+
const version = state.version;
|
|
149
|
+
const exclusiveMin = gt !== undefined
|
|
150
|
+
? gt
|
|
151
|
+
: c["positive"] === true && num(c["min"]) === undefined
|
|
152
|
+
? 0
|
|
153
|
+
: undefined;
|
|
154
|
+
const exclusiveMax = lt !== undefined
|
|
155
|
+
? lt
|
|
156
|
+
: c["negative"] === true && num(c["max"]) === undefined
|
|
157
|
+
? 0
|
|
158
|
+
: undefined;
|
|
159
|
+
return {
|
|
160
|
+
type: c["int"] === true ? "integer" : "number",
|
|
161
|
+
...(num(c["min"]) !== undefined ? { minimum: num(c["min"]) } : {}),
|
|
162
|
+
...(num(c["max"]) !== undefined ? { maximum: num(c["max"]) } : {}),
|
|
163
|
+
...(num(c["multipleOf"]) !== undefined
|
|
164
|
+
? { multipleOf: num(c["multipleOf"]) }
|
|
165
|
+
: {}),
|
|
166
|
+
...(exclusiveMin !== undefined
|
|
167
|
+
? exclusiveBound("minimum", exclusiveMin, version)
|
|
168
|
+
: {}),
|
|
169
|
+
...(exclusiveMax !== undefined
|
|
170
|
+
? exclusiveBound("maximum", exclusiveMax, version)
|
|
171
|
+
: {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** Infers the JSON Schema type of a literal or enum member. */
|
|
175
|
+
function typeOfValue(value) {
|
|
176
|
+
if (typeof value === "string")
|
|
177
|
+
return "string";
|
|
178
|
+
if (typeof value === "number")
|
|
179
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
180
|
+
if (typeof value === "boolean")
|
|
181
|
+
return "boolean";
|
|
182
|
+
if (value === null)
|
|
183
|
+
return "null";
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
function enumSchema(values, version) {
|
|
187
|
+
const types = new Set(values.map(typeOfValue));
|
|
188
|
+
const type = types.size === 1 ? [...types][0] : undefined;
|
|
189
|
+
// `"null"` is not a type in 3.0; leaving the enum untyped is correct there.
|
|
190
|
+
const usable = type === "null" && version !== undefined && !isVersion31(version)
|
|
191
|
+
? undefined
|
|
192
|
+
: type;
|
|
193
|
+
return {
|
|
194
|
+
...(usable ? { type: usable } : {}),
|
|
195
|
+
enum: [...values],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function convertNode(input, state) {
|
|
199
|
+
if (state.depth > state.maxDepth) {
|
|
200
|
+
state.warnings.push(`Schema nesting exceeded the maximum depth of ${state.maxDepth}.`);
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
if (!isSchemaLike(input)) {
|
|
204
|
+
return convertLiteralValue(input, state);
|
|
205
|
+
}
|
|
206
|
+
// Cycle guard. `lazy` schemas exist precisely to describe self-referential
|
|
207
|
+
// shapes, so without this a comment-with-replies schema recurses until the
|
|
208
|
+
// stack gives out.
|
|
209
|
+
if (state.visited.has(input)) {
|
|
210
|
+
state.warnings.push(`Recursive schema detected at "${input._type}"; emitted an empty schema. ` +
|
|
211
|
+
`Register the schema as a named component and reference it with $ref.`);
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
state.visited.add(input);
|
|
215
|
+
state.depth++;
|
|
216
|
+
try {
|
|
217
|
+
return { ...convertSchemaNode(input, state), ...extractMeta(input) };
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
state.depth--;
|
|
221
|
+
state.visited.delete(input);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function convertSchemaNode(schema, state) {
|
|
225
|
+
const c = config(schema);
|
|
226
|
+
switch (schema._type) {
|
|
227
|
+
case "string":
|
|
228
|
+
case "coerce.string":
|
|
229
|
+
return convertString(schema, state);
|
|
230
|
+
case "number":
|
|
231
|
+
case "coerce.number":
|
|
232
|
+
return convertNumber(schema, state);
|
|
233
|
+
case "boolean":
|
|
234
|
+
case "coerce.boolean":
|
|
235
|
+
return { type: "boolean" };
|
|
236
|
+
case "coerce.bigint":
|
|
237
|
+
return { type: "string", format: "int64" };
|
|
238
|
+
case "null":
|
|
239
|
+
// 3.0 has no `null` type; `nullable` on an untyped schema is the
|
|
240
|
+
// closest it can express.
|
|
241
|
+
return isVersion31(state.version)
|
|
242
|
+
? { type: "null" }
|
|
243
|
+
: { nullable: true };
|
|
244
|
+
case "any":
|
|
245
|
+
case "unknown":
|
|
246
|
+
return {};
|
|
247
|
+
case "never":
|
|
248
|
+
return { not: {} };
|
|
249
|
+
case "undefined":
|
|
250
|
+
state.warnings.push("An `undefined` schema has no OpenAPI equivalent; emitted an empty schema.");
|
|
251
|
+
return {};
|
|
252
|
+
case "object": {
|
|
253
|
+
const shape = c["shape"] ?? {};
|
|
254
|
+
const requiredKeys = c["requiredKeys"];
|
|
255
|
+
const properties = {};
|
|
256
|
+
const required = [];
|
|
257
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
258
|
+
defineProperty(properties, key, convertNode(value, state));
|
|
259
|
+
// A field is required unless it is wrapped in `optional`. An explicit
|
|
260
|
+
// `requiredKeys` set (from `.required()`) forces it back on.
|
|
261
|
+
const forced = requiredKeys instanceof Set && requiredKeys.has(key);
|
|
262
|
+
if (forced || !isOptionalSchema(value))
|
|
263
|
+
required.push(key);
|
|
49
264
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
265
|
+
const unknownKeys = c["unknownKeys"];
|
|
266
|
+
return {
|
|
267
|
+
type: "object",
|
|
268
|
+
...(Object.keys(properties).length > 0 ? { properties } : {}),
|
|
269
|
+
...(required.length > 0 ? { required } : {}),
|
|
270
|
+
...(unknownKeys === "strip" || unknownKeys === "strict"
|
|
271
|
+
? { additionalProperties: false }
|
|
272
|
+
: {}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
case "record": {
|
|
276
|
+
const valueSchema = c["_valueSchema"] ?? schema["_valueSchema"];
|
|
277
|
+
return {
|
|
278
|
+
type: "object",
|
|
279
|
+
additionalProperties: valueSchema === undefined ? true : convertNode(valueSchema, state),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
case "array": {
|
|
283
|
+
const items = c["itemSchema"];
|
|
284
|
+
const exact = num(c["length"]);
|
|
285
|
+
return {
|
|
286
|
+
type: "array",
|
|
287
|
+
...(items !== undefined ? { items: convertNode(items, state) } : {}),
|
|
288
|
+
...(exact !== undefined
|
|
289
|
+
? { minItems: exact, maxItems: exact }
|
|
290
|
+
: {
|
|
291
|
+
...(num(c["min"]) !== undefined
|
|
292
|
+
? { minItems: num(c["min"]) }
|
|
293
|
+
: {}),
|
|
294
|
+
...(num(c["max"]) !== undefined
|
|
295
|
+
? { maxItems: num(c["max"]) }
|
|
296
|
+
: {}),
|
|
297
|
+
}),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
case "set": {
|
|
301
|
+
const valueSchema = schema["_valueSchema"];
|
|
302
|
+
return {
|
|
303
|
+
type: "array",
|
|
304
|
+
uniqueItems: true,
|
|
305
|
+
...(valueSchema !== undefined
|
|
306
|
+
? { items: convertNode(valueSchema, state) }
|
|
307
|
+
: {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
case "map": {
|
|
311
|
+
const valueSchema = schema["_valueSchema"];
|
|
312
|
+
state.warnings.push("A `map` schema is represented as an object with additionalProperties; " +
|
|
313
|
+
"non-string keys cannot be expressed in OpenAPI.");
|
|
314
|
+
return {
|
|
315
|
+
type: "object",
|
|
316
|
+
additionalProperties: valueSchema === undefined ? true : convertNode(valueSchema, state),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
case "tuple": {
|
|
320
|
+
const schemas = schema["_schemas"];
|
|
321
|
+
if (!Array.isArray(schemas))
|
|
322
|
+
return { type: "array" };
|
|
323
|
+
const items = schemas.map((entry) => convertNode(entry, state));
|
|
324
|
+
if (!isVersion31(state.version)) {
|
|
325
|
+
// 3.0 has no positional items. `anyOf` over the member schemas keeps
|
|
326
|
+
// the length constraint honest without claiming a per-position type
|
|
327
|
+
// the version cannot express; emitting `prefixItems` instead produces
|
|
328
|
+
// a keyword every 3.0 validator ignores.
|
|
329
|
+
state.warnings.push("A `tuple` schema cannot express positional item types in " +
|
|
330
|
+
"OpenAPI 3.0; emitted a length-constrained array instead.");
|
|
59
331
|
return {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
},
|
|
65
|
-
warnings,
|
|
332
|
+
type: "array",
|
|
333
|
+
...(items.length > 0 ? { items: { anyOf: items } } : {}),
|
|
334
|
+
minItems: items.length,
|
|
335
|
+
maxItems: items.length,
|
|
66
336
|
};
|
|
67
337
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const warnings = [];
|
|
81
|
-
const oneOf = options.map((option) => {
|
|
82
|
-
const converted = convertSchema(option, _visited);
|
|
83
|
-
warnings.push(...converted.warnings);
|
|
84
|
-
return converted.schema;
|
|
85
|
-
});
|
|
86
|
-
return { schema: { oneOf, ...meta }, warnings };
|
|
338
|
+
return {
|
|
339
|
+
type: "array",
|
|
340
|
+
prefixItems: items,
|
|
341
|
+
minItems: items.length,
|
|
342
|
+
maxItems: items.length,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
case "enum": {
|
|
346
|
+
const values = schema["_values"];
|
|
347
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
348
|
+
state.warnings.push("An `enum` schema had no values; `enum` must be non-empty in OpenAPI.");
|
|
349
|
+
return {};
|
|
87
350
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return convertSchema(inner, _visited);
|
|
351
|
+
return enumSchema(values, state.version);
|
|
352
|
+
}
|
|
353
|
+
case "literal": {
|
|
354
|
+
const value = schema["_expected"];
|
|
355
|
+
if (value === undefined) {
|
|
356
|
+
state.warnings.push("A `literal` schema had no value.");
|
|
357
|
+
return {};
|
|
96
358
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
};
|
|
359
|
+
// `const` arrived with JSON Schema 2020-12; a 3.0 document expresses a
|
|
360
|
+
// single permitted value as a one-member enum.
|
|
361
|
+
return isVersion31(state.version)
|
|
362
|
+
? { ...enumSchema([value], state.version), const: value }
|
|
363
|
+
: enumSchema([value], state.version);
|
|
364
|
+
}
|
|
365
|
+
case "union": {
|
|
366
|
+
const schemas = schema["_schemas"];
|
|
367
|
+
if (!Array.isArray(schemas) || schemas.length === 0) {
|
|
368
|
+
state.warnings.push("A `union` schema had no members.");
|
|
369
|
+
return {};
|
|
109
370
|
}
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
schema: {},
|
|
113
|
-
warnings: [
|
|
114
|
-
`Unsupported schema type: ${schema._type ?? "unknown"}`,
|
|
115
|
-
],
|
|
116
|
-
};
|
|
371
|
+
return { anyOf: schemas.map((entry) => convertNode(entry, state)) };
|
|
117
372
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
373
|
+
case "discriminatedUnion": {
|
|
374
|
+
const map = schema["_schemaMap"];
|
|
375
|
+
const discriminator = schema["_discriminator"];
|
|
376
|
+
const members = map instanceof Map ? [...map.values()] : [];
|
|
377
|
+
if (members.length === 0) {
|
|
378
|
+
state.warnings.push("A `discriminatedUnion` schema had no members.");
|
|
379
|
+
return {};
|
|
380
|
+
}
|
|
123
381
|
return {
|
|
124
|
-
|
|
125
|
-
|
|
382
|
+
oneOf: members.map((entry) => convertNode(entry, state)),
|
|
383
|
+
...(typeof discriminator === "string"
|
|
384
|
+
? { discriminator: { propertyName: discriminator } }
|
|
385
|
+
: {}),
|
|
126
386
|
};
|
|
127
387
|
}
|
|
128
|
-
|
|
388
|
+
case "intersection": {
|
|
389
|
+
const left = schema["_left"];
|
|
390
|
+
const right = schema["_right"];
|
|
391
|
+
const parts = [left, right].filter((part) => part !== undefined);
|
|
392
|
+
if (parts.length === 0) {
|
|
393
|
+
state.warnings.push("An `intersection` schema had no members.");
|
|
394
|
+
return {};
|
|
395
|
+
}
|
|
396
|
+
return { allOf: parts.map((part) => convertNode(part, state)) };
|
|
397
|
+
}
|
|
398
|
+
case "optional":
|
|
399
|
+
// Optionality is expressed by the parent's `required` list, not by the
|
|
400
|
+
// property schema, so the wrapper contributes nothing of its own.
|
|
401
|
+
return convertInner(schema["_inner"], state, "optional");
|
|
402
|
+
case "nullable":
|
|
403
|
+
return applyNullable(convertInner(schema["_inner"], state, "nullable"), state.version);
|
|
404
|
+
case "default": {
|
|
405
|
+
const inner = convertInner(schema["_inner"], state, "default");
|
|
406
|
+
const defaultValue = schema["_defaultValue"];
|
|
407
|
+
return defaultValue === undefined
|
|
408
|
+
? inner
|
|
409
|
+
: { ...inner, default: defaultValue };
|
|
410
|
+
}
|
|
411
|
+
case "refine":
|
|
412
|
+
// A refinement is a runtime predicate with no schema equivalent; the
|
|
413
|
+
// inner shape is still the right description of the data.
|
|
414
|
+
return convertInner(schema["_inner"], state, "refine");
|
|
415
|
+
case "transform":
|
|
416
|
+
return convertInner(schema["_base"], state, "transform");
|
|
417
|
+
case "lazy": {
|
|
418
|
+
const resolved = schema["_inner"] ?? resolveLazy(schema, state);
|
|
419
|
+
if (resolved === undefined)
|
|
420
|
+
return {};
|
|
421
|
+
return convertNode(resolved, state);
|
|
422
|
+
}
|
|
423
|
+
default:
|
|
424
|
+
state.warnings.push(`Unsupported schema type: ${schema._type}`);
|
|
425
|
+
return {};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function resolveLazy(schema, state) {
|
|
429
|
+
const factory = schema["_factory"];
|
|
430
|
+
if (typeof factory !== "function")
|
|
431
|
+
return undefined;
|
|
432
|
+
try {
|
|
433
|
+
return factory();
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
state.warnings.push(`A \`lazy\` schema could not be resolved: ${error instanceof Error ? error.message : String(error)}`);
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function convertInner(inner, state, wrapper) {
|
|
441
|
+
if (inner === undefined) {
|
|
442
|
+
state.warnings.push(`A \`${wrapper}\` schema has no inner schema.`);
|
|
443
|
+
return {};
|
|
444
|
+
}
|
|
445
|
+
return convertNode(inner, state);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Converts a bare JavaScript value used as a shorthand schema.
|
|
449
|
+
*
|
|
450
|
+
* `["a"]` means "array of string" and `"x"` means "string" — a convenience
|
|
451
|
+
* for hand-written metadata, not for real schemas.
|
|
452
|
+
*/
|
|
453
|
+
function convertLiteralValue(input, state) {
|
|
454
|
+
if (Array.isArray(input)) {
|
|
455
|
+
const first = input[0];
|
|
456
|
+
if (first === undefined)
|
|
457
|
+
return { type: "array" };
|
|
458
|
+
return { type: "array", items: convertNode(first, state) };
|
|
129
459
|
}
|
|
130
460
|
if (typeof input === "string")
|
|
131
|
-
return {
|
|
132
|
-
if (typeof input === "number")
|
|
133
|
-
return {
|
|
461
|
+
return { type: "string" };
|
|
462
|
+
if (typeof input === "number") {
|
|
463
|
+
return { type: Number.isInteger(input) ? "integer" : "number" };
|
|
464
|
+
}
|
|
134
465
|
if (typeof input === "boolean")
|
|
135
|
-
return {
|
|
136
|
-
|
|
466
|
+
return { type: "boolean" };
|
|
467
|
+
if (input === null) {
|
|
468
|
+
return isVersion31(state.version) ? { type: "null" } : { nullable: true };
|
|
469
|
+
}
|
|
470
|
+
state.warnings.push("Unable to convert unknown schema input.");
|
|
471
|
+
return {};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Converts a schema into an OpenAPI schema object.
|
|
475
|
+
*
|
|
476
|
+
* Warnings describe everything that could not be represented exactly. They
|
|
477
|
+
* are part of the result rather than a side channel, because silently
|
|
478
|
+
* emitting `{}` for an unsupported construct is how a specification ends up
|
|
479
|
+
* documenting nothing.
|
|
480
|
+
*/
|
|
481
|
+
export function convertSchema(input, options) {
|
|
482
|
+
const state = {
|
|
483
|
+
warnings: [],
|
|
484
|
+
visited: new Set(),
|
|
485
|
+
version: options?.version ?? DEFAULT_OPENAPI_VERSION,
|
|
486
|
+
maxDepth: options?.maxDepth ?? 32,
|
|
487
|
+
depth: 0,
|
|
488
|
+
};
|
|
489
|
+
const schema = convertNode(input, state);
|
|
490
|
+
return { schema, warnings: state.warnings };
|
|
137
491
|
}
|
|
138
|
-
|
|
139
|
-
|
|
492
|
+
/** Creates a converter bound to a specification version. */
|
|
493
|
+
export function createSchemaConverter(defaults) {
|
|
494
|
+
return {
|
|
495
|
+
convert: (input, options) => convertSchema(input, { ...defaults, ...options }),
|
|
496
|
+
};
|
|
140
497
|
}
|
|
141
498
|
//# sourceMappingURL=schemaConverter.core.js.map
|