@znight/openapi 0.0.0-stage → 0.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +572 -2
  3. package/dist/build-BTUC96mY.d.mts +174 -0
  4. package/dist/build-DXunOzlI.mjs +733 -0
  5. package/dist/build-DXunOzlI.mjs.map +1 -0
  6. package/dist/compiler.d.mts +16 -0
  7. package/dist/compiler.mjs +2639 -0
  8. package/dist/compiler.mjs.map +1 -0
  9. package/dist/emit-child.d.mts +1 -0
  10. package/dist/emit-child.mjs +124 -0
  11. package/dist/emit-child.mjs.map +1 -0
  12. package/dist/emit.d.mts +2 -0
  13. package/dist/emit.mjs +252 -0
  14. package/dist/emit.mjs.map +1 -0
  15. package/dist/index-CqQ6IRnN.d.mts +60 -0
  16. package/dist/index.d.mts +260 -0
  17. package/dist/index.mjs +777 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/openapi-CQk5dHqc.mjs +39 -0
  20. package/dist/openapi-CQk5dHqc.mjs.map +1 -0
  21. package/dist/shared-BZxtyaB0.mjs +147 -0
  22. package/dist/shared-BZxtyaB0.mjs.map +1 -0
  23. package/dist/testing.d.mts +39 -0
  24. package/dist/testing.mjs +155 -0
  25. package/dist/testing.mjs.map +1 -0
  26. package/dist/types-VzBexMKj.d.mts +213 -0
  27. package/dist/vite.d.mts +24 -0
  28. package/dist/vite.mjs +64 -0
  29. package/dist/vite.mjs.map +1 -0
  30. package/package.json +115 -4
  31. package/src/build.ts +898 -0
  32. package/src/compiler/analyze.ts +81 -0
  33. package/src/compiler/document.ts +612 -0
  34. package/src/compiler/generate.ts +79 -0
  35. package/src/compiler/index.ts +47 -0
  36. package/src/compiler/model.ts +141 -0
  37. package/src/compiler/operations.ts +1475 -0
  38. package/src/compiler/schemas.ts +706 -0
  39. package/src/compiler/validate.ts +102 -0
  40. package/src/compiler/values.ts +81 -0
  41. package/src/converters.ts +137 -0
  42. package/src/decorators.ts +187 -0
  43. package/src/emit/child.ts +144 -0
  44. package/src/emit/index.ts +377 -0
  45. package/src/emit/protocol.ts +35 -0
  46. package/src/index.ts +94 -0
  47. package/src/metadata.ts +112 -0
  48. package/src/openapi.ts +55 -0
  49. package/src/plugin.ts +810 -0
  50. package/src/shared.ts +154 -0
  51. package/src/testing.ts +224 -0
  52. package/src/types.ts +240 -0
  53. package/src/ui.ts +215 -0
  54. package/src/vite.ts +126 -0
