@sezzlee/openapi 0.0.0-stage → 0.2.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.
@@ -0,0 +1,50 @@
1
+ import { childPointer } from "../ir/brand.js";
2
+ import { arrayOf, objectOf, stringOf } from "../ir/json.js";
3
+ /**
4
+ * Resolves the first server of the nearest non-empty list among operation and path item, else the
5
+ * root's. A configured `baseUrl` replaces the root list; it does not override a server an
6
+ * operation or path item declares for itself.
7
+ *
8
+ * @param lists the operation's and the path item's lists, nearest first
9
+ */
10
+ export function serverOf(lists, root, options, diagnostics) {
11
+ const own = lists.find((list) => arrayOf(list.servers).length > 0);
12
+ if (own === undefined && options.baseUrl !== undefined) {
13
+ return stripTrailingSlash(options.baseUrl);
14
+ }
15
+ const chosen = own ?? (arrayOf(root.servers).length > 0 ? root : undefined);
16
+ if (chosen === undefined) {
17
+ return resolve("/", undefined, options, diagnostics);
18
+ }
19
+ const at = childPointer(chosen.at, 0);
20
+ const server = objectOf(arrayOf(chosen.servers)[0]);
21
+ const template = stringOf(server?.["url"]) ?? "/";
22
+ const url = template.replace(/\{([^}]+)\}/g, (_match, name) => {
23
+ const variable = objectOf(objectOf(server?.["variables"])?.[name]);
24
+ const value = options.variables?.[name] ?? stringOf(variable?.["default"]);
25
+ const allowed = arrayOf(variable?.["enum"])
26
+ .map(stringOf)
27
+ .filter((item) => item !== undefined);
28
+ if (value === undefined) {
29
+ diagnostics.report("server_variable_invalid", childPointer(at, "variables", name), `Server variable '${name}' has no default and no configured value.`);
30
+ return "";
31
+ }
32
+ if (allowed.length > 0 && !allowed.includes(value)) {
33
+ diagnostics.report("server_variable_invalid", childPointer(at, "variables", name), `Server variable '${name}' is '${value}', which its enum does not list.`);
34
+ }
35
+ return value;
36
+ });
37
+ return resolve(url, at, options, diagnostics);
38
+ }
39
+ function resolve(url, at, options, diagnostics) {
40
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
41
+ return stripTrailingSlash(url);
42
+ }
43
+ if (options.documentUrl !== undefined &&
44
+ /^https?:/i.test(options.documentUrl)) {
45
+ return stripTrailingSlash(new URL(url, options.documentUrl).toString());
46
+ }
47
+ diagnostics.report("server_url_unresolvable", at ?? "", `Server URL '${url}' is relative and the document was not read from a URL; configure a base URL.`);
48
+ return undefined;
49
+ }
50
+ const stripTrailingSlash = (url) => url.replace(/\/+$/, "");
@@ -0,0 +1,9 @@
1
+ import type { DiagnosticSink } from "../diagnostics.js";
2
+ import { type JsonObject } from "../ir/json.js";
3
+ /**
4
+ * Reads one referenced document. The host decides what may be read: an allowlist of hosts, a
5
+ * deadline and a size limit for a URL, and a root directory for a file. An external reference is a
6
+ * way for a document's author to make the ingesting process issue a request.
7
+ */
8
+ export type DocumentLoader = (url: URL) => Promise<string>;
9
+ export declare function bundleExternal(document: JsonObject, documentUrl: string | undefined, loader: DocumentLoader | undefined, diagnostics: DiagnosticSink): Promise<JsonObject | undefined>;
@@ -0,0 +1,93 @@
1
+ import { childPointer, rootPointer } from "../ir/brand.js";
2
+ import { entriesOf, isObject, } from "../ir/json.js";
3
+ import { parseDocument } from "../parse/parse.js";
4
+ import { resolvePointer } from "./refs.js";
5
+ const maxDepth = 32;
6
+ function firstExternalRef(value, at) {
7
+ if (Array.isArray(value)) {
8
+ for (const [index, item] of value.entries()) {
9
+ const found = firstExternalRef(item, childPointer(at, index));
10
+ if (found !== undefined) {
11
+ return found;
12
+ }
13
+ }
14
+ return undefined;
15
+ }
16
+ if (!isObject(value)) {
17
+ return undefined;
18
+ }
19
+ for (const [key, child] of entriesOf(value)) {
20
+ if (key === "$ref" && typeof child === "string") {
21
+ if (!child.startsWith("#")) {
22
+ return childPointer(at, key);
23
+ }
24
+ continue;
25
+ }
26
+ const found = firstExternalRef(child, childPointer(at, key));
27
+ if (found !== undefined) {
28
+ return found;
29
+ }
30
+ }
31
+ return undefined;
32
+ }
33
+ function load(bundler, url) {
34
+ const key = url.href.replace(/#.*$/, "");
35
+ let pending = bundler.cache.get(key);
36
+ if (pending === undefined) {
37
+ pending = bundler
38
+ .loader(new URL(key))
39
+ .then((text) => parseDocument(text, bundler.diagnostics));
40
+ bundler.cache.set(key, pending);
41
+ }
42
+ return pending;
43
+ }
44
+ /**
45
+ * Inlines every external reference. The referenced fragment is inlined with its own references
46
+ * resolved against its own document, so the result has only local references left.
47
+ */
48
+ async function inline(bundler, value, base, document, at, depth, local) {
49
+ if (Array.isArray(value)) {
50
+ return Promise.all(value.map((item, index) => inline(bundler, item, base, document, childPointer(at, index), depth, local)));
51
+ }
52
+ if (!isObject(value)) {
53
+ return value;
54
+ }
55
+ const ref = value["$ref"];
56
+ if (typeof ref === "string" && (!ref.startsWith("#") || !local)) {
57
+ if (depth >= maxDepth) {
58
+ bundler.diagnostics.report("external_ref_blocked", at, `Reference '${ref}' nests more than ${String(maxDepth)} documents deep.`);
59
+ return {};
60
+ }
61
+ const target = new URL(ref, base);
62
+ const targetDocument = ref.startsWith("#")
63
+ ? document
64
+ : await load(bundler, target);
65
+ if (targetDocument === undefined) {
66
+ return {};
67
+ }
68
+ const fragment = resolvePointer(targetDocument, target.hash);
69
+ if (fragment === undefined) {
70
+ bundler.diagnostics.report("external_ref_blocked", at, `Reference '${ref}' resolves to nothing.`);
71
+ return {};
72
+ }
73
+ return inline(bundler, fragment, target, targetDocument, at, depth + 1, false);
74
+ }
75
+ const out = {};
76
+ for (const [key, child] of entriesOf(value)) {
77
+ out[key] = await inline(bundler, child, base, document, childPointer(at, key), depth, local);
78
+ }
79
+ return out;
80
+ }
81
+ export async function bundleExternal(document, documentUrl, loader, diagnostics) {
82
+ const external = firstExternalRef(document, rootPointer);
83
+ if (external === undefined) {
84
+ return document;
85
+ }
86
+ if (loader === undefined || documentUrl === undefined) {
87
+ diagnostics.report("external_ref_blocked", external, "The document references another document, and no loader was configured to read it.");
88
+ return undefined;
89
+ }
90
+ const bundler = { loader, diagnostics, cache: new Map() };
91
+ const bundled = await inline(bundler, document, new URL(documentUrl), document, rootPointer, 0, true);
92
+ return isObject(bundled) ? bundled : undefined;
93
+ }
@@ -0,0 +1,15 @@
1
+ import { type JsonPointer } from "../ir/brand.js";
2
+ import { type JsonObject, type JsonValue } from "../ir/json.js";
3
+ export declare function resolvePointer(document: JsonObject, ref: string): JsonValue | undefined;
4
+ export declare function pointerOfRef(ref: string): JsonPointer;
5
+ /**
6
+ * Follows a chain of references to a non-schema component (a parameter, request body, response,
7
+ * header or path item) and returns the object it ends at, with the pointer of that object.
8
+ *
9
+ * @throws OperationDropped `circular_component_ref` when the chain returns to a reference it
10
+ * already followed, or ends nowhere
11
+ */
12
+ export declare function resolveComponent(document: JsonObject, value: JsonValue | undefined, at: JsonPointer): {
13
+ readonly node: JsonObject;
14
+ readonly at: JsonPointer;
15
+ } | undefined;
@@ -0,0 +1,49 @@
1
+ import { OperationDropped } from "../diagnostics.js";
2
+ import { segmentsOf } from "../ir/brand.js";
3
+ import { isObject, objectOf, stringOf, } from "../ir/json.js";
4
+ export function resolvePointer(document, ref) {
5
+ let node = document;
6
+ for (const segment of segmentsOf(ref)) {
7
+ if (Array.isArray(node)) {
8
+ node = node[Number(segment)];
9
+ }
10
+ else if (isObject(node)) {
11
+ node = node[segment];
12
+ }
13
+ else {
14
+ return undefined;
15
+ }
16
+ }
17
+ return node;
18
+ }
19
+ export function pointerOfRef(ref) {
20
+ return (ref.startsWith("#") ? ref.slice(1) : ref);
21
+ }
22
+ /**
23
+ * Follows a chain of references to a non-schema component (a parameter, request body, response,
24
+ * header or path item) and returns the object it ends at, with the pointer of that object.
25
+ *
26
+ * @throws OperationDropped `circular_component_ref` when the chain returns to a reference it
27
+ * already followed, or ends nowhere
28
+ */
29
+ export function resolveComponent(document, value, at) {
30
+ let node = objectOf(value);
31
+ let where = at;
32
+ const seen = new Set();
33
+ while (node !== undefined) {
34
+ const ref = stringOf(node["$ref"]);
35
+ if (ref === undefined) {
36
+ return { node, at: where };
37
+ }
38
+ if (seen.has(ref) || !ref.startsWith("#")) {
39
+ throw new OperationDropped("circular_component_ref", where, `Reference '${ref}' ${seen.has(ref) ? "returns to itself" : "is not local"}.`);
40
+ }
41
+ seen.add(ref);
42
+ where = pointerOfRef(ref);
43
+ node = objectOf(resolvePointer(document, ref));
44
+ if (node === undefined) {
45
+ throw new OperationDropped("circular_component_ref", where, `Reference '${ref}' resolves to nothing.`);
46
+ }
47
+ }
48
+ return undefined;
49
+ }
@@ -0,0 +1,24 @@
1
+ import type { JsonSchemaObject } from "@sezzlee/core";
2
+ import { type DiagnosticSink } from "../diagnostics.js";
3
+ import { type JsonPointer } from "../ir/brand.js";
4
+ import { type JsonObject, type JsonValue } from "../ir/json.js";
5
+ import type { DocumentVersion } from "../parse/parse.js";
6
+ export type Direction = "request" | "response";
7
+ export interface SchemaContext {
8
+ readonly document: JsonObject;
9
+ readonly version: DocumentVersion;
10
+ readonly diagnostics: DiagnosticSink;
11
+ }
12
+ /**
13
+ * @param root how a slot whose top level is a reference is treated: `required` dereferences it or
14
+ * drops the operation (a parameter's kind is read from its top-level type), `preferred`
15
+ * dereferences it when that terminates and keeps the reference otherwise
16
+ * @param fileMediaType the media type a `format: binary` string becomes, from a multipart
17
+ * `encoding` entry
18
+ */
19
+ export interface SlotOptions {
20
+ readonly direction: Direction;
21
+ readonly root: "required" | "preferred";
22
+ readonly fileMediaType?: (property: string) => string | undefined;
23
+ }
24
+ export declare function normalizeSlot(context: SchemaContext, schema: JsonValue | undefined, at: JsonPointer, options: SlotOptions): JsonSchemaObject;
@@ -0,0 +1,354 @@
1
+ import { OperationDropped } from "../diagnostics.js";
2
+ import { childPointer, segmentsOf } from "../ir/brand.js";
3
+ import { entriesOf, isObject, stringOf, } from "../ir/json.js";
4
+ import { pointerOfRef, resolvePointer } from "./refs.js";
5
+ const subschemaMaps = [
6
+ "properties",
7
+ "patternProperties",
8
+ "dependentSchemas",
9
+ "$defs",
10
+ ];
11
+ const subschemaSingles = [
12
+ "items",
13
+ "additionalProperties",
14
+ "not",
15
+ "contains",
16
+ "propertyNames",
17
+ "if",
18
+ "then",
19
+ "else",
20
+ "unevaluatedItems",
21
+ "unevaluatedProperties",
22
+ ];
23
+ const subschemaLists = ["allOf", "anyOf", "oneOf", "prefixItems"];
24
+ const annotations = new Set(["discriminator", "xml", "externalDocs"]);
25
+ const escapeDefName = (name) => name.replaceAll("~", "~0").replaceAll("/", "~1");
26
+ class SlotNormalizer {
27
+ context;
28
+ options;
29
+ defs = new Map();
30
+ names = new Map();
31
+ queue = [];
32
+ constructor(context, options) {
33
+ this.context = context;
34
+ this.options = options;
35
+ }
36
+ normalize(schema, at) {
37
+ let root = this.convert(schema, at, undefined);
38
+ while (this.queue.length > 0) {
39
+ const ref = this.queue.shift();
40
+ const name = this.names.get(ref);
41
+ const target = resolvePointer(this.context.document, ref);
42
+ if (target === undefined) {
43
+ this.context.diagnostics.report("openapi_document_invalid", pointerOfRef(ref), `Reference '${ref}' resolves to nothing; it is treated as an unconstrained schema.`);
44
+ }
45
+ this.defs.set(name, this.convert(target, pointerOfRef(ref), undefined));
46
+ }
47
+ root = this.dereferenceRoot(root, at);
48
+ const used = this.referencedDefs(root);
49
+ const defs = [...this.defs.entries()].filter(([name]) => used.has(name));
50
+ if (defs.length === 0) {
51
+ return root;
52
+ }
53
+ return { ...root, $defs: Object.fromEntries(defs) };
54
+ }
55
+ dereferenceRoot(root, at) {
56
+ const nullableRef = nullableReference(root);
57
+ if (nullableRef !== undefined) {
58
+ const inner = this.dereferenceRoot(nullableRef, at);
59
+ return inner === nullableRef
60
+ ? root
61
+ : this.nullable({ nullable: true }, inner);
62
+ }
63
+ let current = root;
64
+ const seen = new Set();
65
+ while (Object.keys(current).length === 1 && current.$ref !== undefined) {
66
+ const name = defNameOf(current.$ref);
67
+ if (seen.has(name) || !this.defs.has(name)) {
68
+ if (this.options.root === "required") {
69
+ throw new OperationDropped("recursive_parameter_schema", at, "The parameter's schema refers to itself at its top level, so it has no scalar or array type to write.");
70
+ }
71
+ return root;
72
+ }
73
+ seen.add(name);
74
+ current = this.defs.get(name);
75
+ }
76
+ return current;
77
+ }
78
+ referencedDefs(root) {
79
+ const used = new Set();
80
+ const pending = [root];
81
+ while (pending.length > 0) {
82
+ const node = pending.pop();
83
+ if (Array.isArray(node)) {
84
+ pending.push(...node);
85
+ }
86
+ else if (isObject(node)) {
87
+ for (const [key, value] of entriesOf(node)) {
88
+ if (key === "$ref" && typeof value === "string") {
89
+ const name = defNameOf(value);
90
+ if (!used.has(name) && this.defs.has(name)) {
91
+ used.add(name);
92
+ pending.push(this.defs.get(name));
93
+ }
94
+ }
95
+ else {
96
+ pending.push(value);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ return used;
102
+ }
103
+ nameFor(ref) {
104
+ const known = this.names.get(ref);
105
+ if (known !== undefined) {
106
+ return known;
107
+ }
108
+ const segments = segmentsOf(ref);
109
+ const base = segments.length === 3 &&
110
+ segments[0] === "components" &&
111
+ segments[1] === "schemas"
112
+ ? segments[2]
113
+ : segments.join("_");
114
+ let name = base;
115
+ const taken = new Set(this.names.values());
116
+ for (let suffix = 2; taken.has(name); suffix += 1) {
117
+ name = `${base}_${suffix}`;
118
+ }
119
+ this.names.set(ref, name);
120
+ this.queue.push(ref);
121
+ return name;
122
+ }
123
+ convert(value, at, property) {
124
+ if (value === true || value === undefined) {
125
+ return {};
126
+ }
127
+ if (value === false) {
128
+ return { not: {} };
129
+ }
130
+ if (!isObject(value)) {
131
+ return {};
132
+ }
133
+ const ref = stringOf(value["$ref"]);
134
+ if (ref !== undefined && ref.startsWith("#")) {
135
+ return this.convertRef(value, ref, at, property);
136
+ }
137
+ const out = {};
138
+ for (const [key, child] of entriesOf(value)) {
139
+ const childAt = childPointer(at, key);
140
+ if (key === "nullable" || key === "default" || key === "$ref") {
141
+ continue;
142
+ }
143
+ if (key.startsWith("x-") || annotations.has(key)) {
144
+ this.context.diagnostics.report("annotation_removed", childAt, `Schema annotations such as '${key.startsWith("x-") ? "x-*" : key}' change nothing the composer writes and are removed.`);
145
+ continue;
146
+ }
147
+ if (key === "example") {
148
+ if (value["examples"] === undefined) {
149
+ out["examples"] = [child];
150
+ }
151
+ continue;
152
+ }
153
+ if (key === "format" && child === "binary") {
154
+ out["contentMediaType"] =
155
+ (property === undefined
156
+ ? undefined
157
+ : this.options.fileMediaType?.(property)) ??
158
+ "application/octet-stream";
159
+ continue;
160
+ }
161
+ if (key === "format" && child === "byte") {
162
+ out["contentEncoding"] = "base64";
163
+ continue;
164
+ }
165
+ if ((key === "exclusiveMinimum" || key === "exclusiveMaximum") &&
166
+ typeof child === "boolean") {
167
+ const bound = key === "exclusiveMinimum" ? "minimum" : "maximum";
168
+ if (child && typeof value[bound] === "number") {
169
+ out[key] = value[bound];
170
+ }
171
+ continue;
172
+ }
173
+ if ((key === "minimum" || key === "maximum") &&
174
+ value[key === "minimum" ? "exclusiveMinimum" : "exclusiveMaximum"] ===
175
+ true) {
176
+ continue;
177
+ }
178
+ if (subschemaMaps.includes(key)) {
179
+ out[key] = this.convertMap(key, child, childAt);
180
+ continue;
181
+ }
182
+ if (subschemaSingles.includes(key)) {
183
+ out[key] =
184
+ typeof child === "boolean"
185
+ ? child
186
+ : this.convert(child, childAt, undefined);
187
+ continue;
188
+ }
189
+ if (subschemaLists.includes(key)) {
190
+ out[key] = (Array.isArray(child) ? child : []).map((member, index) => this.convert(member, childPointer(childAt, index), undefined));
191
+ continue;
192
+ }
193
+ out[key] = child;
194
+ }
195
+ if (Array.isArray(out["required"]) && isObject(out["properties"])) {
196
+ const kept = new Set(Object.keys(out["properties"]));
197
+ out["required"] = out["required"].filter((name) => typeof name === "string" && kept.has(name));
198
+ }
199
+ const merged = mergeAllOf(out);
200
+ return this.nullable(value, merged);
201
+ }
202
+ convertRef(value, ref, at, property) {
203
+ const target = {
204
+ $ref: `#/$defs/${escapeDefName(this.nameFor(ref))}`,
205
+ };
206
+ const siblings = entriesOf(value).filter(([key]) => key !== "$ref" && key !== "nullable");
207
+ if (this.context.version === "3.0" && siblings.length > 0) {
208
+ this.context.diagnostics.report("ref_siblings_ignored", at, "OpenAPI 3.0 ignores keywords next to $ref; only 'nullable' is honoured beside one.");
209
+ return this.nullable(value, target);
210
+ }
211
+ if (siblings.length === 0) {
212
+ return this.nullable(value, target);
213
+ }
214
+ const rest = {};
215
+ for (const [key, child] of siblings) {
216
+ rest[key] = child;
217
+ }
218
+ const converted = this.convert(rest, at, property);
219
+ return this.nullable(value, { ...converted, ...target });
220
+ }
221
+ convertMap(key, child, at) {
222
+ const out = {};
223
+ for (const [name, schema] of entriesOf(child)) {
224
+ if (key === "properties" && this.hidden(schema)) {
225
+ continue;
226
+ }
227
+ out[name] = this.convert(schema, childPointer(at, name), key === "properties" ? name : undefined);
228
+ }
229
+ return out;
230
+ }
231
+ hidden(schema) {
232
+ if (!isObject(schema)) {
233
+ return false;
234
+ }
235
+ return this.options.direction === "request"
236
+ ? schema["readOnly"] === true
237
+ : schema["writeOnly"] === true;
238
+ }
239
+ nullable(source, schema) {
240
+ if (source["nullable"] !== true) {
241
+ return schema;
242
+ }
243
+ const type = schema.type;
244
+ if (typeof type === "string") {
245
+ return {
246
+ ...schema,
247
+ type: [type, "null"],
248
+ ...(Array.isArray(schema.enum) && !schema.enum.includes(null)
249
+ ? { enum: [...schema.enum, null] }
250
+ : {}),
251
+ };
252
+ }
253
+ if (Array.isArray(type)) {
254
+ return type.includes("null")
255
+ ? schema
256
+ : { ...schema, type: [...type, "null"] };
257
+ }
258
+ return { anyOf: [schema, { type: "null" }] };
259
+ }
260
+ }
261
+ function nullableReference(schema) {
262
+ const members = schema.anyOf ?? schema.oneOf;
263
+ if (Object.keys(schema).length !== 1 || !Array.isArray(members)) {
264
+ return undefined;
265
+ }
266
+ if (members.length !== 2) {
267
+ return undefined;
268
+ }
269
+ const [first, second] = members;
270
+ const isNull = (member) => member !== undefined &&
271
+ Object.keys(member).length === 1 &&
272
+ member.type === "null";
273
+ const isRef = (member) => member !== undefined &&
274
+ Object.keys(member).length === 1 &&
275
+ member.$ref !== undefined;
276
+ if (isRef(first) && isNull(second)) {
277
+ return first;
278
+ }
279
+ return isRef(second) && isNull(first) ? second : undefined;
280
+ }
281
+ function defNameOf(ref) {
282
+ const name = ref.replace(/^#\/\$defs\//, "");
283
+ return name.replaceAll("~1", "/").replaceAll("~0", "~");
284
+ }
285
+ const structural = new Set([
286
+ "type",
287
+ "properties",
288
+ "required",
289
+ "additionalProperties",
290
+ "allOf",
291
+ ]);
292
+ /**
293
+ * Merges an `allOf` of plain object schemas into one object, so a body root built by composition
294
+ * can still be flattened. Anything that cannot be merged without choosing between two
295
+ * definitions — a reference, a combinator, two different schemas for one property — is kept as
296
+ * `allOf`, and the core sends such a body in root mode.
297
+ */
298
+ function mergeAllOf(schema) {
299
+ const members = schema["allOf"];
300
+ if (!Array.isArray(members) || members.length === 0) {
301
+ return schema;
302
+ }
303
+ const objects = members.filter(isObject);
304
+ if (objects.length !== members.length) {
305
+ return schema;
306
+ }
307
+ const others = Object.keys(schema).filter((key) => key !== "allOf");
308
+ if (objects.length === 1 &&
309
+ others.every((key) => !structural.has(key)) &&
310
+ Object.keys(objects[0]).length === 1 &&
311
+ objects[0]["$ref"] !== undefined) {
312
+ const { allOf: _allOf, ...rest } = schema;
313
+ return { ...rest, ...objects[0] };
314
+ }
315
+ const mergeable = objects.every((member) => member["$ref"] === undefined &&
316
+ Object.keys(member).every((key) => structural.has(key) || key === "description") &&
317
+ (member["type"] === undefined || member["type"] === "object"));
318
+ if (!mergeable) {
319
+ return schema;
320
+ }
321
+ const properties = {
322
+ ...(isObject(schema["properties"]) ? schema["properties"] : {}),
323
+ };
324
+ const required = new Set(Array.isArray(schema["required"])
325
+ ? schema["required"].filter((item) => typeof item === "string")
326
+ : []);
327
+ for (const member of objects) {
328
+ for (const [name, property] of entriesOf(member["properties"])) {
329
+ const existing = properties[name];
330
+ if (existing !== undefined &&
331
+ JSON.stringify(existing) !== JSON.stringify(property)) {
332
+ return schema;
333
+ }
334
+ properties[name] = property;
335
+ }
336
+ for (const name of Array.isArray(member["required"])
337
+ ? member["required"]
338
+ : []) {
339
+ if (typeof name === "string") {
340
+ required.add(name);
341
+ }
342
+ }
343
+ }
344
+ const { allOf: _allOf, ...rest } = schema;
345
+ return {
346
+ ...rest,
347
+ type: "object",
348
+ properties,
349
+ ...(required.size === 0 ? {} : { required: [...required] }),
350
+ };
351
+ }
352
+ export function normalizeSlot(context, schema, at, options) {
353
+ return new SlotNormalizer(context, options).normalize(schema, at);
354
+ }
@@ -0,0 +1,5 @@
1
+ import { DiagnosticSink } from "../diagnostics.js";
2
+ import { type JsonObject } from "../ir/json.js";
3
+ export type DocumentVersion = "2.0" | "3.0" | "3.1" | "3.2";
4
+ export declare function parseDocument(text: string, diagnostics: DiagnosticSink): JsonObject | undefined;
5
+ export declare function versionOf(document: JsonObject, diagnostics: DiagnosticSink): DocumentVersion | undefined;
@@ -0,0 +1,40 @@
1
+ import { parse as parseYaml } from "yaml";
2
+ import { childPointer, rootPointer } from "../ir/brand.js";
3
+ import { isObject, stringOf } from "../ir/json.js";
4
+ export function parseDocument(text, diagnostics) {
5
+ let value;
6
+ try {
7
+ value = JSON.parse(text);
8
+ }
9
+ catch {
10
+ try {
11
+ value = parseYaml(text, { maxAliasCount: 100 });
12
+ }
13
+ catch (error) {
14
+ diagnostics.report("openapi_document_unparseable", rootPointer, `The document is neither JSON nor YAML: ${error.message}`);
15
+ return undefined;
16
+ }
17
+ }
18
+ if (!isObject(value)) {
19
+ diagnostics.report("openapi_document_unparseable", rootPointer, "The document's root is not an object.");
20
+ return undefined;
21
+ }
22
+ return value;
23
+ }
24
+ export function versionOf(document, diagnostics) {
25
+ const swagger = stringOf(document["swagger"]);
26
+ if (swagger !== undefined) {
27
+ if (swagger === "2.0") {
28
+ return "2.0";
29
+ }
30
+ }
31
+ else {
32
+ const openapi = stringOf(document["openapi"]) ?? "";
33
+ const match = /^3\.([0-2])\.\d+$/.exec(openapi);
34
+ if (match !== null) {
35
+ return `3.${match[1]}`;
36
+ }
37
+ }
38
+ diagnostics.report("openapi_version_unsupported", childPointer(rootPointer, swagger === undefined ? "openapi" : "swagger"), `Version '${swagger ?? stringOf(document["openapi"]) ?? "absent"}' is not Swagger 2.0 or OpenAPI 3.0, 3.1 or 3.2.`);
39
+ return undefined;
40
+ }
@@ -0,0 +1,11 @@
1
+ import type { DiagnosticSink } from "../diagnostics.js";
2
+ import { type JsonPointer } from "../ir/brand.js";
3
+ import { type JsonObject } from "../ir/json.js";
4
+ /** Where a node of the upgraded document came from in the author's 2.0 document. */
5
+ export type Origins = WeakMap<object, JsonPointer>;
6
+ /**
7
+ * Converts a Swagger 2.0 document to the OpenAPI 3 shape the rest of ingestion reads, following
8
+ * swagger2openapi's rules. Nodes that move are recorded in `origins` so a diagnostic still points
9
+ * into the author's document.
10
+ */
11
+ export declare function upgradeSwagger2(document: JsonObject, diagnostics: DiagnosticSink, origins: Origins): JsonObject;