fast-json-stringify 7.0.1 → 7.1.1
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/.github/dependabot.yml +1 -0
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lock-threads.yml +1 -1
- package/.github/workflows/release.yml +24 -0
- package/README.md +20 -3
- package/benchmark/bench-cmp-lib.js +63 -38
- package/benchmark/bench-thread.js +8 -2
- package/benchmark/bench.js +86 -0
- package/benchmark/fixtures/api-responses.js +266 -0
- package/index.js +366 -98
- package/lib/location.js +3 -1
- package/lib/serializer.js +41 -37
- package/lib/standalone.js +22 -3
- package/lib/validator.js +22 -3
- package/package.json +4 -5
- package/test/additionalProperties.test.js +40 -0
- package/test/anyof.test.js +42 -0
- package/test/array.test.js +26 -1
- package/test/basic.test.js +1 -1
- package/test/code-generation-fallbacks.test.js +158 -0
- package/test/compile-validators.test.js +110 -0
- package/test/const.test.js +7 -6
- package/test/date.test.js +28 -1
- package/test/debug-mode.test.js +8 -0
- package/test/deopt.test.js +251 -0
- package/test/if-then-else.test.js +145 -0
- package/test/inferType.test.js +1 -1
- package/test/integer.test.js +1 -1
- package/test/oneof.test.js +242 -0
- package/test/optional-property-prefix.test.js +119 -0
- package/test/property-names.test.js +41 -0
- package/test/regex.test.js +1 -1
- package/test/schema-depth.test.js +227 -0
- package/test/standalone-mode.test.js +109 -2
- package/test/string.test.js +8 -0
- package/test/surrogate.test.js +1 -1
- package/test/typesArray.test.js +59 -0
- package/test/utils/validator.js +20 -0
- package/types/index.d.ts +20 -0
- package/types/index.tst.ts +13 -0
package/index.js
CHANGED
|
@@ -10,8 +10,31 @@ const Location = require('./lib/location')
|
|
|
10
10
|
const validate = require('./lib/schema-validator')
|
|
11
11
|
const mergeSchemas = require('./lib/merge-schemas')
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const DEFAULT_MAX_DEPTH = 100
|
|
14
|
+
const NAMED_FRAGMENT_REF = /^#[a-z_][-\w._]*$/i
|
|
15
|
+
|
|
16
|
+
const singleSchemaKeywords = [
|
|
17
|
+
'additionalItems',
|
|
18
|
+
'additionalProperties',
|
|
19
|
+
'contains',
|
|
20
|
+
'else',
|
|
21
|
+
'if',
|
|
22
|
+
'not',
|
|
23
|
+
'propertyNames',
|
|
24
|
+
'then'
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const arraySchemaKeywords = [
|
|
28
|
+
'allOf',
|
|
29
|
+
'anyOf',
|
|
30
|
+
'oneOf'
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const objectSchemaKeywords = [
|
|
34
|
+
'definitions',
|
|
35
|
+
'patternProperties',
|
|
36
|
+
'properties'
|
|
37
|
+
]
|
|
15
38
|
|
|
16
39
|
const serializerFns = `
|
|
17
40
|
const {
|
|
@@ -42,6 +65,113 @@ const validLargeArrayMechanisms = new Set([
|
|
|
42
65
|
|
|
43
66
|
let schemaIdCounter = 0
|
|
44
67
|
|
|
68
|
+
function getMaxDepth (options) {
|
|
69
|
+
const maxDepth = options.maxDepth === undefined ? DEFAULT_MAX_DEPTH : options.maxDepth
|
|
70
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 0 || maxDepth > DEFAULT_MAX_DEPTH) {
|
|
71
|
+
throw new Error(`Unsupported max schema depth ${maxDepth}. Expected an integer between 0 and ${DEFAULT_MAX_DEPTH}.`)
|
|
72
|
+
}
|
|
73
|
+
return maxDepth
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function schemaDepthError (maxDepth, name) {
|
|
77
|
+
const prefix = name ? `"${name}" ` : ''
|
|
78
|
+
return new Error(`${prefix}schema exceeds maximum depth of ${maxDepth}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertObjectDepth (schema, maxDepth, name) {
|
|
82
|
+
const stack = [{ value: schema, depth: 0, exit: false }]
|
|
83
|
+
const path = new WeakSet()
|
|
84
|
+
|
|
85
|
+
while (stack.length > 0) {
|
|
86
|
+
const entry = stack.pop()
|
|
87
|
+
const value = entry.value
|
|
88
|
+
|
|
89
|
+
if (typeof value !== 'object' || value === null) continue
|
|
90
|
+
|
|
91
|
+
// Schema maps and arrays add at most one container between schema nodes.
|
|
92
|
+
// Keep dependency traversals within the same stack-safety envelope while
|
|
93
|
+
// still allowing maxDepth schema nodes nested through `properties`.
|
|
94
|
+
if (entry.depth > maxDepth * 2) {
|
|
95
|
+
throw schemaDepthError(maxDepth, name)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (entry.exit) {
|
|
99
|
+
path.delete(value)
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (path.has(value)) {
|
|
104
|
+
throw new Error('schema contains a circular object reference; use $ref instead')
|
|
105
|
+
}
|
|
106
|
+
path.add(value)
|
|
107
|
+
stack.push({ value, depth: entry.depth, exit: true })
|
|
108
|
+
|
|
109
|
+
for (const key in value) {
|
|
110
|
+
stack.push({ value: value[key], depth: entry.depth + 1, exit: false })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function assertSchemaDepth (schema, maxDepth, name) {
|
|
116
|
+
const stack = [{ schema, depth: 0 }]
|
|
117
|
+
|
|
118
|
+
while (stack.length > 0) {
|
|
119
|
+
const entry = stack.pop()
|
|
120
|
+
const currentSchema = entry.schema
|
|
121
|
+
|
|
122
|
+
if (entry.depth > maxDepth) {
|
|
123
|
+
throw schemaDepthError(maxDepth, name)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (typeof currentSchema !== 'object' || currentSchema === null) continue
|
|
127
|
+
|
|
128
|
+
const nestedDepth = entry.depth + 1
|
|
129
|
+
|
|
130
|
+
for (const keyword of singleSchemaKeywords) {
|
|
131
|
+
if (typeof currentSchema[keyword] === 'object' && currentSchema[keyword] !== null) {
|
|
132
|
+
stack.push({ schema: currentSchema[keyword], depth: nestedDepth })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const keyword of arraySchemaKeywords) {
|
|
137
|
+
const schemas = currentSchema[keyword]
|
|
138
|
+
if (Array.isArray(schemas)) {
|
|
139
|
+
for (const nestedSchema of schemas) {
|
|
140
|
+
stack.push({ schema: nestedSchema, depth: nestedDepth })
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const items = currentSchema.items
|
|
146
|
+
if (Array.isArray(items)) {
|
|
147
|
+
for (const nestedSchema of items) {
|
|
148
|
+
stack.push({ schema: nestedSchema, depth: nestedDepth })
|
|
149
|
+
}
|
|
150
|
+
} else if (typeof items === 'object' && items !== null) {
|
|
151
|
+
stack.push({ schema: items, depth: nestedDepth })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const keyword of objectSchemaKeywords) {
|
|
155
|
+
const schemas = currentSchema[keyword]
|
|
156
|
+
if (typeof schemas === 'object' && schemas !== null) {
|
|
157
|
+
for (const key of Object.keys(schemas)) {
|
|
158
|
+
stack.push({ schema: schemas[key], depth: nestedDepth })
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const dependencies = currentSchema.dependencies
|
|
164
|
+
if (typeof dependencies === 'object' && dependencies !== null) {
|
|
165
|
+
for (const key of Object.keys(dependencies)) {
|
|
166
|
+
const dependency = dependencies[key]
|
|
167
|
+
if (!Array.isArray(dependency)) {
|
|
168
|
+
stack.push({ schema: dependency, depth: nestedDepth })
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
45
175
|
function isValidSchema (schema, name) {
|
|
46
176
|
if (!validate(schema)) {
|
|
47
177
|
if (name) {
|
|
@@ -57,27 +187,64 @@ function isValidSchema (schema, name) {
|
|
|
57
187
|
}
|
|
58
188
|
|
|
59
189
|
function resolveRef (context, location) {
|
|
60
|
-
const
|
|
190
|
+
const seen = new Set()
|
|
191
|
+
let depth = 0
|
|
61
192
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
193
|
+
while (location.schema.$ref !== undefined) {
|
|
194
|
+
const ref = location.schema.$ref
|
|
195
|
+
const locationRef = location.getSchemaRef()
|
|
66
196
|
|
|
67
|
-
|
|
68
|
-
|
|
197
|
+
if (seen.has(locationRef)) {
|
|
198
|
+
throw new Error(`Cannot resolve circular reference "${ref}"`)
|
|
199
|
+
}
|
|
200
|
+
seen.add(locationRef)
|
|
201
|
+
|
|
202
|
+
if (depth++ > context.maxDepth) {
|
|
203
|
+
throw schemaDepthError(context.maxDepth)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let hashIndex = ref.indexOf('#')
|
|
207
|
+
if (hashIndex === -1) {
|
|
208
|
+
hashIndex = ref.length
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const schemaId = ref.slice(0, hashIndex) || location.schemaId
|
|
212
|
+
const jsonPointer = ref.slice(hashIndex) || '#'
|
|
213
|
+
|
|
214
|
+
const schema = context.refResolver.getSchema(schemaId, jsonPointer)
|
|
215
|
+
if (schema === null) {
|
|
216
|
+
throw new Error(`Cannot find reference "${ref}"`)
|
|
217
|
+
}
|
|
69
218
|
|
|
70
|
-
|
|
71
|
-
if (schema === null) {
|
|
72
|
-
throw new Error(`Cannot find reference "${ref}"`)
|
|
219
|
+
location = new Location(schema, schemaId, jsonPointer)
|
|
73
220
|
}
|
|
74
221
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
222
|
+
return location
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getSchemaDependencies (refResolver, schemaId) {
|
|
226
|
+
const dependencies = {}
|
|
227
|
+
const processed = new Set()
|
|
228
|
+
const pending = [schemaId]
|
|
229
|
+
|
|
230
|
+
while (pending.length > 0) {
|
|
231
|
+
const currentSchemaId = pending.pop()
|
|
232
|
+
if (processed.has(currentSchemaId)) continue
|
|
233
|
+
processed.add(currentSchemaId)
|
|
234
|
+
|
|
235
|
+
for (const ref of refResolver.getSchemaRefs(currentSchemaId)) {
|
|
236
|
+
const dependencySchemaId = ref.schemaId
|
|
237
|
+
if (
|
|
238
|
+
dependencySchemaId === currentSchemaId ||
|
|
239
|
+
dependencies[dependencySchemaId] !== undefined
|
|
240
|
+
) continue
|
|
241
|
+
|
|
242
|
+
dependencies[dependencySchemaId] = refResolver.getSchema(dependencySchemaId)
|
|
243
|
+
pending.push(dependencySchemaId)
|
|
244
|
+
}
|
|
78
245
|
}
|
|
79
246
|
|
|
80
|
-
return
|
|
247
|
+
return dependencies
|
|
81
248
|
}
|
|
82
249
|
|
|
83
250
|
function getMergedLocation (context, mergedSchemaId) {
|
|
@@ -92,6 +259,14 @@ function getSchemaId (schema, rootSchemaId) {
|
|
|
92
259
|
return rootSchemaId
|
|
93
260
|
}
|
|
94
261
|
|
|
262
|
+
// a schema ref on a comment line of the generated code: a line terminator in a property name
|
|
263
|
+
// would end the comment and turn the rest of the name into code. JSON.stringify covers \n and
|
|
264
|
+
// \r, the two separators JS also treats as line terminators are escaped by hand
|
|
265
|
+
const LINE_SEPARATORS = /[\u2028\u2029]/g
|
|
266
|
+
function asComment (schemaRef) {
|
|
267
|
+
return JSON.stringify(schemaRef).replace(LINE_SEPARATORS, (c) => '\\u' + c.charCodeAt(0).toString(16))
|
|
268
|
+
}
|
|
269
|
+
|
|
95
270
|
function getSafeSchemaRef (context, location) {
|
|
96
271
|
let schemaRef = location.getSchemaRef() || ''
|
|
97
272
|
if (schemaRef.startsWith(context.rootSchemaId)) {
|
|
@@ -100,11 +275,59 @@ function getSafeSchemaRef (context, location) {
|
|
|
100
275
|
return schemaRef
|
|
101
276
|
}
|
|
102
277
|
|
|
103
|
-
function
|
|
104
|
-
|
|
278
|
+
function getValidatorSchemaRef (context, location) {
|
|
279
|
+
let schemaRef = location.getSchemaRef()
|
|
280
|
+
const schema = location.schema
|
|
105
281
|
|
|
282
|
+
// A schema that is only a registered $ref can validate against that target
|
|
283
|
+
// instead of a JSON-pointer path through the root schema.
|
|
284
|
+
if (
|
|
285
|
+
typeof schema.$ref !== 'string' ||
|
|
286
|
+
Object.keys(schema).length !== 1
|
|
287
|
+
) {
|
|
288
|
+
return schemaRef
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const ref = schema.$ref
|
|
292
|
+
const hashIndex = ref.indexOf('#')
|
|
293
|
+
const refSchemaId = hashIndex === -1 ? ref : ref.slice(0, hashIndex)
|
|
294
|
+
const refJsonPointer = hashIndex === -1 ? '' : ref.slice(hashIndex)
|
|
295
|
+
const isBareRef = refJsonPointer === '' || refJsonPointer === '#'
|
|
296
|
+
const isNamedFragmentRef = NAMED_FRAGMENT_REF.test(refJsonPointer)
|
|
297
|
+
const isJsonPointerRef = refJsonPointer.startsWith('#/')
|
|
298
|
+
|
|
299
|
+
if (
|
|
300
|
+
(isBareRef || isNamedFragmentRef || isJsonPointerRef) &&
|
|
301
|
+
refSchemaId &&
|
|
302
|
+
context.refResolver.hasSchema(refSchemaId)
|
|
303
|
+
) {
|
|
304
|
+
const refSchema = context.refResolver.getSchema(refSchemaId)
|
|
305
|
+
const refSubSchema = isBareRef
|
|
306
|
+
? refSchema
|
|
307
|
+
: context.refResolver.getSchema(refSchemaId, refJsonPointer)
|
|
308
|
+
const hasRelativeSchemaId = (
|
|
309
|
+
typeof refSchema.$id === 'string' &&
|
|
310
|
+
refSchema.$id.charAt(0) === '#'
|
|
311
|
+
)
|
|
312
|
+
if (
|
|
313
|
+
refSubSchema !== null &&
|
|
314
|
+
(!hasRelativeSchemaId || refSchema.$id === refJsonPointer)
|
|
315
|
+
) {
|
|
316
|
+
schemaRef = refSchemaId + (isBareRef ? '#' : refJsonPointer)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return schemaRef
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function build (schema, options) {
|
|
106
324
|
options = options || {}
|
|
107
325
|
|
|
326
|
+
const maxDepth = getMaxDepth(options)
|
|
327
|
+
assertObjectDepth(schema, maxDepth)
|
|
328
|
+
assertSchemaDepth(schema, maxDepth)
|
|
329
|
+
isValidSchema(schema)
|
|
330
|
+
|
|
108
331
|
const context = {
|
|
109
332
|
functions: [],
|
|
110
333
|
functionsCounter: 0,
|
|
@@ -113,10 +336,14 @@ function build (schema, options) {
|
|
|
113
336
|
refResolver: new RefResolver(),
|
|
114
337
|
rootSchemaId: schema.$id || `__fjs_root_${schemaIdCounter++}`,
|
|
115
338
|
validatorSchemasIds: new Set(),
|
|
339
|
+
validatorSchemaRefs: new Set(),
|
|
116
340
|
mergedSchemasIds: new Map(),
|
|
341
|
+
maxDepth,
|
|
117
342
|
recursiveSchemas: new Set(),
|
|
118
343
|
recursivePaths: new Set(),
|
|
119
344
|
buildingSet: new Set(),
|
|
345
|
+
largeArraySize: 2e4,
|
|
346
|
+
largeArrayMechanism: 'default',
|
|
120
347
|
uid: 0
|
|
121
348
|
}
|
|
122
349
|
|
|
@@ -130,6 +357,8 @@ function build (schema, options) {
|
|
|
130
357
|
const schema = options.schema[key]
|
|
131
358
|
const schemaId = getSchemaId(schema, key)
|
|
132
359
|
if (!context.refResolver.hasSchema(schemaId)) {
|
|
360
|
+
assertObjectDepth(schema, maxDepth, key)
|
|
361
|
+
assertSchemaDepth(schema, maxDepth, key)
|
|
133
362
|
isValidSchema(schema, key)
|
|
134
363
|
context.refResolver.addSchema(schema, key)
|
|
135
364
|
}
|
|
@@ -144,7 +373,7 @@ function build (schema, options) {
|
|
|
144
373
|
|
|
145
374
|
if (options.largeArrayMechanism) {
|
|
146
375
|
if (validLargeArrayMechanisms.has(options.largeArrayMechanism)) {
|
|
147
|
-
largeArrayMechanism = options.largeArrayMechanism
|
|
376
|
+
context.largeArrayMechanism = options.largeArrayMechanism
|
|
148
377
|
} else {
|
|
149
378
|
throw new Error(`Unsupported large array mechanism ${options.largeArrayMechanism}`)
|
|
150
379
|
}
|
|
@@ -155,11 +384,11 @@ function build (schema, options) {
|
|
|
155
384
|
let parsedNumber
|
|
156
385
|
|
|
157
386
|
if (largeArraySizeType === 'string' && Number.isFinite((parsedNumber = Number.parseInt(options.largeArraySize, 10)))) {
|
|
158
|
-
largeArraySize = parsedNumber
|
|
387
|
+
context.largeArraySize = parsedNumber
|
|
159
388
|
} else if (largeArraySizeType === 'number' && Number.isInteger(options.largeArraySize)) {
|
|
160
|
-
largeArraySize = options.largeArraySize
|
|
389
|
+
context.largeArraySize = options.largeArraySize
|
|
161
390
|
} else if (largeArraySizeType === 'bigint') {
|
|
162
|
-
largeArraySize = Number(options.largeArraySize)
|
|
391
|
+
context.largeArraySize = Number(options.largeArraySize)
|
|
163
392
|
} else {
|
|
164
393
|
throw new Error(`Unsupported large array size. Expected integer-like, got ${typeof options.largeArraySize} with value ${options.largeArraySize}`)
|
|
165
394
|
}
|
|
@@ -208,18 +437,25 @@ function build (schema, options) {
|
|
|
208
437
|
}
|
|
209
438
|
|
|
210
439
|
const serializer = new Serializer(options)
|
|
211
|
-
const validator = new Validator(
|
|
440
|
+
const validator = new Validator(
|
|
441
|
+
options.ajv,
|
|
442
|
+
options.mode === 'standalone' && options.inlineValidators
|
|
443
|
+
)
|
|
212
444
|
|
|
213
445
|
for (const schemaId of context.validatorSchemasIds) {
|
|
214
446
|
const schema = context.refResolver.getSchema(schemaId)
|
|
215
447
|
validator.addSchema(schema, schemaId)
|
|
216
448
|
|
|
217
|
-
const dependencies = context.refResolver
|
|
449
|
+
const dependencies = getSchemaDependencies(context.refResolver, schemaId)
|
|
218
450
|
for (const [schemaId, schema] of Object.entries(dependencies)) {
|
|
219
451
|
validator.addSchema(schema, schemaId)
|
|
220
452
|
}
|
|
221
453
|
}
|
|
222
454
|
|
|
455
|
+
if (options.compileValidators) {
|
|
456
|
+
validator.compileSchemas(context.validatorSchemaRefs)
|
|
457
|
+
}
|
|
458
|
+
|
|
223
459
|
if (options.mode === 'debug') {
|
|
224
460
|
return {
|
|
225
461
|
validator,
|
|
@@ -329,7 +565,12 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma, objV
|
|
|
329
565
|
const additionalPropertiesLocation = location.getPropertyLocation('additionalProperties')
|
|
330
566
|
const additionalPropertiesSchema = additionalPropertiesLocation.schema
|
|
331
567
|
|
|
332
|
-
|
|
568
|
+
// `additionalProperties: false` means every property that is not declared in
|
|
569
|
+
// `properties` nor matched by `patternProperties` is dropped, so no branch is
|
|
570
|
+
// emitted for it. Without this guard the `false` schema reaches buildValue,
|
|
571
|
+
// which serializes any boolean schema with `JSON.stringify(value)` and lets
|
|
572
|
+
// the property through.
|
|
573
|
+
if (additionalPropertiesSchema !== undefined && additionalPropertiesSchema !== false) {
|
|
333
574
|
if (additionalPropertiesSchema === true) {
|
|
334
575
|
code += `
|
|
335
576
|
${addComma}
|
|
@@ -447,12 +688,17 @@ function buildInnerObject (context, location, objVar) {
|
|
|
447
688
|
const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
|
|
448
689
|
const defaultValue = propertyLocation.schema.default
|
|
449
690
|
const isRequired = requiredProperties.includes(key) // Should be false here but good to keep
|
|
691
|
+
// Select a complete prefix so the comma does not need a separate concatenation.
|
|
692
|
+
const propertyPrefix = sanitizedKey + ':'
|
|
693
|
+
const addProperty = needsRuntimeComma
|
|
694
|
+
? `json += addComma_${localUid} ? ${JSON.stringify(',' + propertyPrefix)} : ${JSON.stringify(propertyPrefix)}
|
|
695
|
+
addComma_${localUid} = true`
|
|
696
|
+
: `json += ${JSON.stringify(propertyPrefix)}`
|
|
450
697
|
|
|
451
698
|
code += `
|
|
452
699
|
const ${value} = ${objVar}[${sanitizedKey}]
|
|
453
700
|
if (${value} !== undefined) {
|
|
454
|
-
${
|
|
455
|
-
json += ${JSON.stringify(sanitizedKey + ':')}
|
|
701
|
+
${addProperty}
|
|
456
702
|
${buildValue(context, propertyLocation, `${value}`)}
|
|
457
703
|
}`
|
|
458
704
|
|
|
@@ -568,7 +814,7 @@ function buildObject (context, location, input) {
|
|
|
568
814
|
const schemaRef = getSafeSchemaRef(context, location)
|
|
569
815
|
|
|
570
816
|
const functionCode = `
|
|
571
|
-
// ${schemaRef}
|
|
817
|
+
// ${asComment(schemaRef)}
|
|
572
818
|
function ${functionName} (input) {
|
|
573
819
|
const obj = ${toJSON('input')}
|
|
574
820
|
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'}
|
|
@@ -628,14 +874,14 @@ function buildArray (context, location, input) {
|
|
|
628
874
|
|
|
629
875
|
let functionCode = `
|
|
630
876
|
function ${functionName} (obj) {
|
|
631
|
-
// ${schemaRef}
|
|
877
|
+
// ${asComment(schemaRef)}
|
|
632
878
|
let json = ''
|
|
633
879
|
`
|
|
634
880
|
|
|
635
881
|
functionCode += `
|
|
636
882
|
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
|
|
637
883
|
if (!Array.isArray(obj)) {
|
|
638
|
-
throw new TypeError(
|
|
884
|
+
throw new TypeError(${JSON.stringify(`The value of '${schemaRef}' does not match schema definition.`)})
|
|
639
885
|
}
|
|
640
886
|
const arrayLength = obj.length
|
|
641
887
|
`
|
|
@@ -648,8 +894,8 @@ function buildArray (context, location, input) {
|
|
|
648
894
|
`
|
|
649
895
|
}
|
|
650
896
|
|
|
651
|
-
if (largeArrayMechanism === 'json-stringify') {
|
|
652
|
-
functionCode += `if (arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
|
|
897
|
+
if (context.largeArrayMechanism === 'json-stringify') {
|
|
898
|
+
functionCode += `if (arrayLength >= ${context.largeArraySize}) return JSON.stringify(obj)\n`
|
|
653
899
|
}
|
|
654
900
|
|
|
655
901
|
functionCode += `
|
|
@@ -720,7 +966,7 @@ function buildArray (context, location, input) {
|
|
|
720
966
|
if (${objVar} === null) {
|
|
721
967
|
json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
|
|
722
968
|
} else if (!Array.isArray(${objVar})) {
|
|
723
|
-
throw new TypeError(
|
|
969
|
+
throw new TypeError(${JSON.stringify(`The value of '${safeSchemaRef}' does not match schema definition.`)})
|
|
724
970
|
} else {
|
|
725
971
|
const arrayLength_${objVar} = ${objVar}.length
|
|
726
972
|
`
|
|
@@ -733,8 +979,8 @@ function buildArray (context, location, input) {
|
|
|
733
979
|
`
|
|
734
980
|
}
|
|
735
981
|
|
|
736
|
-
if (largeArrayMechanism === 'json-stringify') {
|
|
737
|
-
inlinedCode += `if (arrayLength_${objVar} >= ${largeArraySize}) json += JSON.stringify(${objVar})\n else {`
|
|
982
|
+
if (context.largeArrayMechanism === 'json-stringify') {
|
|
983
|
+
inlinedCode += `if (arrayLength_${objVar} >= ${context.largeArraySize}) json += JSON.stringify(${objVar})\n else {`
|
|
738
984
|
}
|
|
739
985
|
|
|
740
986
|
inlinedCode += `
|
|
@@ -791,7 +1037,7 @@ function buildArray (context, location, input) {
|
|
|
791
1037
|
json += JSON_STR_END_ARRAY
|
|
792
1038
|
`
|
|
793
1039
|
|
|
794
|
-
if (largeArrayMechanism === 'json-stringify') {
|
|
1040
|
+
if (context.largeArrayMechanism === 'json-stringify') {
|
|
795
1041
|
inlinedCode += '}'
|
|
796
1042
|
}
|
|
797
1043
|
|
|
@@ -900,6 +1146,17 @@ function buildMultiTypeSerializer (context, location, input) {
|
|
|
900
1146
|
`
|
|
901
1147
|
break
|
|
902
1148
|
}
|
|
1149
|
+
case 'object': {
|
|
1150
|
+
// An array is `typeof === 'object'`, so it would otherwise be captured
|
|
1151
|
+
// by this branch and serialized as an object (dropping its items). Exclude
|
|
1152
|
+
// arrays here so a sibling `array` type in the same `type` list can match.
|
|
1153
|
+
code += `
|
|
1154
|
+
${statement}((typeof ${input} === "object" && !Array.isArray(${input})) || ${input} === null) {
|
|
1155
|
+
${nestedResult}
|
|
1156
|
+
}
|
|
1157
|
+
`
|
|
1158
|
+
break
|
|
1159
|
+
}
|
|
903
1160
|
default: {
|
|
904
1161
|
code += `
|
|
905
1162
|
${statement}(typeof ${input} === "${type}" || ${input} === null) {
|
|
@@ -911,7 +1168,7 @@ function buildMultiTypeSerializer (context, location, input) {
|
|
|
911
1168
|
}
|
|
912
1169
|
})
|
|
913
1170
|
code += `
|
|
914
|
-
else throw new TypeError(
|
|
1171
|
+
else throw new TypeError(${JSON.stringify(`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.`)})
|
|
915
1172
|
`
|
|
916
1173
|
|
|
917
1174
|
return code
|
|
@@ -971,9 +1228,13 @@ function buildSingleTypeSerializer (context, location, input) {
|
|
|
971
1228
|
|
|
972
1229
|
function detectRecursiveSchemas (context, location) {
|
|
973
1230
|
const pathStack = new Set()
|
|
974
|
-
function traverse (location) {
|
|
1231
|
+
function traverse (location, depth) {
|
|
975
1232
|
const schema = location.schema
|
|
976
|
-
if (typeof schema !== 'object'
|
|
1233
|
+
if (!schema || typeof schema !== 'object') return
|
|
1234
|
+
|
|
1235
|
+
if (depth > context.maxDepth) {
|
|
1236
|
+
throw schemaDepthError(context.maxDepth)
|
|
1237
|
+
}
|
|
977
1238
|
|
|
978
1239
|
const schemaId = location.schemaId || ''
|
|
979
1240
|
const jsonPointer = location.jsonPointer || ''
|
|
@@ -994,65 +1255,66 @@ function detectRecursiveSchemas (context, location) {
|
|
|
994
1255
|
if (schema.$ref) {
|
|
995
1256
|
try {
|
|
996
1257
|
const res = resolveRef(context, location)
|
|
997
|
-
traverse(res)
|
|
1258
|
+
traverse(res, depth)
|
|
998
1259
|
} catch (err) {
|
|
1260
|
+
if (!err.message.startsWith('Cannot find reference')) throw err
|
|
999
1261
|
// Validation will handle missing refs later
|
|
1000
1262
|
}
|
|
1001
1263
|
}
|
|
1002
1264
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
}
|
|
1009
|
-
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
1010
|
-
traverse(location.getPropertyLocation('additionalProperties'))
|
|
1011
|
-
}
|
|
1012
|
-
if (schema.patternProperties) {
|
|
1013
|
-
const patternPropertiesLocation = location.getPropertyLocation('patternProperties')
|
|
1014
|
-
for (const key in schema.patternProperties) {
|
|
1015
|
-
traverse(patternPropertiesLocation.getPropertyLocation(key))
|
|
1265
|
+
const nestedDepth = depth + 1
|
|
1266
|
+
|
|
1267
|
+
for (const keyword of singleSchemaKeywords) {
|
|
1268
|
+
if (typeof schema[keyword] === 'object' && schema[keyword] !== null) {
|
|
1269
|
+
traverse(location.getPropertyLocation(keyword), nestedDepth)
|
|
1016
1270
|
}
|
|
1017
1271
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1272
|
+
|
|
1273
|
+
for (const keyword of arraySchemaKeywords) {
|
|
1274
|
+
const schemas = schema[keyword]
|
|
1275
|
+
if (Array.isArray(schemas)) {
|
|
1276
|
+
const schemasLocation = location.getPropertyLocation(keyword)
|
|
1277
|
+
const schemasLength = schemas.length
|
|
1278
|
+
for (let i = 0; i < schemasLength; i++) {
|
|
1279
|
+
traverse(schemasLocation.getPropertyLocation(i), nestedDepth)
|
|
1023
1280
|
}
|
|
1024
|
-
} else {
|
|
1025
|
-
traverse(itemsLocation)
|
|
1026
1281
|
}
|
|
1027
1282
|
}
|
|
1028
|
-
if (schema.additionalItems && typeof schema.additionalItems === 'object') {
|
|
1029
|
-
traverse(location.getPropertyLocation('additionalItems'))
|
|
1030
|
-
}
|
|
1031
1283
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1284
|
+
const items = schema.items
|
|
1285
|
+
if (Array.isArray(items)) {
|
|
1286
|
+
const itemsLocation = location.getPropertyLocation('items')
|
|
1287
|
+
const itemsLength = items.length
|
|
1288
|
+
for (let i = 0; i < itemsLength; i++) {
|
|
1289
|
+
traverse(itemsLocation.getPropertyLocation(i), nestedDepth)
|
|
1036
1290
|
}
|
|
1291
|
+
} else if (typeof items === 'object' && items !== null) {
|
|
1292
|
+
traverse(location.getPropertyLocation('items'), nestedDepth)
|
|
1037
1293
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1294
|
+
|
|
1295
|
+
for (const keyword of objectSchemaKeywords) {
|
|
1296
|
+
const schemas = schema[keyword]
|
|
1297
|
+
if (typeof schemas === 'object' && schemas !== null) {
|
|
1298
|
+
const schemasLocation = location.getPropertyLocation(keyword)
|
|
1299
|
+
for (const key in schemas) {
|
|
1300
|
+
traverse(schemasLocation.getPropertyLocation(key), nestedDepth)
|
|
1301
|
+
}
|
|
1042
1302
|
}
|
|
1043
1303
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1304
|
+
|
|
1305
|
+
const dependencies = schema.dependencies
|
|
1306
|
+
if (typeof dependencies === 'object' && dependencies !== null) {
|
|
1307
|
+
const dependenciesLocation = location.getPropertyLocation('dependencies')
|
|
1308
|
+
for (const key in dependencies) {
|
|
1309
|
+
if (!Array.isArray(dependencies[key])) {
|
|
1310
|
+
traverse(dependenciesLocation.getPropertyLocation(key), nestedDepth)
|
|
1311
|
+
}
|
|
1048
1312
|
}
|
|
1049
1313
|
}
|
|
1050
|
-
if (schema.then) traverse(location.getPropertyLocation('then'))
|
|
1051
|
-
if (schema.else) traverse(location.getPropertyLocation('else'))
|
|
1052
1314
|
|
|
1053
1315
|
pathStack.delete(fullPath)
|
|
1054
1316
|
}
|
|
1055
|
-
traverse(location)
|
|
1317
|
+
traverse(location, 0)
|
|
1056
1318
|
}
|
|
1057
1319
|
|
|
1058
1320
|
function buildConstSerializer (location, input) {
|
|
@@ -1148,17 +1410,18 @@ function buildOneOf (context, location, input) {
|
|
|
1148
1410
|
}
|
|
1149
1411
|
|
|
1150
1412
|
const nestedResult = buildValue(context, mergedLocation, input)
|
|
1151
|
-
const schemaRef = optionLocation
|
|
1413
|
+
const schemaRef = getValidatorSchemaRef(context, optionLocation)
|
|
1414
|
+
context.validatorSchemaRefs.add(schemaRef)
|
|
1152
1415
|
|
|
1153
1416
|
code += `
|
|
1154
|
-
${index === 0 ? 'if' : 'else if'}(validator.validate(
|
|
1417
|
+
${index === 0 ? 'if' : 'else if'}(validator.validate(${JSON.stringify(schemaRef)}, ${input})) {
|
|
1155
1418
|
${nestedResult}
|
|
1156
1419
|
}
|
|
1157
1420
|
`
|
|
1158
1421
|
}
|
|
1159
1422
|
|
|
1160
1423
|
code += `
|
|
1161
|
-
else throw new TypeError(
|
|
1424
|
+
else throw new TypeError(${JSON.stringify(`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.`)})
|
|
1162
1425
|
`
|
|
1163
1426
|
|
|
1164
1427
|
return code
|
|
@@ -1181,26 +1444,31 @@ function buildIfThenElse (context, location, input) {
|
|
|
1181
1444
|
)
|
|
1182
1445
|
|
|
1183
1446
|
const ifLocation = location.getPropertyLocation('if')
|
|
1184
|
-
const ifSchemaRef = ifLocation
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
thenMergedSchemaId =
|
|
1193
|
-
|
|
1447
|
+
const ifSchemaRef = getValidatorSchemaRef(context, ifLocation)
|
|
1448
|
+
context.validatorSchemaRefs.add(ifSchemaRef)
|
|
1449
|
+
|
|
1450
|
+
// `then` is optional: a schema may pair `if` with `else` alone. In that case
|
|
1451
|
+
// the true branch adds no keywords, so it serializes with the root schema.
|
|
1452
|
+
let thenMergedLocation = rootLocation
|
|
1453
|
+
if (thenSchema !== undefined) {
|
|
1454
|
+
const thenLocation = location.getPropertyLocation('then')
|
|
1455
|
+
let thenMergedSchemaId = context.mergedSchemasIds.get(thenSchema)
|
|
1456
|
+
if (thenMergedSchemaId) {
|
|
1457
|
+
thenMergedLocation = getMergedLocation(context, thenMergedSchemaId)
|
|
1458
|
+
} else {
|
|
1459
|
+
thenMergedSchemaId = `__fjs_merged_${schemaIdCounter++}`
|
|
1460
|
+
context.mergedSchemasIds.set(thenSchema, thenMergedSchemaId)
|
|
1194
1461
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1462
|
+
thenMergedLocation = mergeLocations(context, thenMergedSchemaId, [
|
|
1463
|
+
rootLocation,
|
|
1464
|
+
thenLocation
|
|
1465
|
+
])
|
|
1466
|
+
}
|
|
1199
1467
|
}
|
|
1200
1468
|
|
|
1201
1469
|
if (!elseSchema) {
|
|
1202
1470
|
return `
|
|
1203
|
-
if (validator.validate(
|
|
1471
|
+
if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
|
|
1204
1472
|
${buildValue(context, thenMergedLocation, input)}
|
|
1205
1473
|
} else {
|
|
1206
1474
|
${buildValue(context, rootLocation, input)}
|
|
@@ -1224,7 +1492,7 @@ function buildIfThenElse (context, location, input) {
|
|
|
1224
1492
|
}
|
|
1225
1493
|
|
|
1226
1494
|
return `
|
|
1227
|
-
if (validator.validate(
|
|
1495
|
+
if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
|
|
1228
1496
|
${buildValue(context, thenMergedLocation, input)}
|
|
1229
1497
|
} else {
|
|
1230
1498
|
${buildValue(context, elseMergedLocation, input)}
|
|
@@ -1252,7 +1520,7 @@ function buildValue (context, location, input) {
|
|
|
1252
1520
|
return buildOneOf(context, location, input)
|
|
1253
1521
|
}
|
|
1254
1522
|
|
|
1255
|
-
if (schema.if && schema.then) {
|
|
1523
|
+
if (schema.if && (schema.then || schema.else)) {
|
|
1256
1524
|
return buildIfThenElse(context, location, input)
|
|
1257
1525
|
}
|
|
1258
1526
|
|