schema-components 1.12.10 → 1.13.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/README.md +31 -0
- package/dist/core/adapter.d.mts +1 -1
- package/dist/core/adapter.mjs +37 -8
- package/dist/core/constraints.d.mts +16 -0
- package/dist/core/constraints.mjs +138 -0
- package/dist/core/merge.d.mts +32 -0
- package/dist/core/merge.mjs +96 -0
- package/dist/core/normalise.d.mts +40 -0
- package/dist/core/normalise.mjs +171 -0
- package/dist/core/openapi30.d.mts +38 -0
- package/dist/core/openapi30.mjs +223 -0
- package/dist/core/ref.d.mts +25 -0
- package/dist/core/ref.mjs +86 -0
- package/dist/core/renderer.d.mts +2 -2
- package/dist/core/renderer.mjs +8 -0
- package/dist/core/swagger2.d.mts +10 -0
- package/dist/core/swagger2.mjs +294 -0
- package/dist/core/typeInference.d.mts +2 -0
- package/dist/core/typeInference.mjs +1 -0
- package/dist/core/types.d.mts +2 -3
- package/dist/core/types.mjs +55 -2
- package/dist/core/version.d.mts +2 -0
- package/dist/core/version.mjs +79 -0
- package/dist/core/walkBuilders.d.mts +52 -0
- package/dist/core/walkBuilders.mjs +152 -0
- package/dist/core/walker.d.mts +3 -10
- package/dist/core/walker.mjs +143 -231
- package/dist/html/a11y.d.mts +5 -4
- package/dist/html/renderToHtml.d.mts +3 -3
- package/dist/html/renderToHtml.mjs +23 -379
- package/dist/html/renderToHtmlStream.d.mts +29 -47
- package/dist/html/renderToHtmlStream.mjs +33 -305
- package/dist/html/renderers.d.mts +14 -0
- package/dist/html/renderers.mjs +406 -0
- package/dist/html/streamRenderers.d.mts +13 -0
- package/dist/html/streamRenderers.mjs +243 -0
- package/dist/openapi/components.d.mts +2 -1
- package/dist/openapi/parser.d.mts +59 -2
- package/dist/openapi/parser.mjs +189 -8
- package/dist/react/SchemaComponent.d.mts +4 -2
- package/dist/react/SchemaComponent.mjs +39 -85
- package/dist/react/SchemaView.d.mts +2 -1
- package/dist/react/SchemaView.mjs +21 -9
- package/dist/react/fieldPath.d.mts +20 -0
- package/dist/react/fieldPath.mjs +81 -0
- package/dist/react/headless.d.mts +2 -4
- package/dist/react/headless.mjs +3 -492
- package/dist/react/headlessRenderers.d.mts +23 -0
- package/dist/react/headlessRenderers.mjs +507 -0
- package/dist/renderer-DseHeliw.d.mts +160 -0
- package/dist/themes/mantine.d.mts +1 -1
- package/dist/themes/mantine.mjs +2 -1
- package/dist/themes/mui.d.mts +1 -1
- package/dist/themes/mui.mjs +2 -1
- package/dist/themes/radix.d.mts +1 -1
- package/dist/themes/radix.mjs +2 -1
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/themes/shadcn.mjs +10 -6
- package/dist/typeInference-CRPqVwKu.d.mts +299 -0
- package/dist/types-ag2jYLqQ.d.mts +261 -0
- package/dist/version-CLchheaH.d.mts +40 -0
- package/package.json +1 -1
- package/dist/types-BJzEgJdX.d.mts +0 -335
package/README.md
CHANGED
|
@@ -179,6 +179,37 @@ import { SchemaField } from "schema-components/react/SchemaComponent";
|
|
|
179
179
|
|
|
180
180
|
When the schema is a Zod schema or typed `as const`, only valid dot-paths like `"address.city"` are accepted. Invalid paths trigger TypeScript errors. Runtime schemas accept any string.
|
|
181
181
|
|
|
182
|
+
## Spec support
|
|
183
|
+
|
|
184
|
+
The walker reads canonical Draft 2020-12 JSON Schema. Older drafts and OpenAPI documents are normalised to that form transparently.
|
|
185
|
+
|
|
186
|
+
| Spec | Detection | Normalisation | Notes |
|
|
187
|
+
|---|---|---|---|
|
|
188
|
+
| JSON Schema Draft 04 | `http://json-schema.org/draft-04/schema#` | `exclusiveMinimum`/`Maximum` boolean → number; `id` left in place | |
|
|
189
|
+
| JSON Schema Draft 06 | `http://json-schema.org/draft-06/schema#` | Pass-through (canonical for `const`, `examples[]`, `$id`, `propertyNames`, `contains`) | |
|
|
190
|
+
| JSON Schema Draft 07 | `http://json-schema.org/draft-07/schema#` | Pass-through (adds `if`/`then`/`else`, `contentEncoding`, `contentMediaType`) | |
|
|
191
|
+
| JSON Schema Draft 2019-09 | `https://json-schema.org/draft/2019-09/schema` | `$recursiveRef` → `$ref`, `$recursiveAnchor` → `$anchor` | Adds `unevaluatedProperties`/`Items`, `dependentSchemas`/`Required` |
|
|
192
|
+
| JSON Schema Draft 2020-12 | `https://json-schema.org/draft/2020-12/schema` (default) | `$dynamicRef` → `$ref`, `$dynamicAnchor` → `$anchor`; tuple form via `prefixItems` | |
|
|
193
|
+
| OpenAPI 2.0 (Swagger) | `swagger: "2.0"` | Full document restructure → OpenAPI 3.1; `definitions` → `components/schemas`; body params → `requestBody`; `formData` → `multipart/form-data`; `collectionFormat` → `style`/`explode` | |
|
|
194
|
+
| OpenAPI 3.0.x | `openapi: "3.0.x"` | `nullable` → `anyOf [T, null]`; `discriminator` mapping → injected `const`; `example` → `examples[]` | Callbacks, links, security schemes preserved |
|
|
195
|
+
| OpenAPI 3.1.x | `openapi: "3.1.x"` | Direct (already Draft 2020-12) | Webhooks, `components/pathItems`, JSON Schema `type` arrays, `examples[]` |
|
|
196
|
+
|
|
197
|
+
### Documented type-level fallbacks
|
|
198
|
+
|
|
199
|
+
A few JSON Schema keywords can't be expressed in TypeScript's type system. They are handled at runtime by the walker, but `FromJSONSchema<S>` falls back as follows (each pinned by `tests/type-inference-advanced.test.ts`):
|
|
200
|
+
|
|
201
|
+
| Keyword | Type-level result | Why |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `not` | `unknown` | TypeScript has no type negation |
|
|
204
|
+
| `if` / `then` / `else` | Base schema (conditions ignored) | Requires value-dependent conditional evaluation |
|
|
205
|
+
| `propertyNames` | Ignored | Cannot constrain object key *shape* |
|
|
206
|
+
| `dependentSchemas` / `dependentRequired` | Ignored | Cross-field runtime conditionals |
|
|
207
|
+
| `unevaluatedProperties` / `unevaluatedItems` | Ignored | Requires whole-tree evaluation context |
|
|
208
|
+
| `contains` / `minContains` / `maxContains` | Element type unchanged | Constraint metadata only |
|
|
209
|
+
| `$recursiveRef` | `unknown` | Recursive types not expressible |
|
|
210
|
+
|
|
211
|
+
Runtime rendering and validation handle each of these correctly; only the static type widens.
|
|
212
|
+
|
|
182
213
|
## OpenAPI components
|
|
183
214
|
|
|
184
215
|
Render API operations with type-safe field overrides:
|
package/dist/core/adapter.d.mts
CHANGED
package/dist/core/adapter.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { getProperty, hasProperty, isObject } from "./guards.mjs";
|
|
2
|
+
import { dereference } from "./ref.mjs";
|
|
3
|
+
import { detectJsonSchemaDraft, detectOpenApiVersion, isSwagger2 } from "./version.mjs";
|
|
4
|
+
import { normaliseJsonSchema as normaliseJsonSchema$1, normaliseOpenApiSchemas } from "./normalise.mjs";
|
|
2
5
|
import { z } from "zod";
|
|
3
6
|
//#region src/core/adapter.ts
|
|
4
7
|
/**
|
|
@@ -16,7 +19,7 @@ const schemaCache = /* @__PURE__ */ new WeakMap();
|
|
|
16
19
|
function detectSchemaKind(input) {
|
|
17
20
|
if (hasProperty(input, "_zod")) return "zod4";
|
|
18
21
|
if (hasProperty(input, "_def") && !hasProperty(input, "_zod")) return "zod3";
|
|
19
|
-
if (hasProperty(input, "openapi")) return "openapi";
|
|
22
|
+
if (hasProperty(input, "openapi") || hasProperty(input, "swagger")) return "openapi";
|
|
20
23
|
return "jsonSchema";
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
@@ -70,21 +73,41 @@ function normaliseZod4(input) {
|
|
|
70
73
|
};
|
|
71
74
|
}
|
|
72
75
|
function normaliseJsonSchema(jsonSchema) {
|
|
76
|
+
const normalised = normaliseJsonSchema$1(jsonSchema, detectJsonSchemaDraft(jsonSchema));
|
|
73
77
|
return {
|
|
74
|
-
jsonSchema,
|
|
75
|
-
rootMeta: extractRootMetaFromJson(
|
|
76
|
-
rootDocument:
|
|
78
|
+
jsonSchema: normalised,
|
|
79
|
+
rootMeta: extractRootMetaFromJson(normalised),
|
|
80
|
+
rootDocument: normalised
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
function normaliseZod3() {
|
|
80
84
|
throw new Error("Zod 3 schemas are not yet supported. Convert to Zod 4 or provide JSON Schema directly.");
|
|
81
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Mapping of Swagger 2.0 $ref prefixes to their OpenAPI 3.x equivalents.
|
|
88
|
+
* Used by the adapter to rewrite user-provided ref strings so they
|
|
89
|
+
* resolve correctly against the normalised document.
|
|
90
|
+
*/
|
|
91
|
+
const REF_REWRITES_ADAPTER = [
|
|
92
|
+
["#/definitions/", "#/components/schemas/"],
|
|
93
|
+
["#/parameters/", "#/components/parameters/"],
|
|
94
|
+
["#/responses/", "#/components/responses/"]
|
|
95
|
+
];
|
|
82
96
|
function normaliseOpenApi(doc, ref) {
|
|
83
|
-
const
|
|
97
|
+
const version = detectOpenApiVersion(doc);
|
|
98
|
+
const normalisedDoc = version !== void 0 ? normaliseOpenApiSchemas(doc, version) : doc;
|
|
99
|
+
let rewrittenRef = ref;
|
|
100
|
+
if (rewrittenRef !== void 0 && version !== void 0 && isSwagger2(version)) {
|
|
101
|
+
for (const [from, to] of REF_REWRITES_ADAPTER) if (rewrittenRef.startsWith(from)) {
|
|
102
|
+
rewrittenRef = to + rewrittenRef.slice(from.length);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const resolved = resolveOpenApiRef(normalisedDoc, rewrittenRef);
|
|
84
107
|
return {
|
|
85
108
|
jsonSchema: resolved,
|
|
86
109
|
rootMeta: extractRootMetaFromJson(resolved),
|
|
87
|
-
rootDocument:
|
|
110
|
+
rootDocument: normalisedDoc
|
|
88
111
|
};
|
|
89
112
|
}
|
|
90
113
|
function resolveOpenApiRef(doc, ref) {
|
|
@@ -120,11 +143,17 @@ function resolveOpenApiRef(doc, ref) {
|
|
|
120
143
|
const content = getProperty(requestBody, "content");
|
|
121
144
|
if (!isObject(content)) throw new Error(`No content for ${ref}`);
|
|
122
145
|
const json = getProperty(content, "application/json");
|
|
123
|
-
|
|
124
|
-
const
|
|
146
|
+
const multipart = getProperty(content, "multipart/form-data");
|
|
147
|
+
const mediaType = isObject(json) ? json : isObject(multipart) ? multipart : void 0;
|
|
148
|
+
if (mediaType === void 0) throw new Error(`No content for ${ref}`);
|
|
149
|
+
const schema = getProperty(mediaType, "schema");
|
|
125
150
|
if (!isObject(schema)) throw new Error(`Could not resolve request body schema for ${ref}`);
|
|
126
151
|
return schema;
|
|
127
152
|
}
|
|
153
|
+
if (ref.startsWith("#/")) {
|
|
154
|
+
const resolved = dereference(ref, doc);
|
|
155
|
+
if (resolved !== void 0) return resolved;
|
|
156
|
+
}
|
|
128
157
|
throw new Error(`Unsupported OpenAPI ref format: ${ref}`);
|
|
129
158
|
}
|
|
130
159
|
function extractRootMetaFromJson(jsonSchema) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { E as StringConstraints, b as ObjectConstraints, f as FileConstraints, t as ArrayConstraints, v as NumberConstraints } from "../types-ag2jYLqQ.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/core/constraints.d.ts
|
|
4
|
+
declare function extractStringConstraints(schema: Record<string, unknown>): StringConstraints;
|
|
5
|
+
declare function extractNumberConstraints(schema: Record<string, unknown>): NumberConstraints;
|
|
6
|
+
declare function extractArrayConstraints(schema: Record<string, unknown>): ArrayConstraints;
|
|
7
|
+
declare function extractObjectConstraints(schema: Record<string, unknown>): ObjectConstraints;
|
|
8
|
+
declare function extractFileConstraints(schema: Record<string, unknown>): FileConstraints;
|
|
9
|
+
/**
|
|
10
|
+
* Return a copy of the schema with constraint keywords that don't apply
|
|
11
|
+
* to the given type removed. Meta keywords (description, title, etc.)
|
|
12
|
+
* and composition keywords are always preserved.
|
|
13
|
+
*/
|
|
14
|
+
declare function stripInapplicableConstraints(schema: Record<string, unknown>, targetType: string): Record<string, unknown>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { extractArrayConstraints, extractFileConstraints, extractNumberConstraints, extractObjectConstraints, extractStringConstraints, stripInapplicableConstraints };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { isObject } from "./guards.mjs";
|
|
2
|
+
//#region src/core/constraints.ts
|
|
3
|
+
function getString(obj, key) {
|
|
4
|
+
const value = obj[key];
|
|
5
|
+
return typeof value === "string" ? value : void 0;
|
|
6
|
+
}
|
|
7
|
+
function getNumber(obj, key) {
|
|
8
|
+
const value = obj[key];
|
|
9
|
+
return typeof value === "number" ? value : void 0;
|
|
10
|
+
}
|
|
11
|
+
function getObject(obj, key) {
|
|
12
|
+
const value = obj[key];
|
|
13
|
+
return isObject(value) ? value : void 0;
|
|
14
|
+
}
|
|
15
|
+
function extractStringConstraints(schema) {
|
|
16
|
+
const c = {};
|
|
17
|
+
const minLength = getNumber(schema, "minLength");
|
|
18
|
+
if (minLength !== void 0) c.minLength = minLength;
|
|
19
|
+
const maxLength = getNumber(schema, "maxLength");
|
|
20
|
+
if (maxLength !== void 0) c.maxLength = maxLength;
|
|
21
|
+
const pattern = getString(schema, "pattern");
|
|
22
|
+
if (pattern !== void 0) c.pattern = pattern;
|
|
23
|
+
const format = getString(schema, "format");
|
|
24
|
+
if (format !== void 0) c.format = format;
|
|
25
|
+
const contentEncoding = getString(schema, "contentEncoding");
|
|
26
|
+
if (contentEncoding !== void 0) c.contentEncoding = contentEncoding;
|
|
27
|
+
const contentMediaType = getString(schema, "contentMediaType");
|
|
28
|
+
if (contentMediaType !== void 0) c.contentMediaType = contentMediaType;
|
|
29
|
+
return c;
|
|
30
|
+
}
|
|
31
|
+
function extractNumberConstraints(schema) {
|
|
32
|
+
const c = {};
|
|
33
|
+
const minimum = getNumber(schema, "minimum");
|
|
34
|
+
if (minimum !== void 0) c.minimum = minimum;
|
|
35
|
+
const maximum = getNumber(schema, "maximum");
|
|
36
|
+
if (maximum !== void 0) c.maximum = maximum;
|
|
37
|
+
const exclusiveMinimum = getNumber(schema, "exclusiveMinimum");
|
|
38
|
+
if (exclusiveMinimum !== void 0) c.exclusiveMinimum = exclusiveMinimum;
|
|
39
|
+
const exclusiveMaximum = getNumber(schema, "exclusiveMaximum");
|
|
40
|
+
if (exclusiveMaximum !== void 0) c.exclusiveMaximum = exclusiveMaximum;
|
|
41
|
+
const multipleOf = getNumber(schema, "multipleOf");
|
|
42
|
+
if (multipleOf !== void 0) c.multipleOf = multipleOf;
|
|
43
|
+
return c;
|
|
44
|
+
}
|
|
45
|
+
function extractArrayConstraints(schema) {
|
|
46
|
+
const c = {};
|
|
47
|
+
const minItems = getNumber(schema, "minItems");
|
|
48
|
+
if (minItems !== void 0) c.minItems = minItems;
|
|
49
|
+
const maxItems = getNumber(schema, "maxItems");
|
|
50
|
+
if (maxItems !== void 0) c.maxItems = maxItems;
|
|
51
|
+
if (schema.uniqueItems === true) c.uniqueItems = true;
|
|
52
|
+
const contains = getObject(schema, "contains");
|
|
53
|
+
if (contains !== void 0) c.contains = contains;
|
|
54
|
+
const minContains = getNumber(schema, "minContains");
|
|
55
|
+
if (minContains !== void 0) c.minContains = minContains;
|
|
56
|
+
const maxContains = getNumber(schema, "maxContains");
|
|
57
|
+
if (maxContains !== void 0) c.maxContains = maxContains;
|
|
58
|
+
const unevaluatedItems = getObject(schema, "unevaluatedItems");
|
|
59
|
+
if (unevaluatedItems !== void 0) c.unevaluatedItems = unevaluatedItems;
|
|
60
|
+
return c;
|
|
61
|
+
}
|
|
62
|
+
function extractObjectConstraints(schema) {
|
|
63
|
+
const c = {};
|
|
64
|
+
const minProperties = getNumber(schema, "minProperties");
|
|
65
|
+
if (minProperties !== void 0) c.minProperties = minProperties;
|
|
66
|
+
const maxProperties = getNumber(schema, "maxProperties");
|
|
67
|
+
if (maxProperties !== void 0) c.maxProperties = maxProperties;
|
|
68
|
+
return c;
|
|
69
|
+
}
|
|
70
|
+
function extractFileConstraints(schema) {
|
|
71
|
+
const c = {};
|
|
72
|
+
const contentMediaType = getString(schema, "contentMediaType");
|
|
73
|
+
if (contentMediaType !== void 0) c.mimeTypes = [contentMediaType];
|
|
74
|
+
return c;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Constraint keywords that apply only to specific types.
|
|
78
|
+
* Used to strip inapplicable constraints when expanding type arrays.
|
|
79
|
+
*/
|
|
80
|
+
const STRING_CONSTRAINTS = new Set([
|
|
81
|
+
"minLength",
|
|
82
|
+
"maxLength",
|
|
83
|
+
"pattern"
|
|
84
|
+
]);
|
|
85
|
+
const NUMBER_CONSTRAINTS = new Set([
|
|
86
|
+
"minimum",
|
|
87
|
+
"maximum",
|
|
88
|
+
"exclusiveMinimum",
|
|
89
|
+
"exclusiveMaximum",
|
|
90
|
+
"multipleOf"
|
|
91
|
+
]);
|
|
92
|
+
const ARRAY_CONSTRAINTS = new Set([
|
|
93
|
+
"minItems",
|
|
94
|
+
"maxItems",
|
|
95
|
+
"uniqueItems",
|
|
96
|
+
"contains",
|
|
97
|
+
"minContains",
|
|
98
|
+
"maxContains"
|
|
99
|
+
]);
|
|
100
|
+
const OBJECT_CONSTRAINTS = new Set(["minProperties", "maxProperties"]);
|
|
101
|
+
const ALL_CONSTRAINTS = new Set([
|
|
102
|
+
...STRING_CONSTRAINTS,
|
|
103
|
+
...NUMBER_CONSTRAINTS,
|
|
104
|
+
...ARRAY_CONSTRAINTS,
|
|
105
|
+
...OBJECT_CONSTRAINTS
|
|
106
|
+
]);
|
|
107
|
+
/**
|
|
108
|
+
* Return a copy of the schema with constraint keywords that don't apply
|
|
109
|
+
* to the given type removed. Meta keywords (description, title, etc.)
|
|
110
|
+
* and composition keywords are always preserved.
|
|
111
|
+
*/
|
|
112
|
+
function stripInapplicableConstraints(schema, targetType) {
|
|
113
|
+
let keepForType;
|
|
114
|
+
switch (targetType) {
|
|
115
|
+
case "string":
|
|
116
|
+
keepForType = STRING_CONSTRAINTS;
|
|
117
|
+
break;
|
|
118
|
+
case "number":
|
|
119
|
+
case "integer":
|
|
120
|
+
keepForType = NUMBER_CONSTRAINTS;
|
|
121
|
+
break;
|
|
122
|
+
case "array":
|
|
123
|
+
keepForType = ARRAY_CONSTRAINTS;
|
|
124
|
+
break;
|
|
125
|
+
case "object":
|
|
126
|
+
keepForType = OBJECT_CONSTRAINTS;
|
|
127
|
+
break;
|
|
128
|
+
default: keepForType = /* @__PURE__ */ new Set();
|
|
129
|
+
}
|
|
130
|
+
const result = {};
|
|
131
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
132
|
+
if (ALL_CONSTRAINTS.has(key) && !keepForType.has(key)) continue;
|
|
133
|
+
result[key] = value;
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
export { extractArrayConstraints, extractFileConstraints, extractNumberConstraints, extractObjectConstraints, extractStringConstraints, stripInapplicableConstraints };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/core/merge.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Schema merging, nullable detection, and discriminated union detection.
|
|
4
|
+
*
|
|
5
|
+
* Used by the walker to handle `allOf`, `anyOf [T, null]`, and
|
|
6
|
+
* `oneOf` with discriminator properties.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Merge multiple JSON Schema objects from allOf into one.
|
|
10
|
+
* Merges: properties, required, meta fields, and constraints.
|
|
11
|
+
*/
|
|
12
|
+
declare function mergeAllOf(schemas: unknown[]): Record<string, unknown>;
|
|
13
|
+
interface NormalisedAnyOf {
|
|
14
|
+
inner: Record<string, unknown>;
|
|
15
|
+
isNullable: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Detect `anyOf: [T, { type: "null" }]` → nullable T.
|
|
19
|
+
* Returns the non-null schema and a nullable flag.
|
|
20
|
+
*/
|
|
21
|
+
declare function normaliseAnyOf(options: unknown[]): NormalisedAnyOf | undefined;
|
|
22
|
+
interface Discriminated {
|
|
23
|
+
options: Record<string, unknown>[];
|
|
24
|
+
discriminator: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Detect oneOf where every option is an object with a property
|
|
28
|
+
* that has a `const` value → discriminated union.
|
|
29
|
+
*/
|
|
30
|
+
declare function detectDiscriminated(options: unknown[]): Discriminated | undefined;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { Discriminated, NormalisedAnyOf, detectDiscriminated, mergeAllOf, normaliseAnyOf };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { isObject } from "./guards.mjs";
|
|
2
|
+
//#region src/core/merge.ts
|
|
3
|
+
/**
|
|
4
|
+
* Schema merging, nullable detection, and discriminated union detection.
|
|
5
|
+
*
|
|
6
|
+
* Used by the walker to handle `allOf`, `anyOf [T, null]`, and
|
|
7
|
+
* `oneOf` with discriminator properties.
|
|
8
|
+
*/
|
|
9
|
+
function getString(obj, key) {
|
|
10
|
+
const value = obj[key];
|
|
11
|
+
return typeof value === "string" ? value : void 0;
|
|
12
|
+
}
|
|
13
|
+
function getArray(obj, key) {
|
|
14
|
+
const value = obj[key];
|
|
15
|
+
return Array.isArray(value) ? value : void 0;
|
|
16
|
+
}
|
|
17
|
+
function getObject(obj, key) {
|
|
18
|
+
const value = obj[key];
|
|
19
|
+
return isObject(value) ? value : void 0;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Merge multiple JSON Schema objects from allOf into one.
|
|
23
|
+
* Merges: properties, required, meta fields, and constraints.
|
|
24
|
+
*/
|
|
25
|
+
function mergeAllOf(schemas) {
|
|
26
|
+
const merged = {};
|
|
27
|
+
const properties = {};
|
|
28
|
+
const required = [];
|
|
29
|
+
for (const entry of schemas) {
|
|
30
|
+
if (!isObject(entry)) continue;
|
|
31
|
+
const props = getObject(entry, "properties");
|
|
32
|
+
if (props !== void 0) for (const [key, value] of Object.entries(props)) properties[key] = value;
|
|
33
|
+
const req = getArray(entry, "required");
|
|
34
|
+
if (req !== void 0) {
|
|
35
|
+
for (const r of req) if (typeof r === "string" && !required.includes(r)) required.push(r);
|
|
36
|
+
}
|
|
37
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
38
|
+
if (key === "properties" || key === "required" || key === "allOf" || key === "type") continue;
|
|
39
|
+
if (!(key in merged)) merged[key] = value;
|
|
40
|
+
}
|
|
41
|
+
if (!("type" in merged)) {
|
|
42
|
+
const type = getString(entry, "type");
|
|
43
|
+
if (type !== void 0) merged.type = type;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (Object.keys(properties).length > 0) merged.properties = properties;
|
|
47
|
+
if (required.length > 0) merged.required = required;
|
|
48
|
+
return merged;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Detect `anyOf: [T, { type: "null" }]` → nullable T.
|
|
52
|
+
* Returns the non-null schema and a nullable flag.
|
|
53
|
+
*/
|
|
54
|
+
function normaliseAnyOf(options) {
|
|
55
|
+
if (options.length !== 2) return void 0;
|
|
56
|
+
let inner;
|
|
57
|
+
let hasNull = false;
|
|
58
|
+
for (const opt of options) {
|
|
59
|
+
if (!isObject(opt)) return void 0;
|
|
60
|
+
if (opt.type === "null") hasNull = true;
|
|
61
|
+
else inner = opt;
|
|
62
|
+
}
|
|
63
|
+
if (!hasNull || inner === void 0) return void 0;
|
|
64
|
+
return {
|
|
65
|
+
inner,
|
|
66
|
+
isNullable: true
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Detect oneOf where every option is an object with a property
|
|
71
|
+
* that has a `const` value → discriminated union.
|
|
72
|
+
*/
|
|
73
|
+
function detectDiscriminated(options) {
|
|
74
|
+
if (options.length === 0) return void 0;
|
|
75
|
+
let discriminator;
|
|
76
|
+
for (const opt of options) {
|
|
77
|
+
if (!isObject(opt)) return void 0;
|
|
78
|
+
const props = getObject(opt, "properties");
|
|
79
|
+
if (props === void 0) return void 0;
|
|
80
|
+
let foundKey;
|
|
81
|
+
for (const [key, value] of Object.entries(props)) if (isObject(value) && "const" in value) {
|
|
82
|
+
foundKey = key;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
if (foundKey === void 0) return void 0;
|
|
86
|
+
if (discriminator === void 0) discriminator = foundKey;
|
|
87
|
+
else if (discriminator !== foundKey) return;
|
|
88
|
+
}
|
|
89
|
+
if (discriminator === void 0) return void 0;
|
|
90
|
+
return {
|
|
91
|
+
options: options.filter(isObject),
|
|
92
|
+
discriminator
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { detectDiscriminated, mergeAllOf, normaliseAnyOf };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { n as OpenApiVersionInfo, t as JsonSchemaDraft } from "../version-CLchheaH.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/core/normalise.d.ts
|
|
4
|
+
type NodeTransform = (node: Record<string, unknown>) => Record<string, unknown>;
|
|
5
|
+
/**
|
|
6
|
+
* Deep-normalise a JSON Schema object by applying a per-node transform
|
|
7
|
+
* and recursing into every sub-schema location.
|
|
8
|
+
*/
|
|
9
|
+
declare function deepNormalise(schema: Record<string, unknown>, transform: NodeTransform): Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Normalise Draft 04 `exclusiveMinimum`/`exclusiveMaximum` from boolean
|
|
12
|
+
* to number form.
|
|
13
|
+
*
|
|
14
|
+
* In Draft 04:
|
|
15
|
+
* - `exclusiveMinimum: true` + `minimum: 5` → value must be > 5
|
|
16
|
+
* - `exclusiveMinimum: false` (or absent) + `minimum: 5` → value must be >= 5
|
|
17
|
+
*
|
|
18
|
+
* In Draft 06+:
|
|
19
|
+
* - `exclusiveMinimum: 5` → value must be > 5 (no separate `minimum`)
|
|
20
|
+
* - `minimum: 5` → value must be >= 5
|
|
21
|
+
*
|
|
22
|
+
* The transform converts boolean form to number form so the walker can
|
|
23
|
+
* treat `exclusiveMinimum`/`exclusiveMaximum` uniformly as numbers.
|
|
24
|
+
*/
|
|
25
|
+
declare function normaliseDraft04Node(node: Record<string, unknown>): Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Normalise a JSON Schema to canonical Draft 2020-12 form.
|
|
28
|
+
* Deep-clones the input — the original is never mutated.
|
|
29
|
+
*/
|
|
30
|
+
declare function normaliseJsonSchema(schema: Record<string, unknown>, draft: JsonSchemaDraft): Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Normalise an OpenAPI document's schemas for walker consumption.
|
|
33
|
+
* Handles version-specific keyword transformations.
|
|
34
|
+
*
|
|
35
|
+
* Returns the same object reference if no normalisation is needed
|
|
36
|
+
* (OpenAPI 3.1.x), or a deep-cloned normalised copy otherwise.
|
|
37
|
+
*/
|
|
38
|
+
declare function normaliseOpenApiSchemas(doc: Record<string, unknown>, version: OpenApiVersionInfo): Record<string, unknown>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { NodeTransform, deepNormalise, normaliseDraft04Node, normaliseJsonSchema, normaliseOpenApiSchemas };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { isObject } from "./guards.mjs";
|
|
2
|
+
import { isOpenApi30, isSwagger2 } from "./version.mjs";
|
|
3
|
+
import { deepNormaliseOpenApi30Doc } from "./openapi30.mjs";
|
|
4
|
+
import { normaliseSwagger2Document } from "./swagger2.mjs";
|
|
5
|
+
//#region src/core/normalise.ts
|
|
6
|
+
/**
|
|
7
|
+
* Keys whose values are `Record<string, SubSchema>` — objects where each
|
|
8
|
+
* property is a sub-schema.
|
|
9
|
+
*/
|
|
10
|
+
const OBJECT_SUBSCHEMA_KEYS = new Set([
|
|
11
|
+
"properties",
|
|
12
|
+
"patternProperties",
|
|
13
|
+
"$defs",
|
|
14
|
+
"definitions",
|
|
15
|
+
"dependentSchemas"
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Keys whose values are `SubSchema[]` — arrays of sub-schemas.
|
|
19
|
+
*/
|
|
20
|
+
const ARRAY_SUBSCHEMA_KEYS = new Set([
|
|
21
|
+
"allOf",
|
|
22
|
+
"anyOf",
|
|
23
|
+
"oneOf",
|
|
24
|
+
"prefixItems"
|
|
25
|
+
]);
|
|
26
|
+
/**
|
|
27
|
+
* Keys whose values are a single sub-schema object.
|
|
28
|
+
*/
|
|
29
|
+
const SINGLE_SUBSCHEMA_KEYS = new Set([
|
|
30
|
+
"additionalProperties",
|
|
31
|
+
"not",
|
|
32
|
+
"contains",
|
|
33
|
+
"propertyNames",
|
|
34
|
+
"if",
|
|
35
|
+
"then",
|
|
36
|
+
"else",
|
|
37
|
+
"unevaluatedProperties",
|
|
38
|
+
"unevaluatedItems"
|
|
39
|
+
]);
|
|
40
|
+
/**
|
|
41
|
+
* Normalise each element of an unknown array by applying deepNormalise
|
|
42
|
+
* to object elements and passing others through unchanged.
|
|
43
|
+
*/
|
|
44
|
+
function normaliseArray(items, transform) {
|
|
45
|
+
const result = [];
|
|
46
|
+
for (const item of items) result.push(isObject(item) ? deepNormalise(item, transform) : item);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Normalise each value of a sub-schema map (e.g. properties, $defs).
|
|
51
|
+
*/
|
|
52
|
+
function normaliseSubSchemaMap(map, transform) {
|
|
53
|
+
const result = {};
|
|
54
|
+
for (const [k, v] of Object.entries(map)) result[k] = isObject(v) ? deepNormalise(v, transform) : v;
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Deep-normalise a JSON Schema object by applying a per-node transform
|
|
59
|
+
* and recursing into every sub-schema location.
|
|
60
|
+
*/
|
|
61
|
+
function deepNormalise(schema, transform) {
|
|
62
|
+
const node = transform({ ...schema });
|
|
63
|
+
const result = {};
|
|
64
|
+
for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMap(value, transform);
|
|
65
|
+
else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArray(value, transform);
|
|
66
|
+
else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormalise(value, transform);
|
|
67
|
+
else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArray(value, transform);
|
|
68
|
+
else if (isObject(value)) result[key] = deepNormalise(value, transform);
|
|
69
|
+
else result[key] = value;
|
|
70
|
+
else result[key] = value;
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Normalise Draft 04 `exclusiveMinimum`/`exclusiveMaximum` from boolean
|
|
75
|
+
* to number form.
|
|
76
|
+
*
|
|
77
|
+
* In Draft 04:
|
|
78
|
+
* - `exclusiveMinimum: true` + `minimum: 5` → value must be > 5
|
|
79
|
+
* - `exclusiveMinimum: false` (or absent) + `minimum: 5` → value must be >= 5
|
|
80
|
+
*
|
|
81
|
+
* In Draft 06+:
|
|
82
|
+
* - `exclusiveMinimum: 5` → value must be > 5 (no separate `minimum`)
|
|
83
|
+
* - `minimum: 5` → value must be >= 5
|
|
84
|
+
*
|
|
85
|
+
* The transform converts boolean form to number form so the walker can
|
|
86
|
+
* treat `exclusiveMinimum`/`exclusiveMaximum` uniformly as numbers.
|
|
87
|
+
*/
|
|
88
|
+
function normaliseDraft04Node(node) {
|
|
89
|
+
if (node.exclusiveMinimum === true && typeof node.minimum === "number") {
|
|
90
|
+
node.exclusiveMinimum = node.minimum;
|
|
91
|
+
delete node.minimum;
|
|
92
|
+
} else if (node.exclusiveMinimum === false) delete node.exclusiveMinimum;
|
|
93
|
+
if (node.exclusiveMaximum === true && typeof node.maximum === "number") {
|
|
94
|
+
node.exclusiveMaximum = node.maximum;
|
|
95
|
+
delete node.maximum;
|
|
96
|
+
} else if (node.exclusiveMaximum === false) delete node.exclusiveMaximum;
|
|
97
|
+
if (typeof node.id === "string" && !("$id" in node)) {
|
|
98
|
+
node.$id = node.id;
|
|
99
|
+
delete node.id;
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(node.items) && !("prefixItems" in node)) {
|
|
102
|
+
node.prefixItems = node.items;
|
|
103
|
+
delete node.items;
|
|
104
|
+
if ("additionalItems" in node) {
|
|
105
|
+
node.items = node.additionalItems;
|
|
106
|
+
delete node.additionalItems;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return node;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Normalise Draft 2019-09 `$recursiveRef` to `$ref`.
|
|
113
|
+
*
|
|
114
|
+
* `$recursiveRef` resolves to the nearest `$recursiveAnchor` in the
|
|
115
|
+
* dynamic scope. For rendering, the common pattern is a root
|
|
116
|
+
* `$recursiveAnchor: true` — the normaliser converts
|
|
117
|
+
* `$recursiveRef: "#"` to `$ref: "#"` pointing to the root.
|
|
118
|
+
*
|
|
119
|
+
* If a `$recursiveAnchor` name is given (non-empty string), the ref
|
|
120
|
+
* is converted to `$ref: "#<anchor>"` so the existing $anchor
|
|
121
|
+
* resolution in ref.ts can find it.
|
|
122
|
+
*/
|
|
123
|
+
function normaliseDraft201909Node(node) {
|
|
124
|
+
if (typeof node.$recursiveRef === "string") {
|
|
125
|
+
node.$ref = "#";
|
|
126
|
+
delete node.$recursiveRef;
|
|
127
|
+
}
|
|
128
|
+
if (node.$recursiveAnchor === true) {
|
|
129
|
+
if (typeof node.$anchor !== "string") node.$anchor = "__recursive__";
|
|
130
|
+
delete node.$recursiveAnchor;
|
|
131
|
+
}
|
|
132
|
+
return node;
|
|
133
|
+
}
|
|
134
|
+
function normaliseDynamicRefNode(node) {
|
|
135
|
+
if (typeof node.$dynamicRef === "string") {
|
|
136
|
+
node.$ref = node.$dynamicRef;
|
|
137
|
+
delete node.$dynamicRef;
|
|
138
|
+
}
|
|
139
|
+
if (typeof node.$dynamicAnchor === "string") {
|
|
140
|
+
if (typeof node.$anchor !== "string") node.$anchor = node.$dynamicAnchor;
|
|
141
|
+
delete node.$dynamicAnchor;
|
|
142
|
+
}
|
|
143
|
+
return node;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Normalise a JSON Schema to canonical Draft 2020-12 form.
|
|
147
|
+
* Deep-clones the input — the original is never mutated.
|
|
148
|
+
*/
|
|
149
|
+
function normaliseJsonSchema(schema, draft) {
|
|
150
|
+
switch (draft) {
|
|
151
|
+
case "draft-04": return deepNormalise(schema, normaliseDraft04Node);
|
|
152
|
+
case "draft-2019-09": return deepNormalise(schema, normaliseDraft201909Node);
|
|
153
|
+
case "draft-2020-12": return deepNormalise(schema, normaliseDynamicRefNode);
|
|
154
|
+
case "draft-06":
|
|
155
|
+
case "draft-07": return schema;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Normalise an OpenAPI document's schemas for walker consumption.
|
|
160
|
+
* Handles version-specific keyword transformations.
|
|
161
|
+
*
|
|
162
|
+
* Returns the same object reference if no normalisation is needed
|
|
163
|
+
* (OpenAPI 3.1.x), or a deep-cloned normalised copy otherwise.
|
|
164
|
+
*/
|
|
165
|
+
function normaliseOpenApiSchemas(doc, version) {
|
|
166
|
+
if (isSwagger2(version)) return normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node);
|
|
167
|
+
if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(doc, deepNormalise);
|
|
168
|
+
return doc;
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
export { deepNormalise, normaliseDraft04Node, normaliseJsonSchema, normaliseOpenApiSchemas };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NodeTransform } from "./normalise.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/core/openapi30.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Normalise OpenAPI 3.0.x `nullable` keyword to `anyOf [T, null]`.
|
|
6
|
+
*
|
|
7
|
+
* OpenAPI 3.0 uses `nullable: true` instead of the JSON Schema standard
|
|
8
|
+
* `anyOf: [T, { type: "null" }]`. The walker understands the latter form
|
|
9
|
+
* natively, so this normaliser converts `nullable` to `anyOf`.
|
|
10
|
+
*
|
|
11
|
+
* Only applied when `nullable` is explicitly `true`. `nullable: false` or
|
|
12
|
+
* absent is the default and requires no transformation.
|
|
13
|
+
*/
|
|
14
|
+
declare function normaliseOpenApi30Node(node: Record<string, unknown>): Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Normalise OpenAPI 3.0.x `discriminator` keyword by injecting `const`
|
|
17
|
+
* values into each `oneOf`/`anyOf` option's discriminator property.
|
|
18
|
+
*
|
|
19
|
+
* In OpenAPI 3.0, `discriminator` is a sibling of `oneOf`/`anyOf`:
|
|
20
|
+
* discriminator: { propertyName: "type" }
|
|
21
|
+
* The walker detects discriminated unions from `oneOf` + `const` on a
|
|
22
|
+
* property, so this normaliser injects the `const` values from the
|
|
23
|
+
* `mapping` or infers them from `$ref` fragment names.
|
|
24
|
+
*/
|
|
25
|
+
declare function normaliseOpenApi30Discriminator(node: Record<string, unknown>): Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Combined OpenAPI 3.0.x node transform: nullable + discriminator.
|
|
28
|
+
* Applied to every schema node in an OpenAPI 3.0 document.
|
|
29
|
+
*/
|
|
30
|
+
declare function normaliseOpenApi30Combined(node: Record<string, unknown>): Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Deep-normalise all schemas in an OpenAPI 3.0.x document.
|
|
33
|
+
* Walks components/schemas, path operations, parameters, request bodies,
|
|
34
|
+
* and responses — applying `nullable` normalisation to each schema.
|
|
35
|
+
*/
|
|
36
|
+
declare function deepNormaliseOpenApi30Doc(doc: Record<string, unknown>, deepNormalise: (schema: Record<string, unknown>, transform: NodeTransform) => Record<string, unknown>): Record<string, unknown>;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { deepNormaliseOpenApi30Doc, normaliseOpenApi30Combined, normaliseOpenApi30Discriminator, normaliseOpenApi30Node };
|