fast-json-stringify 7.0.0 → 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 +43 -2
- 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 +25 -8
- package/benchmark/bench-thread.js +6 -2
- package/benchmark/bench.js +86 -0
- package/index.js +360 -94
- 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 +6 -7
- package/test/additionalProperties.test.js +87 -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 +27 -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/patternProperties.test.js +24 -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,11 +10,35 @@ const Location = require('./lib/location')
|
|
|
10
10
|
const validate = require('./lib/schema-validator')
|
|
11
11
|
const mergeSchemas = require('./lib/merge-schemas')
|
|
12
12
|
|
|
13
|
-
const SINGLE_TICK = /'/g
|
|
14
|
-
|
|
15
13
|
let largeArraySize = 2e4
|
|
16
14
|
let largeArrayMechanism = 'default'
|
|
17
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
|
+
|
|
18
42
|
const serializerFns = `
|
|
19
43
|
const {
|
|
20
44
|
asString,
|
|
@@ -44,6 +68,113 @@ const validLargeArrayMechanisms = new Set([
|
|
|
44
68
|
|
|
45
69
|
let schemaIdCounter = 0
|
|
46
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
|
+
|
|
47
178
|
function isValidSchema (schema, name) {
|
|
48
179
|
if (!validate(schema)) {
|
|
49
180
|
if (name) {
|
|
@@ -59,27 +190,64 @@ function isValidSchema (schema, name) {
|
|
|
59
190
|
}
|
|
60
191
|
|
|
61
192
|
function resolveRef (context, location) {
|
|
62
|
-
const
|
|
193
|
+
const seen = new Set()
|
|
194
|
+
let depth = 0
|
|
63
195
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
196
|
+
while (location.schema.$ref !== undefined) {
|
|
197
|
+
const ref = location.schema.$ref
|
|
198
|
+
const locationRef = location.getSchemaRef()
|
|
68
199
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
}
|
|
71
221
|
|
|
72
|
-
|
|
73
|
-
if (schema === null) {
|
|
74
|
-
throw new Error(`Cannot find reference "${ref}"`)
|
|
222
|
+
location = new Location(schema, schemaId, jsonPointer)
|
|
75
223
|
}
|
|
76
224
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
248
|
}
|
|
81
249
|
|
|
82
|
-
return
|
|
250
|
+
return dependencies
|
|
83
251
|
}
|
|
84
252
|
|
|
85
253
|
function getMergedLocation (context, mergedSchemaId) {
|
|
@@ -94,6 +262,14 @@ function getSchemaId (schema, rootSchemaId) {
|
|
|
94
262
|
return rootSchemaId
|
|
95
263
|
}
|
|
96
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
|
+
|
|
97
273
|
function getSafeSchemaRef (context, location) {
|
|
98
274
|
let schemaRef = location.getSchemaRef() || ''
|
|
99
275
|
if (schemaRef.startsWith(context.rootSchemaId)) {
|
|
@@ -102,11 +278,59 @@ function getSafeSchemaRef (context, location) {
|
|
|
102
278
|
return schemaRef
|
|
103
279
|
}
|
|
104
280
|
|
|
105
|
-
function
|
|
106
|
-
|
|
281
|
+
function getValidatorSchemaRef (context, location) {
|
|
282
|
+
let schemaRef = location.getSchemaRef()
|
|
283
|
+
const schema = location.schema
|
|
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
|
+
}
|
|
107
322
|
|
|
323
|
+
return schemaRef
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function build (schema, options) {
|
|
108
327
|
options = options || {}
|
|
109
328
|
|
|
329
|
+
const maxDepth = getMaxDepth(options)
|
|
330
|
+
assertObjectDepth(schema, maxDepth)
|
|
331
|
+
assertSchemaDepth(schema, maxDepth)
|
|
332
|
+
isValidSchema(schema)
|
|
333
|
+
|
|
110
334
|
const context = {
|
|
111
335
|
functions: [],
|
|
112
336
|
functionsCounter: 0,
|
|
@@ -115,7 +339,9 @@ function build (schema, options) {
|
|
|
115
339
|
refResolver: new RefResolver(),
|
|
116
340
|
rootSchemaId: schema.$id || `__fjs_root_${schemaIdCounter++}`,
|
|
117
341
|
validatorSchemasIds: new Set(),
|
|
342
|
+
validatorSchemaRefs: new Set(),
|
|
118
343
|
mergedSchemasIds: new Map(),
|
|
344
|
+
maxDepth,
|
|
119
345
|
recursiveSchemas: new Set(),
|
|
120
346
|
recursivePaths: new Set(),
|
|
121
347
|
buildingSet: new Set(),
|
|
@@ -132,6 +358,8 @@ function build (schema, options) {
|
|
|
132
358
|
const schema = options.schema[key]
|
|
133
359
|
const schemaId = getSchemaId(schema, key)
|
|
134
360
|
if (!context.refResolver.hasSchema(schemaId)) {
|
|
361
|
+
assertObjectDepth(schema, maxDepth, key)
|
|
362
|
+
assertSchemaDepth(schema, maxDepth, key)
|
|
135
363
|
isValidSchema(schema, key)
|
|
136
364
|
context.refResolver.addSchema(schema, key)
|
|
137
365
|
}
|
|
@@ -210,18 +438,25 @@ function build (schema, options) {
|
|
|
210
438
|
}
|
|
211
439
|
|
|
212
440
|
const serializer = new Serializer(options)
|
|
213
|
-
const validator = new Validator(
|
|
441
|
+
const validator = new Validator(
|
|
442
|
+
options.ajv,
|
|
443
|
+
options.mode === 'standalone' && options.inlineValidators
|
|
444
|
+
)
|
|
214
445
|
|
|
215
446
|
for (const schemaId of context.validatorSchemasIds) {
|
|
216
447
|
const schema = context.refResolver.getSchema(schemaId)
|
|
217
448
|
validator.addSchema(schema, schemaId)
|
|
218
449
|
|
|
219
|
-
const dependencies = context.refResolver
|
|
450
|
+
const dependencies = getSchemaDependencies(context.refResolver, schemaId)
|
|
220
451
|
for (const [schemaId, schema] of Object.entries(dependencies)) {
|
|
221
452
|
validator.addSchema(schema, schemaId)
|
|
222
453
|
}
|
|
223
454
|
}
|
|
224
455
|
|
|
456
|
+
if (options.compileValidators) {
|
|
457
|
+
validator.compileSchemas(context.validatorSchemaRefs)
|
|
458
|
+
}
|
|
459
|
+
|
|
225
460
|
if (options.mode === 'debug') {
|
|
226
461
|
return {
|
|
227
462
|
validator,
|
|
@@ -331,7 +566,12 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma, objV
|
|
|
331
566
|
const additionalPropertiesLocation = location.getPropertyLocation('additionalProperties')
|
|
332
567
|
const additionalPropertiesSchema = additionalPropertiesLocation.schema
|
|
333
568
|
|
|
334
|
-
|
|
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) {
|
|
335
575
|
if (additionalPropertiesSchema === true) {
|
|
336
576
|
code += `
|
|
337
577
|
${addComma}
|
|
@@ -382,10 +622,9 @@ function buildInnerObject (context, location, objVar) {
|
|
|
382
622
|
const localUid = context.uid++
|
|
383
623
|
let addComma = ''
|
|
384
624
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
625
|
+
// propertiesKeys is sorted required-first; the guard checks [0] is required because
|
|
626
|
+
// otherwise additionalProperties/patternProperties would emit a stray `{ ,"k":v }`.
|
|
627
|
+
if (propertiesKeys.length > 0 && requiredProperties.includes(propertiesKeys[0])) {
|
|
389
628
|
// The first property is required, so we don't need a comma.
|
|
390
629
|
// For the subsequent properties, we can blindly add a comma.
|
|
391
630
|
|
|
@@ -450,12 +689,17 @@ function buildInnerObject (context, location, objVar) {
|
|
|
450
689
|
const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
|
|
451
690
|
const defaultValue = propertyLocation.schema.default
|
|
452
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)}`
|
|
453
698
|
|
|
454
699
|
code += `
|
|
455
700
|
const ${value} = ${objVar}[${sanitizedKey}]
|
|
456
701
|
if (${value} !== undefined) {
|
|
457
|
-
${
|
|
458
|
-
json += ${JSON.stringify(sanitizedKey + ':')}
|
|
702
|
+
${addProperty}
|
|
459
703
|
${buildValue(context, propertyLocation, `${value}`)}
|
|
460
704
|
}`
|
|
461
705
|
|
|
@@ -571,7 +815,7 @@ function buildObject (context, location, input) {
|
|
|
571
815
|
const schemaRef = getSafeSchemaRef(context, location)
|
|
572
816
|
|
|
573
817
|
const functionCode = `
|
|
574
|
-
// ${schemaRef}
|
|
818
|
+
// ${asComment(schemaRef)}
|
|
575
819
|
function ${functionName} (input) {
|
|
576
820
|
const obj = ${toJSON('input')}
|
|
577
821
|
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'}
|
|
@@ -631,14 +875,14 @@ function buildArray (context, location, input) {
|
|
|
631
875
|
|
|
632
876
|
let functionCode = `
|
|
633
877
|
function ${functionName} (obj) {
|
|
634
|
-
// ${schemaRef}
|
|
878
|
+
// ${asComment(schemaRef)}
|
|
635
879
|
let json = ''
|
|
636
880
|
`
|
|
637
881
|
|
|
638
882
|
functionCode += `
|
|
639
883
|
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
|
|
640
884
|
if (!Array.isArray(obj)) {
|
|
641
|
-
throw new TypeError(
|
|
885
|
+
throw new TypeError(${JSON.stringify(`The value of '${schemaRef}' does not match schema definition.`)})
|
|
642
886
|
}
|
|
643
887
|
const arrayLength = obj.length
|
|
644
888
|
`
|
|
@@ -723,7 +967,7 @@ function buildArray (context, location, input) {
|
|
|
723
967
|
if (${objVar} === null) {
|
|
724
968
|
json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
|
|
725
969
|
} else if (!Array.isArray(${objVar})) {
|
|
726
|
-
throw new TypeError(
|
|
970
|
+
throw new TypeError(${JSON.stringify(`The value of '${safeSchemaRef}' does not match schema definition.`)})
|
|
727
971
|
} else {
|
|
728
972
|
const arrayLength_${objVar} = ${objVar}.length
|
|
729
973
|
`
|
|
@@ -903,6 +1147,17 @@ function buildMultiTypeSerializer (context, location, input) {
|
|
|
903
1147
|
`
|
|
904
1148
|
break
|
|
905
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
|
+
}
|
|
906
1161
|
default: {
|
|
907
1162
|
code += `
|
|
908
1163
|
${statement}(typeof ${input} === "${type}" || ${input} === null) {
|
|
@@ -914,7 +1169,7 @@ function buildMultiTypeSerializer (context, location, input) {
|
|
|
914
1169
|
}
|
|
915
1170
|
})
|
|
916
1171
|
code += `
|
|
917
|
-
else throw new TypeError(
|
|
1172
|
+
else throw new TypeError(${JSON.stringify(`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.`)})
|
|
918
1173
|
`
|
|
919
1174
|
|
|
920
1175
|
return code
|
|
@@ -974,9 +1229,13 @@ function buildSingleTypeSerializer (context, location, input) {
|
|
|
974
1229
|
|
|
975
1230
|
function detectRecursiveSchemas (context, location) {
|
|
976
1231
|
const pathStack = new Set()
|
|
977
|
-
function traverse (location) {
|
|
1232
|
+
function traverse (location, depth) {
|
|
978
1233
|
const schema = location.schema
|
|
979
|
-
if (typeof schema !== 'object'
|
|
1234
|
+
if (!schema || typeof schema !== 'object') return
|
|
1235
|
+
|
|
1236
|
+
if (depth > context.maxDepth) {
|
|
1237
|
+
throw schemaDepthError(context.maxDepth)
|
|
1238
|
+
}
|
|
980
1239
|
|
|
981
1240
|
const schemaId = location.schemaId || ''
|
|
982
1241
|
const jsonPointer = location.jsonPointer || ''
|
|
@@ -997,65 +1256,66 @@ function detectRecursiveSchemas (context, location) {
|
|
|
997
1256
|
if (schema.$ref) {
|
|
998
1257
|
try {
|
|
999
1258
|
const res = resolveRef(context, location)
|
|
1000
|
-
traverse(res)
|
|
1259
|
+
traverse(res, depth)
|
|
1001
1260
|
} catch (err) {
|
|
1261
|
+
if (!err.message.startsWith('Cannot find reference')) throw err
|
|
1002
1262
|
// Validation will handle missing refs later
|
|
1003
1263
|
}
|
|
1004
1264
|
}
|
|
1005
1265
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
1013
|
-
traverse(location.getPropertyLocation('additionalProperties'))
|
|
1014
|
-
}
|
|
1015
|
-
if (schema.patternProperties) {
|
|
1016
|
-
const patternPropertiesLocation = location.getPropertyLocation('patternProperties')
|
|
1017
|
-
for (const key in schema.patternProperties) {
|
|
1018
|
-
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)
|
|
1019
1271
|
}
|
|
1020
1272
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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)
|
|
1026
1281
|
}
|
|
1027
|
-
} else {
|
|
1028
|
-
traverse(itemsLocation)
|
|
1029
1282
|
}
|
|
1030
1283
|
}
|
|
1031
|
-
if (schema.additionalItems && typeof schema.additionalItems === 'object') {
|
|
1032
|
-
traverse(location.getPropertyLocation('additionalItems'))
|
|
1033
|
-
}
|
|
1034
1284
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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)
|
|
1039
1291
|
}
|
|
1292
|
+
} else if (typeof items === 'object' && items !== null) {
|
|
1293
|
+
traverse(location.getPropertyLocation('items'), nestedDepth)
|
|
1040
1294
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
+
}
|
|
1045
1303
|
}
|
|
1046
1304
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
+
}
|
|
1051
1313
|
}
|
|
1052
1314
|
}
|
|
1053
|
-
if (schema.then) traverse(location.getPropertyLocation('then'))
|
|
1054
|
-
if (schema.else) traverse(location.getPropertyLocation('else'))
|
|
1055
1315
|
|
|
1056
1316
|
pathStack.delete(fullPath)
|
|
1057
1317
|
}
|
|
1058
|
-
traverse(location)
|
|
1318
|
+
traverse(location, 0)
|
|
1059
1319
|
}
|
|
1060
1320
|
|
|
1061
1321
|
function buildConstSerializer (location, input) {
|
|
@@ -1074,7 +1334,7 @@ function buildConstSerializer (location, input) {
|
|
|
1074
1334
|
`
|
|
1075
1335
|
}
|
|
1076
1336
|
|
|
1077
|
-
code += `json +=
|
|
1337
|
+
code += `json += ${JSON.stringify(JSON.stringify(schema.const))}`
|
|
1078
1338
|
|
|
1079
1339
|
if (hasNullType) {
|
|
1080
1340
|
code += `
|
|
@@ -1151,17 +1411,18 @@ function buildOneOf (context, location, input) {
|
|
|
1151
1411
|
}
|
|
1152
1412
|
|
|
1153
1413
|
const nestedResult = buildValue(context, mergedLocation, input)
|
|
1154
|
-
const schemaRef = optionLocation
|
|
1414
|
+
const schemaRef = getValidatorSchemaRef(context, optionLocation)
|
|
1415
|
+
context.validatorSchemaRefs.add(schemaRef)
|
|
1155
1416
|
|
|
1156
1417
|
code += `
|
|
1157
|
-
${index === 0 ? 'if' : 'else if'}(validator.validate(
|
|
1418
|
+
${index === 0 ? 'if' : 'else if'}(validator.validate(${JSON.stringify(schemaRef)}, ${input})) {
|
|
1158
1419
|
${nestedResult}
|
|
1159
1420
|
}
|
|
1160
1421
|
`
|
|
1161
1422
|
}
|
|
1162
1423
|
|
|
1163
1424
|
code += `
|
|
1164
|
-
else throw new TypeError(
|
|
1425
|
+
else throw new TypeError(${JSON.stringify(`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.`)})
|
|
1165
1426
|
`
|
|
1166
1427
|
|
|
1167
1428
|
return code
|
|
@@ -1184,26 +1445,31 @@ function buildIfThenElse (context, location, input) {
|
|
|
1184
1445
|
)
|
|
1185
1446
|
|
|
1186
1447
|
const ifLocation = location.getPropertyLocation('if')
|
|
1187
|
-
const ifSchemaRef = ifLocation
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
thenMergedSchemaId =
|
|
1196
|
-
|
|
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)
|
|
1197
1462
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1463
|
+
thenMergedLocation = mergeLocations(context, thenMergedSchemaId, [
|
|
1464
|
+
rootLocation,
|
|
1465
|
+
thenLocation
|
|
1466
|
+
])
|
|
1467
|
+
}
|
|
1202
1468
|
}
|
|
1203
1469
|
|
|
1204
1470
|
if (!elseSchema) {
|
|
1205
1471
|
return `
|
|
1206
|
-
if (validator.validate(
|
|
1472
|
+
if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
|
|
1207
1473
|
${buildValue(context, thenMergedLocation, input)}
|
|
1208
1474
|
} else {
|
|
1209
1475
|
${buildValue(context, rootLocation, input)}
|
|
@@ -1227,7 +1493,7 @@ function buildIfThenElse (context, location, input) {
|
|
|
1227
1493
|
}
|
|
1228
1494
|
|
|
1229
1495
|
return `
|
|
1230
|
-
if (validator.validate(
|
|
1496
|
+
if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
|
|
1231
1497
|
${buildValue(context, thenMergedLocation, input)}
|
|
1232
1498
|
} else {
|
|
1233
1499
|
${buildValue(context, elseMergedLocation, input)}
|
|
@@ -1255,7 +1521,7 @@ function buildValue (context, location, input) {
|
|
|
1255
1521
|
return buildOneOf(context, location, input)
|
|
1256
1522
|
}
|
|
1257
1523
|
|
|
1258
|
-
if (schema.if && schema.then) {
|
|
1524
|
+
if (schema.if && (schema.then || schema.else)) {
|
|
1259
1525
|
return buildIfThenElse(context, location, input)
|
|
1260
1526
|
}
|
|
1261
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
|
}
|