@orpc/json-schema 2.0.0-beta.47 → 2.0.0-beta.48
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 +2 -3
- package/dist/coercer.d.mts +29 -0
- package/dist/coercer.mjs +465 -0
- package/dist/composition-utils.d.mts +41 -0
- package/dist/composition-utils.mjs +357 -0
- package/dist/constants.d.mts +4 -0
- package/dist/constants.mjs +61 -0
- package/dist/convert.d.mts +29 -0
- package/dist/convert.mjs +14 -0
- package/dist/index.d.mts +10 -297
- package/dist/index.mjs +9 -1117
- package/dist/ref-utils.d.mts +44 -0
- package/dist/ref-utils.mjs +117 -0
- package/dist/smart-coercion-handler-plugin.d.mts +22 -0
- package/dist/smart-coercion-handler-plugin.mjs +42 -0
- package/dist/smart-coercion-link-plugin.d.mts +28 -0
- package/dist/smart-coercion-link-plugin.mjs +61 -0
- package/dist/standard-json-schema-converter.d.mts +16 -0
- package/dist/standard-json-schema-converter.mjs +30 -0
- package/dist/types.d.mts +51 -0
- package/dist/types.mjs +40 -0
- package/dist/utils.d.mts +40 -0
- package/dist/utils.mjs +50 -0
- package/package.json +6 -6
- package/dist/index.d.ts +0 -297
package/README.md
CHANGED
|
@@ -191,15 +191,14 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
|
|
|
191
191
|
<td align="center"><a href="https://kellychan.im/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kelly Peilin Chan"><img src="https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&v=4" width="139" alt="Kelly Peilin Chan"/><br />Kelly Peilin Chan</a></td>
|
|
192
192
|
<td align="center"><a href="https://guyariely.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Guy Ariely"><img src="https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&v=4" width="139" alt="Guy Ariely"/><br />Guy Ariely</a></td>
|
|
193
193
|
<td align="center"><a href="https://paulsenon.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="PaulSenon"><img src="https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&v=4" width="139" alt="PaulSenon"/><br />PaulSenon</a></td>
|
|
194
|
-
<td align="center"><a href="https://
|
|
194
|
+
<td align="center"><a href="https://piscis.dev/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Alex"><img src="https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&v=4" width="139" alt="Alex"/><br />Alex</a></td>
|
|
195
195
|
</tr>
|
|
196
196
|
<tr>
|
|
197
|
-
<td align="center"><a href="https://piscis.dev/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Alex"><img src="https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&v=4" width="139" alt="Alex"/><br />Alex</a></td>
|
|
198
197
|
<td align="center"><a href="https://opensource.gubanov.eu/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Andrey Gubanov"><img src="https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&v=4" width="139" alt="Andrey Gubanov"/><br />Andrey Gubanov</a></td>
|
|
199
198
|
</tr>
|
|
200
199
|
</table>
|
|
201
200
|
|
|
202
|
-
With thanks to [
|
|
201
|
+
With thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.
|
|
203
202
|
|
|
204
203
|
## Open Source Programs
|
|
205
204
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { JsonSchema } from "./types.mjs";
|
|
2
|
+
export interface JsonSchemaCoercerOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Maximum digits, excluding a leading `-`, of a string coerced to `bigint`. Longer strings are left unchanged.
|
|
5
|
+
* `Number.POSITIVE_INFINITY` disables it.
|
|
6
|
+
*
|
|
7
|
+
* @default 4300
|
|
8
|
+
*/
|
|
9
|
+
maxBigIntDigits?: number | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Maximum coercion steps per input value, where a step applies one schema to one value.
|
|
12
|
+
* Inputs with fewer than 100 values are budgeted as 100. Past the budget the whole input is left uncoerced,
|
|
13
|
+
* so validation still runs and reports any mismatch.
|
|
14
|
+
*
|
|
15
|
+
* @default 1000
|
|
16
|
+
*/
|
|
17
|
+
maxStepsPerValue?: number | undefined;
|
|
18
|
+
}
|
|
19
|
+
export declare class JsonSchemaCoercer {
|
|
20
|
+
private readonly nativeCoerceOptions;
|
|
21
|
+
private readonly maxStepsPerValue;
|
|
22
|
+
/** Reused between calls, since building a context is slow until V8 warms up. */
|
|
23
|
+
private idleContext;
|
|
24
|
+
constructor(options?: JsonSchemaCoercerOptions);
|
|
25
|
+
coerce([schema, optional]: [schema: JsonSchema, optional: boolean], value: unknown): unknown;
|
|
26
|
+
private coerceInternal;
|
|
27
|
+
private coerceRef;
|
|
28
|
+
private extendBudget;
|
|
29
|
+
}
|
package/dist/coercer.mjs
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { DEFAULT_MAX_BIGINT_DIGITS, get, getOwn, isPlainObject, isTypescriptObject, parseBigIntString, toArray, tryOrUndefined } from "@orpc/shared";
|
|
2
|
+
import { decodeJsonPointerSegment } from "./ref-utils.mjs";
|
|
3
|
+
import { JsonSchemaXNativeType } from "./types.mjs";
|
|
4
|
+
|
|
5
|
+
const UNSATISFIED = 0;
|
|
6
|
+
|
|
7
|
+
const LOOSELY_SATISFIED = 1;
|
|
8
|
+
|
|
9
|
+
const SATISFIED = 2;
|
|
10
|
+
|
|
11
|
+
const NATIVE_TYPES = /* @__PURE__ */ (() => new Map([
|
|
12
|
+
[JsonSchemaXNativeType.Date, {
|
|
13
|
+
types: ["string"],
|
|
14
|
+
is: (value) => value instanceof Date,
|
|
15
|
+
coerce: (value) => typeof value === "string" ? stringToDate(value) : value
|
|
16
|
+
}],
|
|
17
|
+
[JsonSchemaXNativeType.BigInt, {
|
|
18
|
+
types: [
|
|
19
|
+
"string",
|
|
20
|
+
"integer",
|
|
21
|
+
"number"
|
|
22
|
+
],
|
|
23
|
+
is: (value) => typeof value === "bigint",
|
|
24
|
+
coerce: (value, { maxBigIntDigits }) => typeof value === "string" ? stringToBigInt(value, maxBigIntDigits) : typeof value === "number" ? numberToBigInt(value) : value
|
|
25
|
+
}],
|
|
26
|
+
[JsonSchemaXNativeType.Url, {
|
|
27
|
+
types: ["string"],
|
|
28
|
+
is: (value) => value instanceof URL,
|
|
29
|
+
coerce: (value) => typeof value === "string" ? stringToURL(value) : value
|
|
30
|
+
}],
|
|
31
|
+
[JsonSchemaXNativeType.Set, {
|
|
32
|
+
types: ["array"],
|
|
33
|
+
is: (value) => value instanceof Set,
|
|
34
|
+
coerce: (value) => Array.isArray(value) ? arrayToSet(value) : value,
|
|
35
|
+
afterTypeCheck: true
|
|
36
|
+
}],
|
|
37
|
+
[JsonSchemaXNativeType.Map, {
|
|
38
|
+
types: ["array"],
|
|
39
|
+
is: (value) => value instanceof Map,
|
|
40
|
+
coerce: (value) => Array.isArray(value) ? arrayToMap(value) : value,
|
|
41
|
+
afterTypeCheck: true
|
|
42
|
+
}]
|
|
43
|
+
]))();
|
|
44
|
+
function minSatisfaction(a, b) {
|
|
45
|
+
return a < b ? a : b;
|
|
46
|
+
}
|
|
47
|
+
const MIN_BUDGETED_VALUES = 100;
|
|
48
|
+
function createContext() {
|
|
49
|
+
return {
|
|
50
|
+
rootSchema: true,
|
|
51
|
+
memo: undefined,
|
|
52
|
+
memoize: false,
|
|
53
|
+
steps: 0,
|
|
54
|
+
maxSteps: 0,
|
|
55
|
+
input: undefined
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
class CoercionBudgetExceededError extends Error {}
|
|
59
|
+
export class JsonSchemaCoercer {
|
|
60
|
+
nativeCoerceOptions;
|
|
61
|
+
maxStepsPerValue;
|
|
62
|
+
|
|
63
|
+
idleContext = createContext();
|
|
64
|
+
constructor(options = {}) {
|
|
65
|
+
this.nativeCoerceOptions = { maxBigIntDigits: options.maxBigIntDigits ?? DEFAULT_MAX_BIGINT_DIGITS };
|
|
66
|
+
this.maxStepsPerValue = options.maxStepsPerValue ?? 1e3;
|
|
67
|
+
}
|
|
68
|
+
coerce([schema, optional], value) {
|
|
69
|
+
if (optional && value === undefined) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
const context = this.idleContext ?? createContext();
|
|
73
|
+
this.idleContext = undefined;
|
|
74
|
+
context.rootSchema = schema;
|
|
75
|
+
context.memoize = false;
|
|
76
|
+
context.steps = 0;
|
|
77
|
+
context.maxSteps = this.maxStepsPerValue * MIN_BUDGETED_VALUES;
|
|
78
|
+
context.input = value;
|
|
79
|
+
try {
|
|
80
|
+
const [, coerced] = this.coerceInternal(context, schema, value);
|
|
81
|
+
return coerced;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
|
|
84
|
+
if (error instanceof CoercionBudgetExceededError) {
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
} finally {
|
|
89
|
+
|
|
90
|
+
context.memo = undefined;
|
|
91
|
+
context.input = undefined;
|
|
92
|
+
this.idleContext = context;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
coerceInternal(context, schema, value, appliedRefs) {
|
|
96
|
+
if (++context.steps > context.maxSteps) {
|
|
97
|
+
this.extendBudget(context);
|
|
98
|
+
}
|
|
99
|
+
if (typeof schema === "boolean") {
|
|
100
|
+
return [schema ? SATISFIED : UNSATISFIED, value];
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(schema.type)) {
|
|
103
|
+
return this.coerceInternal(context, { anyOf: schema.type.map((type) => ({
|
|
104
|
+
...schema,
|
|
105
|
+
type
|
|
106
|
+
})) }, value, appliedRefs);
|
|
107
|
+
}
|
|
108
|
+
let coerced = value;
|
|
109
|
+
let satisfied = SATISFIED;
|
|
110
|
+
if (typeof schema.$ref === "string") {
|
|
111
|
+
const resolved = schema.$ref.startsWith("#/") ? get(context.rootSchema, schema.$ref.slice("#/".length).split("/").map(decodeJsonPointerSegment)) : schema.$ref === "#" ? context.rootSchema : undefined;
|
|
112
|
+
if (resolved !== undefined && !appliedRefs?.has(resolved)) {
|
|
113
|
+
if (schema.type === "object" || schema.type === "array") {
|
|
114
|
+
context.memoize = true;
|
|
115
|
+
}
|
|
116
|
+
const [subSatisfied, subCoerced] = this.coerceRef(context, resolved, coerced, appliedRefs);
|
|
117
|
+
coerced = subCoerced;
|
|
118
|
+
satisfied = minSatisfaction(satisfied, subSatisfied);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const enumValues = schema.const !== undefined ? [schema.const] : schema.enum;
|
|
122
|
+
if (enumValues !== undefined && !enumValues.includes(coerced)) {
|
|
123
|
+
if (typeof coerced === "string") {
|
|
124
|
+
const numberValue = stringToNumber(coerced);
|
|
125
|
+
if (enumValues.includes(numberValue)) {
|
|
126
|
+
coerced = numberValue;
|
|
127
|
+
} else {
|
|
128
|
+
const booleanValue = stringToBoolean(coerced);
|
|
129
|
+
if (enumValues.includes(booleanValue)) {
|
|
130
|
+
coerced = booleanValue;
|
|
131
|
+
} else {
|
|
132
|
+
satisfied = UNSATISFIED;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
satisfied = UNSATISFIED;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const declaredNativeType = NATIVE_TYPES.get(schema["x-native-type"]);
|
|
140
|
+
const nativeType = schema.type === undefined || declaredNativeType?.types.includes(schema.type) ? declaredNativeType : undefined;
|
|
141
|
+
if (nativeType && !nativeType.afterTypeCheck) {
|
|
142
|
+
coerced = nativeType.coerce(coerced, this.nativeCoerceOptions);
|
|
143
|
+
}
|
|
144
|
+
if (schema.type && !nativeType?.is(coerced)) {
|
|
145
|
+
switch (schema.type) {
|
|
146
|
+
case "null": {
|
|
147
|
+
if (coerced !== null) {
|
|
148
|
+
satisfied = UNSATISFIED;
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "string": {
|
|
153
|
+
if (typeof coerced !== "string") {
|
|
154
|
+
satisfied = UNSATISFIED;
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case "number": {
|
|
159
|
+
if (typeof coerced === "string") {
|
|
160
|
+
coerced = stringToNumber(coerced);
|
|
161
|
+
}
|
|
162
|
+
if (typeof coerced !== "number") {
|
|
163
|
+
satisfied = UNSATISFIED;
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case "integer": {
|
|
168
|
+
if (typeof coerced === "string") {
|
|
169
|
+
coerced = stringToInteger(coerced);
|
|
170
|
+
}
|
|
171
|
+
if (typeof coerced !== "number" || !Number.isInteger(coerced)) {
|
|
172
|
+
satisfied = UNSATISFIED;
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case "boolean": {
|
|
177
|
+
if (typeof coerced === "string") {
|
|
178
|
+
coerced = stringToBoolean(coerced);
|
|
179
|
+
}
|
|
180
|
+
if (typeof coerced !== "boolean") {
|
|
181
|
+
satisfied = UNSATISFIED;
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "array": {
|
|
186
|
+
if (Array.isArray(coerced)) {
|
|
187
|
+
const prefixItemSchemas = "prefixItems" in schema ? toArray(schema.prefixItems) : Array.isArray(schema.items) ? schema.items : [];
|
|
188
|
+
const itemSchema = Array.isArray(schema.items) ? schema.additionalItems : schema.items;
|
|
189
|
+
let shouldUseCoercedItems = false;
|
|
190
|
+
const coercedItems = coerced.map((item, i) => {
|
|
191
|
+
const subSchema = prefixItemSchemas[i] ?? itemSchema;
|
|
192
|
+
if (subSchema === undefined) {
|
|
193
|
+
satisfied = minSatisfaction(satisfied, LOOSELY_SATISFIED);
|
|
194
|
+
return item;
|
|
195
|
+
}
|
|
196
|
+
const [subSatisfied, subCoerced] = this.coerceInternal(context, subSchema, item);
|
|
197
|
+
satisfied = minSatisfaction(satisfied, subSatisfied);
|
|
198
|
+
if (subCoerced !== item) {
|
|
199
|
+
shouldUseCoercedItems = true;
|
|
200
|
+
}
|
|
201
|
+
return subCoerced;
|
|
202
|
+
});
|
|
203
|
+
if (typeof schema.minItems === "number" && coercedItems.length < schema.minItems) {
|
|
204
|
+
satisfied = UNSATISFIED;
|
|
205
|
+
}
|
|
206
|
+
if (shouldUseCoercedItems) {
|
|
207
|
+
coerced = coercedItems;
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
satisfied = UNSATISFIED;
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case "object": {
|
|
215
|
+
if (Array.isArray(coerced)) {
|
|
216
|
+
coerced = { ...coerced };
|
|
217
|
+
}
|
|
218
|
+
if (isPlainObject(coerced)) {
|
|
219
|
+
let shouldUseCoercedItems = false;
|
|
220
|
+
|
|
221
|
+
const coercedItems = { ...coerced };
|
|
222
|
+
const patternProperties = Object.entries(schema.patternProperties ?? {}).flatMap(([key, value]) => {
|
|
223
|
+
|
|
224
|
+
const pattern = tryOrUndefined(() => new RegExp(key));
|
|
225
|
+
return pattern ? [[pattern, value]] : [];
|
|
226
|
+
});
|
|
227
|
+
const propertySchemas = schema.properties ?? {};
|
|
228
|
+
for (const key of Object.keys(coerced)) {
|
|
229
|
+
const value = coerced[key];
|
|
230
|
+
const subSchema = getOwn(propertySchemas, key) ?? patternProperties.find(([pattern]) => pattern.test(key))?.[1] ?? schema.additionalProperties;
|
|
231
|
+
if (value !== undefined || schema.required?.includes(key)) {
|
|
232
|
+
if (subSchema === undefined) {
|
|
233
|
+
satisfied = minSatisfaction(satisfied, LOOSELY_SATISFIED);
|
|
234
|
+
} else {
|
|
235
|
+
const [subSatisfied, subCoerced] = this.coerceInternal(context, subSchema, value);
|
|
236
|
+
coercedItems[key] = subCoerced;
|
|
237
|
+
satisfied = minSatisfaction(satisfied, subSatisfied);
|
|
238
|
+
if (subCoerced !== value) {
|
|
239
|
+
shouldUseCoercedItems = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (schema.required?.some((key) => !Object.hasOwn(coercedItems, key))) {
|
|
245
|
+
satisfied = UNSATISFIED;
|
|
246
|
+
}
|
|
247
|
+
if (shouldUseCoercedItems) {
|
|
248
|
+
coerced = coercedItems;
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
satisfied = UNSATISFIED;
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (nativeType) {
|
|
258
|
+
if (nativeType.afterTypeCheck) {
|
|
259
|
+
coerced = nativeType.coerce(coerced, this.nativeCoerceOptions);
|
|
260
|
+
}
|
|
261
|
+
if (!nativeType.is(coerced)) {
|
|
262
|
+
satisfied = UNSATISFIED;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (schema.allOf) {
|
|
266
|
+
context.memoize = true;
|
|
267
|
+
for (const subSchema of schema.allOf) {
|
|
268
|
+
const [subSatisfied, subCoerced] = this.coerceInternal(context, subSchema, coerced, appliedRefs);
|
|
269
|
+
coerced = subCoerced;
|
|
270
|
+
satisfied = minSatisfaction(satisfied, subSatisfied);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
274
|
+
if (schema[key]) {
|
|
275
|
+
context.memoize = true;
|
|
276
|
+
|
|
277
|
+
let best;
|
|
278
|
+
for (const subSchema of schema[key]) {
|
|
279
|
+
const [subSatisfied, subCoerced] = this.coerceInternal(context, subSchema, coerced, appliedRefs);
|
|
280
|
+
if (subSatisfied === UNSATISFIED) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (subCoerced === coerced) {
|
|
284
|
+
best = {
|
|
285
|
+
satisfied: subSatisfied,
|
|
286
|
+
value: subCoerced
|
|
287
|
+
};
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (!best || subSatisfied > best.satisfied) {
|
|
291
|
+
best = {
|
|
292
|
+
satisfied: subSatisfied,
|
|
293
|
+
value: subCoerced
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (best) {
|
|
299
|
+
coerced = best.value;
|
|
300
|
+
satisfied = minSatisfaction(satisfied, best.satisfied);
|
|
301
|
+
} else {
|
|
302
|
+
satisfied = UNSATISFIED;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (typeof schema.not !== "undefined") {
|
|
307
|
+
context.memoize = true;
|
|
308
|
+
const [notSatisfied] = this.coerceInternal(context, schema.not, coerced, appliedRefs);
|
|
309
|
+
if (notSatisfied !== UNSATISFIED) {
|
|
310
|
+
satisfied = UNSATISFIED;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return [satisfied, coerced];
|
|
314
|
+
}
|
|
315
|
+
coerceRef(context, resolved, value, appliedRefs) {
|
|
316
|
+
let memo;
|
|
317
|
+
|
|
318
|
+
if (context.memoize && !appliedRefs?.size && isTypescriptObject(value)) {
|
|
319
|
+
context.memo ??= new Map();
|
|
320
|
+
memo = context.memo.get(resolved);
|
|
321
|
+
if (!memo) {
|
|
322
|
+
memo = new Map();
|
|
323
|
+
context.memo.set(resolved, memo);
|
|
324
|
+
}
|
|
325
|
+
const cached = memo.get(value);
|
|
326
|
+
if (cached) {
|
|
327
|
+
return cached;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
appliedRefs ??= new Set();
|
|
331
|
+
appliedRefs.add(resolved);
|
|
332
|
+
const result = this.coerceInternal(context, resolved, value, appliedRefs);
|
|
333
|
+
appliedRefs.delete(resolved);
|
|
334
|
+
memo?.set(value, result);
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
extendBudget(context) {
|
|
338
|
+
const limit = 2 * context.steps / this.maxStepsPerValue;
|
|
339
|
+
context.maxSteps = this.maxStepsPerValue * Math.max(countValues(context.input, limit), MIN_BUDGETED_VALUES);
|
|
340
|
+
if (context.steps > context.maxSteps) {
|
|
341
|
+
throw new CoercionBudgetExceededError();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function countValues(input, limit) {
|
|
346
|
+
const walked = new Set();
|
|
347
|
+
const stack = [input];
|
|
348
|
+
let count = 1;
|
|
349
|
+
while (stack.length) {
|
|
350
|
+
const value = stack.pop();
|
|
351
|
+
if ((Array.isArray(value) || isPlainObject(value)) && !walked.has(value)) {
|
|
352
|
+
walked.add(value);
|
|
353
|
+
const items = Array.isArray(value) ? value : Object.values(value);
|
|
354
|
+
for (let i = 0; i < items.length; i++) {
|
|
355
|
+
if (++count >= limit) {
|
|
356
|
+
return count;
|
|
357
|
+
}
|
|
358
|
+
const item = items[i];
|
|
359
|
+
if (isTypescriptObject(item)) {
|
|
360
|
+
stack.push(item);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return count;
|
|
366
|
+
}
|
|
367
|
+
const NUMBER_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
368
|
+
function stringToNumber(value) {
|
|
369
|
+
if (!NUMBER_PATTERN.test(value)) {
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
const num = Number(value);
|
|
373
|
+
|
|
374
|
+
if (num < Number.MIN_SAFE_INTEGER || num > Number.MAX_SAFE_INTEGER) {
|
|
375
|
+
return value;
|
|
376
|
+
}
|
|
377
|
+
return num;
|
|
378
|
+
}
|
|
379
|
+
const INTEGER_PATTERN = /^-?(?:0|[1-9]\d*)$/;
|
|
380
|
+
function stringToInteger(value) {
|
|
381
|
+
if (!INTEGER_PATTERN.test(value)) {
|
|
382
|
+
return value;
|
|
383
|
+
}
|
|
384
|
+
const num = Number(value);
|
|
385
|
+
if (!Number.isSafeInteger(num)) {
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
return num;
|
|
389
|
+
}
|
|
390
|
+
function stringToBigInt(value, maxDigits) {
|
|
391
|
+
return parseBigIntString(value, maxDigits) ?? value;
|
|
392
|
+
}
|
|
393
|
+
function numberToBigInt(value) {
|
|
394
|
+
if (!Number.isInteger(value)) {
|
|
395
|
+
return value;
|
|
396
|
+
}
|
|
397
|
+
return BigInt(value);
|
|
398
|
+
}
|
|
399
|
+
function stringToBoolean(value) {
|
|
400
|
+
const lower = value.toLowerCase();
|
|
401
|
+
if (lower === "false" || lower === "off") {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
if (lower === "true" || lower === "on") {
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
const DATE_TIME_PATTERN = /^(?!-0{6})(?:[+-]\d{2})?\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
410
|
+
const DAYS_IN_MONTH = [
|
|
411
|
+
31,
|
|
412
|
+
29,
|
|
413
|
+
31,
|
|
414
|
+
30,
|
|
415
|
+
31,
|
|
416
|
+
30,
|
|
417
|
+
31,
|
|
418
|
+
31,
|
|
419
|
+
30,
|
|
420
|
+
31,
|
|
421
|
+
30,
|
|
422
|
+
31
|
|
423
|
+
];
|
|
424
|
+
function stringToDate(value) {
|
|
425
|
+
if (!DATE_TIME_PATTERN.test(value)) {
|
|
426
|
+
return value;
|
|
427
|
+
}
|
|
428
|
+
const yearEnd = value.indexOf("-", 1);
|
|
429
|
+
const month = readTwoDigits(value, yearEnd + 1);
|
|
430
|
+
const day = readTwoDigits(value, yearEnd + 4);
|
|
431
|
+
if (day < 1 || day > (DAYS_IN_MONTH[month - 1] ?? 0) || month === 2 && day === 29 && !isLeapYear(Number(value.slice(0, yearEnd)))) {
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
const date = new Date(value);
|
|
435
|
+
if (Number.isNaN(date.getTime())) {
|
|
436
|
+
return value;
|
|
437
|
+
}
|
|
438
|
+
return date;
|
|
439
|
+
}
|
|
440
|
+
function readTwoDigits(value, index) {
|
|
441
|
+
return (value.charCodeAt(index) - 48) * 10 + value.charCodeAt(index + 1) - 48;
|
|
442
|
+
}
|
|
443
|
+
function isLeapYear(year) {
|
|
444
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
445
|
+
}
|
|
446
|
+
function stringToURL(value) {
|
|
447
|
+
return tryOrUndefined(() => new URL(value)) ?? value;
|
|
448
|
+
}
|
|
449
|
+
function arrayToSet(value) {
|
|
450
|
+
const set = new Set(value);
|
|
451
|
+
if (set.size !== value.length) {
|
|
452
|
+
return value;
|
|
453
|
+
}
|
|
454
|
+
return set;
|
|
455
|
+
}
|
|
456
|
+
function arrayToMap(value) {
|
|
457
|
+
if (value.some((item) => !Array.isArray(item) || item.length !== 2)) {
|
|
458
|
+
return value;
|
|
459
|
+
}
|
|
460
|
+
const result = new Map(value);
|
|
461
|
+
if (result.size !== value.length) {
|
|
462
|
+
return value;
|
|
463
|
+
}
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* These utilities assume the schema has only one root-level `$defs` object
|
|
3
|
+
* and exclusively use absolute JSON pointers for `$ref` values.
|
|
4
|
+
*/
|
|
5
|
+
import type { JsonSchema } from "./types.mjs";
|
|
6
|
+
import type { JsonArraySchema, JsonObjectSchema } from "./utils.mjs";
|
|
7
|
+
/**
|
|
8
|
+
* Combines multiple schemas under the requested composition keyword, promoting branch `$defs` to the root.
|
|
9
|
+
*/
|
|
10
|
+
export declare function combineJsonSchemasWithComposition(keyword: "allOf" | "anyOf" | "oneOf", schemas: JsonSchema[]): JsonSchema;
|
|
11
|
+
export type JsonObjectSchemaEntry = [name: string, schema: JsonSchema, optional: boolean];
|
|
12
|
+
/**
|
|
13
|
+
* Combines object property entries back into a single object schema.
|
|
14
|
+
* When given, `additionalProperties` constrains every property the entries do not name.
|
|
15
|
+
*/
|
|
16
|
+
export declare function combineJsonObjectSchemaEntries(entries: JsonObjectSchemaEntry[], additionalProperties?: JsonSchema): JsonObjectSchema;
|
|
17
|
+
/**
|
|
18
|
+
* Parses an object schema, or a composition of object schemas, into property entries.
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractJsonObjectSchemaEntries(schema: JsonSchema): JsonObjectSchemaEntry[] | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Returns the `additionalProperties` an object schema declares at its root, following root-local refs,
|
|
23
|
+
* with the root `$defs` attached so it can be passed to {@link combineJsonObjectSchemaEntries}.
|
|
24
|
+
* Composition branches are not inspected, since their keyword only constrains that branch.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractJsonObjectSchemaAdditionalProperties(schema: JsonSchema): JsonSchema | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Returns true when an object can stand for every value the schema describes: the schema constrains objects,
|
|
29
|
+
* by `type` or object keywords, composes only such schemas, or is unconstrained, since an object satisfies it.
|
|
30
|
+
* Unlike {@link extractJsonObjectSchemaEntries}, an unconstrained schema counts as object-like.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isJsonObjectLikeSchema(schema: JsonSchema): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Flattens `anyOf` and `oneOf` unions when they are the only active constraints.
|
|
35
|
+
*/
|
|
36
|
+
export declare function flattenJsonUnionSchema(schema: JsonSchema): JsonSchema[];
|
|
37
|
+
/**
|
|
38
|
+
* Matches a union made of a single item schema and its array form.
|
|
39
|
+
*/
|
|
40
|
+
export declare function matchArrayableJsonSchema(schema: JsonSchema): undefined | [itemSchema: JsonSchema, arraySchema: JsonArraySchema];
|
|
41
|
+
export declare function deduplicateJsonSchemas(schemas: JsonSchema[]): JsonSchema[];
|