@@ -0,0 +1,733 @@
1
+ import { f as jsonEquals, l as compareStrings, m as schemaRef, n as ERROR_RESPONSES, p as reasonPhrase, t as ERROR_COMPONENTS, u as componentName } from "./shared-BZxtyaB0.mjs";
2
+ import { ErrorCode, ZnightError, displayName } from "@znight/core";
3
+ // src/metadata.ts
4
+ /** Format version of {@link OpenApiMetadata}. */
5
+ const OPENAPI_METADATA_VERSION = 1;
6
+ // src/converters.ts
7
+ function propsOf(schema) {
8
+ const props = schema["~standard"];
9
+ return typeof props === "object" && props !== null ? props : void 0;
10
+ }
11
+ /** `vendor` of a Standard Schema (`zod`, `valibot`, `arktype`...). */
12
+ function vendorOf(schema) {
13
+ const vendor = propsOf(schema)?.vendor;
14
+ return typeof vendor === "string" ? vendor : "unknown";
15
+ }
16
+ /** How a date travels in JSON: an ISO 8601 string. */
17
+ const DATE_TIME = Object.freeze({
18
+ type: "string",
19
+ format: "date-time"
20
+ });
21
+ /**
22
+ * Zod dates are serialised with `Date.prototype.toJSON`: in responses they are ISO strings. A
23
+ * coerced date (`z.coerce.date()`) reads a string in requests (query, path, body). Other
24
+ * unrepresentable parts throw (or become `{}` when lenient).
25
+ */
26
+ function zodOptions(context) {
27
+ return { unrepresentable: (site) => {
28
+ const def = site.zodSchema?._zod?.def;
29
+ if (def?.type === "date" && (context.io === "output" || def.coerce === true)) return { ...DATE_TIME };
30
+ return context.lenient ? "any" : "throw";
31
+ } };
32
+ }
33
+ /**
34
+ * `@valibot/to-json-schema` options: dates of responses are ISO strings (`Date.prototype.toJSON`),
35
+ * other unrepresentable parts throw (or are left out when lenient).
36
+ */
37
+ function valibotOptions(context) {
38
+ return {
39
+ ...context.lenient ? { errorMode: "ignore" } : {},
40
+ overrideSchema: (site) => context.io === "output" && site.valibotSchema?.type === "date" ? { ...DATE_TIME } : void 0
41
+ };
42
+ }
43
+ function callStandard(props, context, libraryOptions) {
44
+ const convert = props?.jsonSchema?.[context.io];
45
+ if (typeof convert !== "function") return void 0;
46
+ return convert.call(props?.jsonSchema, {
47
+ target: context.target,
48
+ ...libraryOptions === void 0 ? {} : { libraryOptions }
49
+ });
50
+ }
51
+ /**
52
+ * Standard JSON Schema (`schema['~standard'].jsonSchema[io]({ target: 'draft-2020-12' })`): Zod ≥ 4.2,
53
+ * ArkType ≥ 2.1.28 and every library implementing the interface.
54
+ */
55
+ const standardJsonSchemaConverter = Object.freeze({
56
+ name: "standard-json-schema",
57
+ convert(schema, context) {
58
+ const props = propsOf(schema);
59
+ if (typeof props?.jsonSchema?.[context.io] !== "function") return void 0;
60
+ return callStandard(props, context, props.vendor === "zod" ? zodOptions(context) : void 0);
61
+ }
62
+ });
63
+ /** Valibot schemas (no `~standard.jsonSchema`) through `toStandardJsonSchema()` of `@valibot/to-json-schema`. */
64
+ const valibotConverter = Object.freeze({
65
+ name: "valibot",
66
+ async convert(schema, context) {
67
+ const props = propsOf(schema);
68
+ if (props?.vendor !== "valibot" || props.jsonSchema !== void 0) return void 0;
69
+ let module;
70
+ try {
71
+ module = await import("@valibot/to-json-schema");
72
+ } catch (error) {
73
+ throw new Error("Valibot schemas need the optional peer @valibot/to-json-schema (npm install @valibot/to-json-schema)", { cause: error });
74
+ }
75
+ if (typeof module.toStandardJsonSchema !== "function") throw new Error("@valibot/to-json-schema has no toStandardJsonSchema(): install version 1.5 or later");
76
+ return callStandard(propsOf(module.toStandardJsonSchema(schema)), context, valibotOptions(context));
77
+ }
78
+ });
79
+ /** Built-in converters, tried after the user's. */
80
+ const BUILT_IN_CONVERTERS = Object.freeze([standardJsonSchemaConverter, valibotConverter]);
81
+ // src/build.ts
82
+ /** Reads and checks `metadata.extensions.openapi` (ZNIGHT030 when missing, ZNIGHT018 for another version). */
83
+ function openApiSection(metadata) {
84
+ const section = (metadata?.extensions)?.openapi;
85
+ if (section === void 0 || section === null || typeof section !== "object") throw new ZnightError(ErrorCode.InvalidOpenApiDefinition, "The metadata has no OpenAPI section (metadata.extensions.openapi)", { hints: ["Import @znight/openapi in a scanned file (src/main.ts imports OpenApiPlugin) and regenerate the container (znight generate)"] });
86
+ const version = section.version;
87
+ if (version !== 1) throw new ZnightError(ErrorCode.VersionMismatch, `Unsupported metadata.extensions.openapi version ${JSON.stringify(version)} (this @znight/openapi reads version 1)`, { hints: ["Use the same version of @znight/compiler and @znight/openapi, then regenerate the container"] });
88
+ const value = section;
89
+ if (!Array.isArray(value.operations) || !Array.isArray(value.schemas) || typeof value.document !== "object") throw new ZnightError(ErrorCode.InvalidOpenApiDefinition, "Malformed metadata.extensions.openapi", { hints: ["Regenerate the container (znight generate)"] });
90
+ return value;
91
+ }
92
+ /**
93
+ * Assembles the OpenAPI 3.1 document of an application from its generated metadata: converts the
94
+ * Standard Schemas (user converters, Standard JSON Schema, Valibot), hoists their `$defs`, names
95
+ * the components, fills the placeholders of the compiler, applies `filter`, prunes the unused
96
+ * components, applies `transform`, deep-freezes and serialises once.
97
+ *
98
+ * @throws ZnightError ZNIGHT030 (no OpenAPI section), ZNIGHT018 (another format version), ZNIGHT034
99
+ * (component name conflict), ZNIGHT037 (schema conversion, unless `unrepresentable: 'warn'`).
100
+ */
101
+ async function buildOpenApiDocument(metadata, options = {}) {
102
+ const started = performance.now();
103
+ const section = openApiSection(metadata);
104
+ const warnings = [];
105
+ const lenient = options.unrepresentable === "warn";
106
+ const converters = [...options.converters ?? [], ...BUILT_IN_CONVERTERS];
107
+ const failures = [];
108
+ const converted = /* @__PURE__ */ new Map();
109
+ for (const slot of section.schemas) {
110
+ const entry = {};
111
+ for (const io of [...slot.io].sort()) {
112
+ const result = await convertSlot(slot, io, converters, false);
113
+ if (result.schema !== void 0) {
114
+ entry[io] = result.schema;
115
+ continue;
116
+ }
117
+ if (!lenient) {
118
+ failures.push(result.message);
119
+ continue;
120
+ }
121
+ const retried = await convertSlot(slot, io, converters, true);
122
+ entry[io] = retried.schema ?? {};
123
+ warnings.push({
124
+ code: ErrorCode.SchemaConversionFailed,
125
+ message: `${result.message}; documented as ${retried.schema === void 0 ? "any value ({})" : "an approximation"}`,
126
+ hints: []
127
+ });
128
+ }
129
+ converted.set(slot.name, entry);
130
+ }
131
+ if (failures.length > 0) throw new ZnightError(ErrorCode.SchemaConversionFailed, `Cannot convert ${failures.length === 1 ? "a schema" : `${failures.length} schemas`} to JSON Schema:\n${failures.map((line) => ` - ${line}`).join("\n")}`, { hints: ["Add a converter (OpenApiPlugin.configure({ converters }), --converters), or document the schema with unrepresentable: \"warn\""] });
132
+ const schemas = /* @__PURE__ */ new Map();
133
+ const owners = /* @__PURE__ */ new Map();
134
+ const define = (name, schema, owner) => {
135
+ const existing = schemas.get(name);
136
+ if (existing === void 0) {
137
+ schemas.set(name, schema);
138
+ owners.set(name, owner);
139
+ return;
140
+ }
141
+ if (jsonEquals(existing, schema)) return;
142
+ throw new ZnightError(ErrorCode.SchemaNameConflict, `Two different schemas are named "${name}" in the OpenAPI document (${owners.get(name)} and ${owner})`, { hints: ["Rename one of them (exported schema names and ids of hoisted definitions name the components)"] });
143
+ };
144
+ for (const [name, schema] of Object.entries(section.components ?? {})) define(name, schema, "a TypeScript declaration");
145
+ const named = nameConverted(section.schemas, converted);
146
+ for (const item of named.components) define(item.name, item.schema, item.owner);
147
+ const slotNames = named.slots;
148
+ const errors = section.errors;
149
+ const responses = {};
150
+ if (errors !== void 0) {
151
+ let body;
152
+ let validation;
153
+ if (errors.schema === void 0) {
154
+ define(ERROR_COMPONENTS.body, HTTP_ERROR_BODY, "the standard errors");
155
+ define(ERROR_COMPONENTS.validation, VALIDATION_ERROR_BODY, "the standard errors");
156
+ body = ERROR_COMPONENTS.body;
157
+ validation = ERROR_COMPONENTS.validation;
158
+ } else {
159
+ body = slotNames.get(errors.schema)?.output ?? errors.schema;
160
+ validation = body;
161
+ }
162
+ const response = (status, name) => ({
163
+ description: reasonPhrase(status),
164
+ content: { [errors.contentType]: { schema: { $ref: schemaRef(name) } } }
165
+ });
166
+ responses[ERROR_RESPONSES.badRequest] = response("400", validation);
167
+ responses[ERROR_RESPONSES.payloadTooLarge] = response("413", body);
168
+ responses[ERROR_RESPONSES.unsupportedMediaType] = response("415", body);
169
+ }
170
+ const duplicates = [];
171
+ const resolve = (value, where = "") => replacePlaceholders(value, slotNames, schemas, (parameter, slot) => duplicates.push(`${where}: the ${parameter.in} parameter "${parameter.name}" of the schema ${slot}`));
172
+ for (const [name, schema] of schemas) schemas.set(name, resolve(schema));
173
+ const views = operationViews(metadata, section);
174
+ const paths = {};
175
+ let operations = 0;
176
+ section.operations.forEach((definition, index) => {
177
+ const view = views[index];
178
+ if (options.filter !== void 0 && !options.filter(view)) return;
179
+ let item = paths[definition.path];
180
+ if (item === void 0) {
181
+ item = {};
182
+ paths[definition.path] = item;
183
+ }
184
+ item[definition.method] = resolve(definition.operation, `${definition.method.toUpperCase()} ${definition.path}`);
185
+ operations++;
186
+ });
187
+ if (duplicates.length > 0) warnings.push({
188
+ code: ErrorCode.InconsistentOperation,
189
+ message: `${duplicates.length === 1 ? "A parameter expanded from a whole-object schema is" : "Parameters expanded from whole-object schemas are"} already documented (OpenAPI forbids two parameters with one name and location), the duplicate is dropped:\n${duplicates.map((line) => ` - ${line}`).join("\n")}`,
190
+ hints: ["Remove the property from the schema, or the @Query('name') / @ApiQuery('name') that reads it again"]
191
+ });
192
+ const template = section.document;
193
+ const document = { openapi: template.openapi };
194
+ document["info"] = options.info === void 0 ? template.info : {
195
+ ...template.info,
196
+ ...options.info
197
+ };
198
+ const servers = options.servers ?? template.servers ?? (options.prefix !== void 0 && options.prefix !== "" && options.prefix !== "/" ? [{ url: options.prefix }] : void 0);
199
+ if (servers !== void 0) document["servers"] = servers;
200
+ if (template.security !== void 0) document["security"] = template.security;
201
+ const tags = tagsOf(template.tags ?? [], paths, section.tagDescriptions ?? {});
202
+ if (tags.length > 0) document["tags"] = tags;
203
+ if (template.externalDocs !== void 0) document["externalDocs"] = template.externalDocs;
204
+ document["paths"] = paths;
205
+ const components = prune(document, {
206
+ schemas: Object.fromEntries([...schemas].sort(([a], [b]) => compareStrings(a, b))),
207
+ responses,
208
+ securitySchemes: section.securitySchemes ?? {}
209
+ });
210
+ if (Object.keys(components).length > 0) document["components"] = components;
211
+ for (const [key, value] of Object.entries(template)) if (key.startsWith("x-") && value !== void 0) document[key] = value;
212
+ document["x-znight-fingerprint"] = section.fingerprint;
213
+ const transformed = options.transform === void 0 ? document : options.transform(document);
214
+ const json = `${JSON.stringify(transformed, null, 2)}\n`;
215
+ const frozen = deepFreeze(JSON.parse(json));
216
+ const componentCount = Object.keys(frozen.components?.schemas ?? {}).length;
217
+ return {
218
+ document: frozen,
219
+ json,
220
+ warnings,
221
+ stats: {
222
+ operations,
223
+ schemas: componentCount,
224
+ durationMs: performance.now() - started
225
+ }
226
+ };
227
+ }
228
+ /** Root keys of a document, in the order {@link buildOpenApiDocument} writes them. */
229
+ const ROOT_ORDER = [
230
+ "openapi",
231
+ "info",
232
+ "servers",
233
+ "security",
234
+ "tags",
235
+ "externalDocs",
236
+ "paths",
237
+ "components"
238
+ ];
239
+ /**
240
+ * Applies the runtime options of {@link buildOpenApiDocument} to a prebuilt document (`OpenApiPlugin`
241
+ * with `document: { file }`), so that a file and a build serve the same document: `filter` (the
242
+ * operations are matched by path and method, unreferenced components and unused tags are pruned),
243
+ * `info`, `servers`, else the `prefix` rule when the file declares no servers, then `transform`.
244
+ * `undefined` when there is nothing to apply (the file is served as is).
245
+ */
246
+ function applyDocumentOptions(text, metadata, options) {
247
+ const document = JSON.parse(text);
248
+ const prefixed = document["servers"] === void 0 && options.prefix !== void 0 && options.prefix !== "" && options.prefix !== "/";
249
+ if (options.filter === void 0 && options.info === void 0 && options.servers === void 0 && options.transform === void 0 && !prefixed) return;
250
+ if (options.filter !== void 0) filterDocument(document, metadata, options.filter);
251
+ if (options.info !== void 0) document["info"] = {
252
+ ...document["info"],
253
+ ...options.info
254
+ };
255
+ if (options.servers !== void 0) document["servers"] = options.servers;
256
+ else if (prefixed) document["servers"] = [{ url: options.prefix }];
257
+ const ordered = {};
258
+ for (const key of ROOT_ORDER) if (document[key] !== void 0) ordered[key] = document[key];
259
+ for (const [key, value] of Object.entries(document)) if (!(key in ordered)) ordered[key] = value;
260
+ const transformed = options.transform === void 0 ? ordered : options.transform(ordered);
261
+ const json = `${JSON.stringify(transformed, null, 2)}\n`;
262
+ return {
263
+ document: deepFreeze(JSON.parse(json)),
264
+ json
265
+ };
266
+ }
267
+ /** Removes the operations `filter` rejects, the paths left empty, the unused tags and components. */
268
+ function filterDocument(document, metadata, filter) {
269
+ const section = openApiSection(metadata);
270
+ const views = /* @__PURE__ */ new Map();
271
+ for (const view of operationViews(metadata, section)) views.set(`${view.method} ${view.path}`, view);
272
+ const paths = {};
273
+ for (const [path, item] of Object.entries(document["paths"] ?? {})) {
274
+ const kept = {};
275
+ let methods = 0;
276
+ for (const [key, value] of Object.entries(item)) {
277
+ if (METHODS.has(key)) {
278
+ const view = views.get(`${key} ${path}`);
279
+ if (view !== void 0 && !filter(view)) continue;
280
+ methods++;
281
+ }
282
+ kept[key] = value;
283
+ }
284
+ if (methods > 0) paths[path] = kept;
285
+ }
286
+ document["paths"] = paths;
287
+ if (Array.isArray(document["tags"])) {
288
+ const declared = new Set((section.document.tags ?? []).map((tag) => tag.name));
289
+ const used = new Set(tagsOf([], paths, {}).map((tag) => tag.name));
290
+ const tags = document["tags"].filter((tag) => declared.has(tag.name) || used.has(tag.name));
291
+ if (tags.length > 0) document["tags"] = tags;
292
+ else delete document["tags"];
293
+ }
294
+ const components = document["components"] ?? {};
295
+ delete document["components"];
296
+ const pruned = prune(document, {
297
+ schemas: components["schemas"] ?? {},
298
+ responses: components["responses"] ?? {},
299
+ securitySchemes: components["securitySchemes"] ?? {}
300
+ });
301
+ const result = {};
302
+ for (const [kind, value] of Object.entries(components)) if (kind !== "schemas" && kind !== "responses" && kind !== "securitySchemes") result[kind] = value;
303
+ else if (pruned[kind] !== void 0) result[kind] = pruned[kind];
304
+ if (Object.keys(result).length > 0) document["components"] = result;
305
+ }
306
+ const METHODS = /* @__PURE__ */ new Set([
307
+ "get",
308
+ "put",
309
+ "post",
310
+ "delete",
311
+ "options",
312
+ "head",
313
+ "patch",
314
+ "trace"
315
+ ]);
316
+ async function convertSlot(slot, io, converters, lenient) {
317
+ const where = slot.source === void 0 ? "" : ` (${slot.source.file}:${slot.source.line}:${slot.source.column})`;
318
+ const label = `${slot.name}${where}, ${vendorOf(slot.schema)} schema, ${io}`;
319
+ for (const converter of converters) {
320
+ let result;
321
+ try {
322
+ result = await converter.convert(slot.schema, {
323
+ io,
324
+ target: "draft-2020-12",
325
+ lenient
326
+ });
327
+ } catch (error) {
328
+ return {
329
+ schema: void 0,
330
+ message: `${label}: the converter "${converter.name}" failed: ${error instanceof Error ? error.message : String(error)}`
331
+ };
332
+ }
333
+ if (result === void 0) continue;
334
+ if (typeof result !== "object" || result === null || Array.isArray(result)) return {
335
+ schema: void 0,
336
+ message: `${label}: the converter "${converter.name}" returned ${Array.isArray(result) ? "an array" : typeof result} instead of a JSON Schema object`
337
+ };
338
+ return {
339
+ schema: result,
340
+ message: ""
341
+ };
342
+ }
343
+ return {
344
+ schema: void 0,
345
+ message: `${label}: no converter supports it (the library implements no Standard JSON Schema)`
346
+ };
347
+ }
348
+ /** Drops `$schema` and `$id` (top level). */
349
+ function clean(schema) {
350
+ const { $schema: _schema, $id: _id, ...rest } = schema;
351
+ return JSON.parse(JSON.stringify(rest));
352
+ }
353
+ /**
354
+ * Definition names a library generates for anonymous sub-schemas (Zod: `__schema0` for a recursive
355
+ * schema without id). They are local to one conversion, so they are scoped to their slot.
356
+ */
357
+ const ANONYMOUS_DEFINITION = /^__schema(\d+)$/;
358
+ /** Marks the references between definitions until the final names are known (`\0<name>\0<rest>`). */
359
+ const MARK = "\0";
360
+ /**
361
+ * Names the converted schemas (steps 3 and 4 of §7.3): hoists `$defs` / `definitions` into
362
+ * components, scopes anonymous definitions to their slot (`Tree_schema0`), takes the definition as
363
+ * the slot component when the root only references it (`.meta({ id })` equal to the export name),
364
+ * and gives `<Name>Input` to the input variant of every schema — slot or definition — whose input
365
+ * and output JSON Schemas differ (directly or through the definitions it references). Two different
366
+ * schemas of one io under one name are ZNIGHT034.
367
+ */
368
+ function nameConverted(slots, converted) {
369
+ const units = [];
370
+ for (const slot of slots) {
371
+ const entry = converted.get(slot.name) ?? {};
372
+ for (const io of ["output", "input"]) {
373
+ const schema = entry[io];
374
+ if (schema !== void 0) units.push(splitUnit(slot.name, io, clean(schema)));
375
+ }
376
+ }
377
+ const variants = /* @__PURE__ */ new Map();
378
+ for (const unit of units) for (const { name, body, owner } of unit.bodies) {
379
+ let entry = variants.get(name);
380
+ if (entry === void 0) {
381
+ entry = { owners: {} };
382
+ variants.set(name, entry);
383
+ }
384
+ const existing = entry[unit.io];
385
+ if (existing === void 0) {
386
+ entry[unit.io] = body;
387
+ entry.owners[unit.io] = owner;
388
+ } else if (!jsonEquals(existing, body)) throw new ZnightError(ErrorCode.SchemaNameConflict, `Two different schemas are named "${name}" in the OpenAPI document (${entry.owners[unit.io]} and ${owner})`, { hints: ["Rename one of them (exported schema names and ids of hoisted definitions name the components)"] });
389
+ }
390
+ const split = /* @__PURE__ */ new Set();
391
+ for (const [name, entry] of variants) if (entry.input !== void 0 && entry.output !== void 0 && !jsonEquals(entry.input, entry.output)) split.add(name);
392
+ for (let changed = true; changed;) {
393
+ changed = false;
394
+ for (const [name, entry] of variants) {
395
+ if (split.has(name) || entry.input === void 0 || entry.output === void 0) continue;
396
+ if ([...referencedNames(entry.input)].some((target) => split.has(target))) {
397
+ split.add(name);
398
+ changed = true;
399
+ }
400
+ }
401
+ }
402
+ const nameFor = (name, io) => io === "input" && split.has(name) ? `${name}Input` : name;
403
+ const components = [];
404
+ for (const [name, entry] of variants) {
405
+ if (entry.output !== void 0) components.push({
406
+ name,
407
+ schema: resolveMarks(entry.output, "output", nameFor),
408
+ owner: entry.owners["output"]
409
+ });
410
+ if (entry.input !== void 0 && (entry.output === void 0 || split.has(name))) components.push({
411
+ name: nameFor(name, "input"),
412
+ schema: resolveMarks(entry.input, "input", nameFor),
413
+ owner: entry.owners["input"]
414
+ });
415
+ }
416
+ const names = /* @__PURE__ */ new Map();
417
+ for (const slot of slots) {
418
+ const entry = converted.get(slot.name) ?? {};
419
+ const result = {};
420
+ if (entry.output !== void 0) result.output = nameFor(slot.name, "output");
421
+ if (entry.input !== void 0) result.input = nameFor(slot.name, "input");
422
+ names.set(slot.name, result);
423
+ }
424
+ return {
425
+ components,
426
+ slots: names
427
+ };
428
+ }
429
+ /**
430
+ * Splits a converted schema into its root and its `$defs` / `definitions` (at every level), with
431
+ * global names: the slot name for the root, `<slot>_schemaN` for anonymous definitions, the
432
+ * definition name (a library id) otherwise. Inside an OpenAPI document `#` is the document, not the
433
+ * schema, so every local reference is rewritten (`#/$defs/X`, `#`, `#/properties/...`).
434
+ */
435
+ function splitUnit(slot, io, schema) {
436
+ const defs = /* @__PURE__ */ new Map();
437
+ const collect = (value) => {
438
+ if (typeof value !== "object" || value === null) return;
439
+ if (Array.isArray(value)) {
440
+ for (const item of value) collect(item);
441
+ return;
442
+ }
443
+ for (const key of ["$defs", "definitions"]) {
444
+ const block = value[key];
445
+ if (typeof block === "object" && block !== null && !Array.isArray(block)) for (const [defName, def] of Object.entries(block)) defs.set(defName, def);
446
+ }
447
+ for (const [key, item] of Object.entries(value)) if (key !== "$defs" && key !== "definitions") collect(item);
448
+ };
449
+ collect(schema);
450
+ const globals = /* @__PURE__ */ new Map();
451
+ for (const local of defs.keys()) {
452
+ const anonymous = ANONYMOUS_DEFINITION.exec(local);
453
+ globals.set(local, anonymous === null ? componentName(local) : componentName(`${slot}_schema${anonymous[1]}`));
454
+ }
455
+ let root = schema;
456
+ const target = Object.keys(schema).filter((key) => key !== "$defs" && key !== "definitions").length === 1 && typeof schema["$ref"] === "string" ? localTarget(schema["$ref"]) : void 0;
457
+ if (target !== void 0 && target.rest === "" && defs.has(target.local)) {
458
+ if (globals.get(target.local) === slot || ANONYMOUS_DEFINITION.test(target.local)) {
459
+ root = defs.get(target.local);
460
+ defs.delete(target.local);
461
+ globals.set(target.local, slot);
462
+ }
463
+ }
464
+ const rewrite = (value) => {
465
+ if (Array.isArray(value)) return value.map(rewrite);
466
+ if (typeof value !== "object" || value === null) return value;
467
+ const result = {};
468
+ for (const [key, item] of Object.entries(value)) {
469
+ if (key === "$defs" || key === "definitions") continue;
470
+ if (key === "$ref" && typeof item === "string") result[key] = markRef(item, slot, globals);
471
+ else result[key] = rewrite(item);
472
+ }
473
+ return result;
474
+ };
475
+ const bodies = [{
476
+ name: slot,
477
+ body: rewrite(root),
478
+ owner: `the schema ${slot}`
479
+ }];
480
+ for (const [local, def] of defs) bodies.push({
481
+ name: globals.get(local),
482
+ body: rewrite(def),
483
+ owner: `a definition of ${slot}`
484
+ });
485
+ return {
486
+ slot,
487
+ io,
488
+ bodies
489
+ };
490
+ }
491
+ /** The local definition a reference points to (`#/$defs/X/rest`), JSON pointer and URI escapes decoded. */
492
+ function localTarget(ref) {
493
+ for (const prefix of ["#/$defs/", "#/definitions/"]) {
494
+ if (!ref.startsWith(prefix)) continue;
495
+ const tail = ref.slice(prefix.length);
496
+ const slash = tail.indexOf("/");
497
+ const segment = slash === -1 ? tail : tail.slice(0, slash);
498
+ let local;
499
+ try {
500
+ local = decodeURIComponent(segment);
501
+ } catch {
502
+ local = segment;
503
+ }
504
+ return {
505
+ local: local.replaceAll("~1", "/").replaceAll("~0", "~"),
506
+ rest: slash === -1 ? "" : tail.slice(slash)
507
+ };
508
+ }
509
+ }
510
+ function markRef(ref, slot, globals) {
511
+ const target = localTarget(ref);
512
+ if (target !== void 0) return `${MARK}${globals.get(target.local) ?? componentName(target.local)}${MARK}${target.rest}`;
513
+ if (ref === "#") return `${MARK}${slot}${MARK}`;
514
+ if (ref.startsWith("#/")) return `${MARK}${slot}${MARK}${ref.slice(1)}`;
515
+ return ref;
516
+ }
517
+ function parseMark(ref) {
518
+ if (!ref.startsWith(MARK)) return void 0;
519
+ const end = ref.indexOf(MARK, 1);
520
+ return end === -1 ? void 0 : {
521
+ name: ref.slice(1, end),
522
+ rest: ref.slice(end + 1)
523
+ };
524
+ }
525
+ /** Global names referenced by a marked body. */
526
+ function referencedNames(value, found = /* @__PURE__ */ new Set()) {
527
+ if (Array.isArray(value)) for (const item of value) referencedNames(item, found);
528
+ else if (typeof value === "object" && value !== null) for (const [key, item] of Object.entries(value)) {
529
+ const mark = key === "$ref" && typeof item === "string" ? parseMark(item) : void 0;
530
+ if (mark === void 0) referencedNames(item, found);
531
+ else found.add(mark.name);
532
+ }
533
+ return found;
534
+ }
535
+ /** Replaces the marked references with `#/components/schemas/<final name>`. */
536
+ function resolveMarks(value, io, nameFor) {
537
+ if (Array.isArray(value)) return value.map((item) => resolveMarks(item, io, nameFor));
538
+ if (typeof value !== "object" || value === null) return value;
539
+ const result = {};
540
+ for (const [key, item] of Object.entries(value)) {
541
+ const mark = key === "$ref" && typeof item === "string" ? parseMark(item) : void 0;
542
+ result[key] = mark === void 0 ? resolveMarks(item, io, nameFor) : `${schemaRef(nameFor(mark.name, io))}${mark.rest}`;
543
+ }
544
+ return result;
545
+ }
546
+ function isPlaceholder(value) {
547
+ return typeof value === "object" && value !== null && typeof value.$znight === "string";
548
+ }
549
+ function replacePlaceholders(value, names, schemas, duplicate) {
550
+ if (Array.isArray(value)) {
551
+ if (!value.some((item) => isPlaceholder(item) && item.$znight === "parameters")) return value.map((item) => replacePlaceholders(item, names, schemas, duplicate));
552
+ const explicit = /* @__PURE__ */ new Set();
553
+ for (const item of value) {
554
+ const key = isPlaceholder(item) ? void 0 : parameterKey(item);
555
+ if (key !== void 0) explicit.add(key);
556
+ }
557
+ const expanded = /* @__PURE__ */ new Set();
558
+ const result = [];
559
+ for (const item of value) {
560
+ if (!isPlaceholder(item) || item.$znight !== "parameters") {
561
+ result.push(replacePlaceholders(item, names, schemas, duplicate));
562
+ continue;
563
+ }
564
+ for (const parameter of expandParameters(item, names, schemas)) {
565
+ const key = parameterKey(parameter);
566
+ if (explicit.has(key) || expanded.has(key)) {
567
+ duplicate(parameter, item.name);
568
+ continue;
569
+ }
570
+ expanded.add(key);
571
+ result.push(parameter);
572
+ }
573
+ }
574
+ return result;
575
+ }
576
+ if (typeof value !== "object" || value === null) return value;
577
+ if (isPlaceholder(value) && value.$znight === "schema") {
578
+ const name = names.get(value.name)?.[value.io] ?? value.name;
579
+ return { $ref: schemaRef(name) };
580
+ }
581
+ const result = {};
582
+ for (const [key, item] of Object.entries(value)) result[key] = replacePlaceholders(item, names, schemas, duplicate);
583
+ return result;
584
+ }
585
+ /** `in name` of a Parameter Object (header names are case-insensitive), `undefined` for a `$ref`. */
586
+ function parameterKey(parameter) {
587
+ if (typeof parameter !== "object" || parameter === null) return void 0;
588
+ const { in: location, name } = parameter;
589
+ if (typeof location !== "string" || typeof name !== "string") return void 0;
590
+ return `${location} ${location === "header" ? name.toLowerCase() : name}`;
591
+ }
592
+ /** One parameter per property of the (input) object schema of a slot. */
593
+ function expandParameters(placeholder, names, schemas) {
594
+ const name = names.get(placeholder.name)?.input ?? placeholder.name;
595
+ let schema = schemas.get(name);
596
+ for (let depth = 0; depth < 8 && typeof schema?.["$ref"] === "string"; depth++) schema = schemas.get(String(schema["$ref"]).slice(21));
597
+ const properties = schema?.["properties"];
598
+ if (schema === void 0 || typeof properties !== "object" || properties === null || Array.isArray(properties)) throw new ZnightError(ErrorCode.SchemaConversionFailed, `The ${placeholder.in ?? "query"} schema ${placeholder.name} is not an object schema: its properties cannot become parameters`, { hints: [`Validate the whole ${placeholder.in ?? "query"} object with an object schema (z.object({...}))`] });
599
+ const required = new Set(Array.isArray(schema["required"]) ? schema["required"] : []);
600
+ return Object.entries(properties).map(([property, propertySchema]) => {
601
+ const { description, ...rest } = propertySchema;
602
+ const parameter = {
603
+ name: property,
604
+ in: placeholder.in ?? "query"
605
+ };
606
+ if (typeof description === "string") parameter["description"] = description;
607
+ if (placeholder.in === "path" || required.has(property)) parameter["required"] = true;
608
+ parameter["schema"] = rest;
609
+ if ((rest["type"] === "array" || Array.isArray(rest["items"])) && placeholder.in === "query") {
610
+ parameter["style"] = "form";
611
+ parameter["explode"] = true;
612
+ }
613
+ return parameter;
614
+ });
615
+ }
616
+ /** Declared tags first (in order), then the used tags, sorted. */
617
+ function tagsOf(declared, paths, descriptions) {
618
+ const tags = [...declared];
619
+ const known = new Set(declared.map((tag) => tag.name));
620
+ const used = /* @__PURE__ */ new Set();
621
+ for (const item of Object.values(paths)) for (const operation of Object.values(item)) for (const tag of operation.tags ?? []) used.add(tag);
622
+ for (const tag of [...used].sort(compareStrings)) {
623
+ if (known.has(tag)) continue;
624
+ const description = descriptions[tag];
625
+ tags.push(description === void 0 ? { name: tag } : {
626
+ name: tag,
627
+ description
628
+ });
629
+ }
630
+ return tags;
631
+ }
632
+ /** Components referenced by the document (paths, security), transitively. */
633
+ function prune(document, components) {
634
+ const used = {
635
+ schemas: /* @__PURE__ */ new Set(),
636
+ responses: /* @__PURE__ */ new Set(),
637
+ securitySchemes: /* @__PURE__ */ new Set()
638
+ };
639
+ const visit = (value) => {
640
+ if (Array.isArray(value)) {
641
+ for (const item of value) visit(item);
642
+ return;
643
+ }
644
+ if (typeof value !== "object" || value === null) return;
645
+ for (const [key, item] of Object.entries(value)) if (key === "$ref" && typeof item === "string") {
646
+ const match = /^#\/components\/(schemas|responses)\/([^/]+)/.exec(item);
647
+ const kind = match?.[1];
648
+ const name = match?.[2] === void 0 ? void 0 : decodeURIComponent(match[2].replaceAll("~1", "/").replaceAll("~0", "~"));
649
+ if (kind !== void 0 && name !== void 0 && !used[kind].has(name)) {
650
+ used[kind].add(name);
651
+ visit(components[kind][name]);
652
+ }
653
+ } else if (key === "security" && Array.isArray(item)) for (const requirement of item) for (const scheme of Object.keys(requirement)) used.securitySchemes.add(scheme);
654
+ else visit(item);
655
+ };
656
+ visit(document);
657
+ const result = {};
658
+ for (const kind of [
659
+ "schemas",
660
+ "responses",
661
+ "securitySchemes"
662
+ ]) {
663
+ const kept = {};
664
+ for (const name of Object.keys(components[kind]).sort(compareStrings)) if (used[kind].has(name)) kept[name] = components[kind][name];
665
+ if (Object.keys(kept).length > 0) result[kind] = kept;
666
+ }
667
+ return result;
668
+ }
669
+ /** Views of the operations of the section (effective route metadata from `extensions.http`). */
670
+ function operationViews(metadata, section = openApiSection(metadata)) {
671
+ const http = metadata.extensions?.http;
672
+ const groups = /* @__PURE__ */ new Map();
673
+ for (const [group, definition] of Object.entries(http?.groups ?? {})) for (const key of definition.keys ?? []) groups.set(key, group);
674
+ const groupOf = (key) => groups.get(key) ?? key;
675
+ return section.operations.map((definition) => {
676
+ const controller = http?.controllers?.find((candidate) => candidate.token === definition.controller);
677
+ const routeMeta = (controller?.routes?.find((candidate) => candidate.handler === definition.handler && candidate.method === definition.route))?.meta ?? {};
678
+ const decided = new Set(Object.keys(routeMeta).map(groupOf));
679
+ const meta = {};
680
+ for (const [key, value] of Object.entries(controller?.meta ?? {})) if (!decided.has(groupOf(key))) meta[key] = value;
681
+ Object.assign(meta, routeMeta);
682
+ const operation = definition.operation;
683
+ return Object.freeze({
684
+ method: definition.method,
685
+ path: definition.path,
686
+ operationId: operation.operationId ?? "",
687
+ tags: Object.freeze([...operation.tags ?? []]),
688
+ controller: displayName(definition.controller),
689
+ handler: definition.handler,
690
+ meta: Object.freeze(meta)
691
+ });
692
+ });
693
+ }
694
+ function deepFreeze(value) {
695
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
696
+ Object.freeze(value);
697
+ for (const item of Object.values(value)) deepFreeze(item);
698
+ }
699
+ return value;
700
+ }
701
+ /** `HttpErrorBody` (the body of every Znight error response). */
702
+ const HTTP_ERROR_BODY = {
703
+ type: "object",
704
+ properties: {
705
+ statusCode: { type: "integer" },
706
+ error: { type: "string" },
707
+ message: { type: "string" },
708
+ details: {}
709
+ },
710
+ required: [
711
+ "statusCode",
712
+ "error",
713
+ "message"
714
+ ]
715
+ };
716
+ /** `ValidationErrorBody`: a 400 body; validation failures list their issues in `details`. */
717
+ const VALIDATION_ERROR_BODY = { allOf: [{ $ref: schemaRef(ERROR_COMPONENTS.body) }, {
718
+ type: "object",
719
+ properties: { details: {
720
+ type: "array",
721
+ items: {
722
+ type: "object",
723
+ properties: {
724
+ path: { type: "string" },
725
+ message: { type: "string" }
726
+ },
727
+ required: ["path", "message"]
728
+ }
729
+ } }
730
+ }] };
731
+ export { BUILT_IN_CONVERTERS as a, OPENAPI_METADATA_VERSION as c, operationViews as i, buildOpenApiDocument as n, standardJsonSchemaConverter as o, openApiSection as r, valibotConverter as s, applyDocumentOptions as t };
732
+
733
+ //# sourceMappingURL=build-DXunOzlI.mjs.map