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