fast-json-stringify 5.1.0 → 5.2.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/workflows/benchmark.yml +12 -12
- package/.github/workflows/ci.yml +2 -0
- package/benchmark/bench.js +54 -0
- package/index.js +74 -122
- package/package.json +4 -3
- package/ref-resolver.js +84 -0
- package/serializer.js +9 -0
- package/standalone.js +8 -8
- package/test/const.test.js +235 -8
- package/test/debug-mode.test.js +10 -4
- package/test/nullable.test.js +97 -0
- package/test/ref.test.js +28 -0
- package/test/typesArray.test.js +29 -0
- package/validator.js +75 -0
- package/ajv.js +0 -30
|
@@ -11,15 +11,15 @@ jobs:
|
|
|
11
11
|
permissions:
|
|
12
12
|
contents: read
|
|
13
13
|
outputs:
|
|
14
|
-
PR-BENCH-12: ${{ steps.benchmark-pr.outputs.BENCH_RESULT12 }}
|
|
15
14
|
PR-BENCH-14: ${{ steps.benchmark-pr.outputs.BENCH_RESULT14 }}
|
|
16
15
|
PR-BENCH-16: ${{ steps.benchmark-pr.outputs.BENCH_RESULT16 }}
|
|
17
|
-
|
|
16
|
+
PR-BENCH-18: ${{ steps.benchmark-pr.outputs.BENCH_RESULT18 }}
|
|
18
17
|
MAIN-BENCH-14: ${{ steps.benchmark-main.outputs.BENCH_RESULT14 }}
|
|
19
18
|
MAIN-BENCH-16: ${{ steps.benchmark-main.outputs.BENCH_RESULT16 }}
|
|
19
|
+
MAIN-BENCH-18: ${{ steps.benchmark-main.outputs.BENCH_RESULT18 }}
|
|
20
20
|
strategy:
|
|
21
21
|
matrix:
|
|
22
|
-
node-version: [
|
|
22
|
+
node-version: [14, 16, 18]
|
|
23
23
|
steps:
|
|
24
24
|
- uses: actions/checkout@v3
|
|
25
25
|
with:
|
|
@@ -75,38 +75,38 @@ jobs:
|
|
|
75
75
|
with:
|
|
76
76
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
77
77
|
message: |
|
|
78
|
-
**Node**:
|
|
78
|
+
**Node**: 14
|
|
79
79
|
**PR**:
|
|
80
80
|
```
|
|
81
|
-
${{ needs.benchmark.outputs.PR-BENCH-
|
|
81
|
+
${{ needs.benchmark.outputs.PR-BENCH-14 }}
|
|
82
82
|
```
|
|
83
83
|
**MAIN**:
|
|
84
84
|
```
|
|
85
|
-
${{ needs.benchmark.outputs.MAIN-BENCH-
|
|
85
|
+
${{ needs.benchmark.outputs.MAIN-BENCH-14 }}
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
---
|
|
89
89
|
|
|
90
|
-
**Node**:
|
|
90
|
+
**Node**: 16
|
|
91
91
|
**PR**:
|
|
92
92
|
```
|
|
93
|
-
${{ needs.benchmark.outputs.PR-BENCH-
|
|
93
|
+
${{ needs.benchmark.outputs.PR-BENCH-16 }}
|
|
94
94
|
```
|
|
95
95
|
**MAIN**:
|
|
96
96
|
```
|
|
97
|
-
${{ needs.benchmark.outputs.MAIN-BENCH-
|
|
97
|
+
${{ needs.benchmark.outputs.MAIN-BENCH-16 }}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
102
|
-
**Node**:
|
|
102
|
+
**Node**: 18
|
|
103
103
|
**PR**:
|
|
104
104
|
```
|
|
105
|
-
${{ needs.benchmark.outputs.PR-BENCH-
|
|
105
|
+
${{ needs.benchmark.outputs.PR-BENCH-18 }}
|
|
106
106
|
```
|
|
107
107
|
**MAIN**:
|
|
108
108
|
```
|
|
109
|
-
${{ needs.benchmark.outputs.MAIN-BENCH-
|
|
109
|
+
${{ needs.benchmark.outputs.MAIN-BENCH-18 }}
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
- uses: actions-ecosystem/action-remove-labels@v1
|
package/.github/workflows/ci.yml
CHANGED
package/benchmark/bench.js
CHANGED
|
@@ -213,6 +213,60 @@ const benchmarks = [
|
|
|
213
213
|
n5: 42,
|
|
214
214
|
b5: true
|
|
215
215
|
}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'object with const string property',
|
|
219
|
+
schema: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: {
|
|
222
|
+
a: { const: 'const string' }
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
input: { a: 'const string' }
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'object with const number property',
|
|
229
|
+
schema: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
properties: {
|
|
232
|
+
a: { const: 1 }
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
input: { a: 1 }
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'object with const bool property',
|
|
239
|
+
schema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
a: { const: true }
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
input: { a: true }
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'object with const object property',
|
|
249
|
+
schema: {
|
|
250
|
+
type: 'object',
|
|
251
|
+
properties: {
|
|
252
|
+
foo: { const: { bar: 'baz' } }
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
input: {
|
|
256
|
+
foo: { bar: 'baz' }
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'object with const null property',
|
|
261
|
+
schema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
foo: { const: null }
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
input: {
|
|
268
|
+
foo: null
|
|
269
|
+
}
|
|
216
270
|
}
|
|
217
271
|
]
|
|
218
272
|
|
package/index.js
CHANGED
|
@@ -9,7 +9,8 @@ const { randomUUID } = require('crypto')
|
|
|
9
9
|
|
|
10
10
|
const validate = require('./schema-validator')
|
|
11
11
|
const Serializer = require('./serializer')
|
|
12
|
-
const
|
|
12
|
+
const Validator = require('./validator')
|
|
13
|
+
const RefResolver = require('./ref-resolver')
|
|
13
14
|
|
|
14
15
|
let largeArraySize = 2e4
|
|
15
16
|
let largeArrayMechanism = 'default'
|
|
@@ -57,20 +58,12 @@ function resolveRef (location, ref) {
|
|
|
57
58
|
const schemaId = ref.slice(0, hashIndex) || location.schemaId
|
|
58
59
|
const jsonPointer = ref.slice(hashIndex) || '#'
|
|
59
60
|
|
|
60
|
-
const
|
|
61
|
+
const schema = refResolver.getSchema(schemaId, jsonPointer)
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
ajvSchema = ajvInstance.getSchema(schemaRef)
|
|
65
|
-
} catch (error) {
|
|
63
|
+
if (schema === undefined) {
|
|
66
64
|
throw new Error(`Cannot find reference "${ref}"`)
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
if (ajvSchema === undefined) {
|
|
70
|
-
throw new Error(`Cannot find reference "${ref}"`)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const schema = ajvSchema.schema
|
|
74
67
|
if (schema.$ref !== undefined) {
|
|
75
68
|
return resolveRef({ schema, schemaId, jsonPointer }, schema.$ref)
|
|
76
69
|
}
|
|
@@ -82,41 +75,31 @@ const arrayItemsReferenceSerializersMap = new Map()
|
|
|
82
75
|
const objectReferenceSerializersMap = new Map()
|
|
83
76
|
|
|
84
77
|
let rootSchemaId = null
|
|
85
|
-
let
|
|
78
|
+
let refResolver = null
|
|
79
|
+
let validator = null
|
|
86
80
|
let contextFunctions = null
|
|
87
81
|
|
|
88
82
|
function build (schema, options) {
|
|
89
|
-
schema = clone(schema)
|
|
90
|
-
|
|
91
83
|
arrayItemsReferenceSerializersMap.clear()
|
|
92
84
|
objectReferenceSerializersMap.clear()
|
|
93
85
|
|
|
94
86
|
contextFunctions = []
|
|
95
87
|
options = options || {}
|
|
96
88
|
|
|
97
|
-
|
|
89
|
+
refResolver = new RefResolver()
|
|
90
|
+
validator = new Validator(options.ajv)
|
|
91
|
+
|
|
98
92
|
rootSchemaId = schema.$id || randomUUID()
|
|
99
93
|
|
|
100
94
|
isValidSchema(schema)
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
validator.addSchema(schema, rootSchemaId)
|
|
96
|
+
refResolver.addSchema(schema, rootSchemaId)
|
|
103
97
|
|
|
104
98
|
if (options.schema) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
isValidSchema(externalSchema, key)
|
|
110
|
-
extendDateTimeType(externalSchema)
|
|
111
|
-
|
|
112
|
-
let schemaKey = externalSchema.$id || key
|
|
113
|
-
if (externalSchema.$id !== undefined && externalSchema.$id[0] === '#') {
|
|
114
|
-
schemaKey = key + externalSchema.$id // relative URI
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (ajvInstance.getSchema(schemaKey) === undefined) {
|
|
118
|
-
ajvInstance.addSchema(externalSchema, schemaKey)
|
|
119
|
-
}
|
|
99
|
+
for (const key of Object.keys(options.schema)) {
|
|
100
|
+
isValidSchema(options.schema[key], key)
|
|
101
|
+
validator.addSchema(options.schema[key], key)
|
|
102
|
+
refResolver.addSchema(options.schema[key], key)
|
|
120
103
|
}
|
|
121
104
|
}
|
|
122
105
|
|
|
@@ -157,27 +140,28 @@ function build (schema, options) {
|
|
|
157
140
|
return main
|
|
158
141
|
`
|
|
159
142
|
|
|
160
|
-
const dependenciesName = ['
|
|
143
|
+
const dependenciesName = ['validator', 'serializer', contextFunctionCode]
|
|
161
144
|
|
|
162
145
|
if (options.debugMode) {
|
|
163
146
|
options.mode = 'debug'
|
|
164
147
|
}
|
|
165
148
|
|
|
166
149
|
if (options.mode === 'debug') {
|
|
167
|
-
return { code: dependenciesName.join('\n'), ajv:
|
|
150
|
+
return { code: dependenciesName.join('\n'), validator, ajv: validator.ajv }
|
|
168
151
|
}
|
|
169
152
|
|
|
170
153
|
if (options.mode === 'standalone') {
|
|
171
154
|
// lazy load
|
|
172
155
|
const buildStandaloneCode = require('./standalone')
|
|
173
|
-
return buildStandaloneCode(options,
|
|
156
|
+
return buildStandaloneCode(options, validator, contextFunctionCode)
|
|
174
157
|
}
|
|
175
158
|
|
|
176
159
|
/* eslint no-new-func: "off" */
|
|
177
|
-
const contextFunc = new Function('
|
|
178
|
-
const stringifyFunc = contextFunc(
|
|
160
|
+
const contextFunc = new Function('validator', 'serializer', contextFunctionCode)
|
|
161
|
+
const stringifyFunc = contextFunc(validator, serializer)
|
|
179
162
|
|
|
180
|
-
|
|
163
|
+
refResolver = null
|
|
164
|
+
validator = null
|
|
181
165
|
rootSchemaId = null
|
|
182
166
|
contextFunctions = null
|
|
183
167
|
arrayItemsReferenceSerializersMap.clear()
|
|
@@ -341,17 +325,16 @@ function buildCode (location) {
|
|
|
341
325
|
const propertiesLocation = mergeLocation(location, 'properties')
|
|
342
326
|
Object.keys(schema.properties || {}).forEach((key) => {
|
|
343
327
|
let propertyLocation = mergeLocation(propertiesLocation, key)
|
|
344
|
-
if (
|
|
345
|
-
propertyLocation = resolveRef(location,
|
|
346
|
-
schema.properties[key] = propertyLocation.schema
|
|
328
|
+
if (propertyLocation.$ref) {
|
|
329
|
+
propertyLocation = resolveRef(location, propertyLocation.$ref)
|
|
347
330
|
}
|
|
348
331
|
|
|
349
|
-
// Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
|
|
350
|
-
// see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
|
|
351
|
-
|
|
352
332
|
const sanitized = JSON.stringify(key)
|
|
353
333
|
const asString = JSON.stringify(sanitized)
|
|
354
334
|
|
|
335
|
+
// Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
|
|
336
|
+
// see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
|
|
337
|
+
|
|
355
338
|
code += `
|
|
356
339
|
if (obj[${sanitized}] !== undefined) {
|
|
357
340
|
${addComma}
|
|
@@ -360,7 +343,7 @@ function buildCode (location) {
|
|
|
360
343
|
|
|
361
344
|
code += buildValue(propertyLocation, `obj[${JSON.stringify(key)}]`)
|
|
362
345
|
|
|
363
|
-
const defaultValue = schema.
|
|
346
|
+
const defaultValue = propertyLocation.schema.default
|
|
364
347
|
if (defaultValue !== undefined) {
|
|
365
348
|
code += `
|
|
366
349
|
} else {
|
|
@@ -475,16 +458,6 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
|
|
|
475
458
|
mergedSchema.anyOf.push(...allOfSchema.anyOf)
|
|
476
459
|
}
|
|
477
460
|
|
|
478
|
-
if (allOfSchema.fjs_type !== undefined) {
|
|
479
|
-
if (
|
|
480
|
-
mergedSchema.fjs_type !== undefined &&
|
|
481
|
-
mergedSchema.fjs_type !== allOfSchema.fjs_type
|
|
482
|
-
) {
|
|
483
|
-
throw new Error('allOf schemas have different fjs_type values')
|
|
484
|
-
}
|
|
485
|
-
mergedSchema.fjs_type = allOfSchema.fjs_type
|
|
486
|
-
}
|
|
487
|
-
|
|
488
461
|
if (allOfSchema.allOf !== undefined) {
|
|
489
462
|
mergeAllOfSchema(location, allOfSchema, mergedSchema)
|
|
490
463
|
}
|
|
@@ -492,7 +465,8 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
|
|
|
492
465
|
delete mergedSchema.allOf
|
|
493
466
|
|
|
494
467
|
mergedSchema.$id = `merged_${randomUUID()}`
|
|
495
|
-
|
|
468
|
+
validator.addSchema(mergedSchema)
|
|
469
|
+
refResolver.addSchema(mergedSchema)
|
|
496
470
|
location.schemaId = mergedSchema.$id
|
|
497
471
|
location.jsonPointer = '#'
|
|
498
472
|
}
|
|
@@ -521,7 +495,7 @@ function addIfThenElse (location) {
|
|
|
521
495
|
const ifSchemaRef = ifLocation.schemaId + ifLocation.jsonPointer
|
|
522
496
|
|
|
523
497
|
let code = `
|
|
524
|
-
if (
|
|
498
|
+
if (validator.validate("${ifSchemaRef}", obj)) {
|
|
525
499
|
`
|
|
526
500
|
|
|
527
501
|
const thenLocation = mergeLocation(location, 'then')
|
|
@@ -795,14 +769,21 @@ function buildValue (location, input) {
|
|
|
795
769
|
location.schema = mergedSchema
|
|
796
770
|
}
|
|
797
771
|
|
|
798
|
-
|
|
799
|
-
const nullable = schema.nullable === true
|
|
772
|
+
const type = schema.type
|
|
773
|
+
const nullable = schema.nullable === true || (Array.isArray(type) && type.includes('null'))
|
|
800
774
|
|
|
801
775
|
let code = ''
|
|
802
776
|
let funcName
|
|
803
777
|
|
|
804
|
-
if (
|
|
805
|
-
|
|
778
|
+
if ('const' in schema) {
|
|
779
|
+
if (nullable) {
|
|
780
|
+
code += `
|
|
781
|
+
json += ${input} === null ? 'null' : '${JSON.stringify(schema.const)}'
|
|
782
|
+
`
|
|
783
|
+
return code
|
|
784
|
+
}
|
|
785
|
+
code += `json += '${JSON.stringify(schema.const)}'`
|
|
786
|
+
return code
|
|
806
787
|
}
|
|
807
788
|
|
|
808
789
|
switch (type) {
|
|
@@ -810,7 +791,15 @@ function buildValue (location, input) {
|
|
|
810
791
|
code += 'json += serializer.asNull()'
|
|
811
792
|
break
|
|
812
793
|
case 'string': {
|
|
813
|
-
|
|
794
|
+
if (schema.format === 'date-time') {
|
|
795
|
+
funcName = nullable ? 'serializer.asDateTimeNullable.bind(serializer)' : 'serializer.asDateTime.bind(serializer)'
|
|
796
|
+
} else if (schema.format === 'date') {
|
|
797
|
+
funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
|
|
798
|
+
} else if (schema.format === 'time') {
|
|
799
|
+
funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
|
|
800
|
+
} else {
|
|
801
|
+
funcName = nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
|
|
802
|
+
}
|
|
814
803
|
code += `json += ${funcName}(${input})`
|
|
815
804
|
break
|
|
816
805
|
}
|
|
@@ -827,15 +816,7 @@ function buildValue (location, input) {
|
|
|
827
816
|
code += `json += ${funcName}(${input})`
|
|
828
817
|
break
|
|
829
818
|
case 'object':
|
|
830
|
-
|
|
831
|
-
funcName = nullable ? 'serializer.asDateTimeNullable.bind(serializer)' : 'serializer.asDateTime.bind(serializer)'
|
|
832
|
-
} else if (schema.format === 'date') {
|
|
833
|
-
funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
|
|
834
|
-
} else if (schema.format === 'time') {
|
|
835
|
-
funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
|
|
836
|
-
} else {
|
|
837
|
-
funcName = buildObject(location)
|
|
838
|
-
}
|
|
819
|
+
funcName = buildObject(location)
|
|
839
820
|
code += `json += ${funcName}(${input})`
|
|
840
821
|
break
|
|
841
822
|
case 'array':
|
|
@@ -853,7 +834,7 @@ function buildValue (location, input) {
|
|
|
853
834
|
const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer
|
|
854
835
|
const nestedResult = buildValue(optionLocation, input)
|
|
855
836
|
code += `
|
|
856
|
-
${index === 0 ? 'if' : 'else if'}(
|
|
837
|
+
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))
|
|
857
838
|
${nestedResult}
|
|
858
839
|
`
|
|
859
840
|
}
|
|
@@ -865,19 +846,10 @@ function buildValue (location, input) {
|
|
|
865
846
|
code += `
|
|
866
847
|
json += JSON.stringify(${input})
|
|
867
848
|
`
|
|
868
|
-
} else
|
|
869
|
-
code += `
|
|
870
|
-
if(ajv.validate(${JSON.stringify(schema)}, ${input}))
|
|
871
|
-
json += '${JSON.stringify(schema.const)}'
|
|
872
|
-
else
|
|
873
|
-
throw new Error(\`Item $\{JSON.stringify(${input})} does not match schema definition.\`)
|
|
874
|
-
`
|
|
875
|
-
} else if (schema.type === undefined) {
|
|
849
|
+
} else {
|
|
876
850
|
code += `
|
|
877
851
|
json += JSON.stringify(${input})
|
|
878
852
|
`
|
|
879
|
-
} else {
|
|
880
|
-
throw new Error(`${schema.type} unsupported`)
|
|
881
853
|
}
|
|
882
854
|
break
|
|
883
855
|
default:
|
|
@@ -901,7 +873,18 @@ function buildValue (location, input) {
|
|
|
901
873
|
switch (type) {
|
|
902
874
|
case 'string': {
|
|
903
875
|
code += `
|
|
904
|
-
${statement}(
|
|
876
|
+
${statement}(
|
|
877
|
+
typeof ${input} === "string" ||
|
|
878
|
+
${input} === null ||
|
|
879
|
+
${input} instanceof Date ||
|
|
880
|
+
${input} instanceof RegExp ||
|
|
881
|
+
(
|
|
882
|
+
typeof ${input} === "object" &&
|
|
883
|
+
typeof ${input}.toString === "function" &&
|
|
884
|
+
${input}.toString !== Object.prototype.toString &&
|
|
885
|
+
!(${input} instanceof Date)
|
|
886
|
+
)
|
|
887
|
+
)
|
|
905
888
|
${nestedResult}
|
|
906
889
|
`
|
|
907
890
|
break
|
|
@@ -921,17 +904,10 @@ function buildValue (location, input) {
|
|
|
921
904
|
break
|
|
922
905
|
}
|
|
923
906
|
case 'object': {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
${
|
|
927
|
-
|
|
928
|
-
`
|
|
929
|
-
} else {
|
|
930
|
-
code += `
|
|
931
|
-
${statement}(typeof ${input} === "object" || ${input} === null)
|
|
932
|
-
${nestedResult}
|
|
933
|
-
`
|
|
934
|
-
}
|
|
907
|
+
code += `
|
|
908
|
+
${statement}(typeof ${input} === "object" || ${input} === null)
|
|
909
|
+
${nestedResult}
|
|
910
|
+
`
|
|
935
911
|
break
|
|
936
912
|
}
|
|
937
913
|
default: {
|
|
@@ -960,34 +936,10 @@ function buildValue (location, input) {
|
|
|
960
936
|
return code
|
|
961
937
|
}
|
|
962
938
|
|
|
963
|
-
// Ajv does not support js date format. In order to properly validate objects containing a date,
|
|
964
|
-
// it needs to replace all occurrences of the string date format with a custom keyword fjs_type.
|
|
965
|
-
// (see https://github.com/fastify/fast-json-stringify/pull/441)
|
|
966
|
-
function extendDateTimeType (schema) {
|
|
967
|
-
if (schema === null) return
|
|
968
|
-
|
|
969
|
-
if (schema.type === 'string') {
|
|
970
|
-
schema.fjs_type = 'string'
|
|
971
|
-
schema.type = ['string', 'object']
|
|
972
|
-
} else if (
|
|
973
|
-
Array.isArray(schema.type) &&
|
|
974
|
-
schema.type.includes('string') &&
|
|
975
|
-
!schema.type.includes('object')
|
|
976
|
-
) {
|
|
977
|
-
schema.fjs_type = 'string'
|
|
978
|
-
schema.type.push('object')
|
|
979
|
-
}
|
|
980
|
-
for (const property in schema) {
|
|
981
|
-
if (typeof schema[property] === 'object') {
|
|
982
|
-
extendDateTimeType(schema[property])
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
939
|
function isEmpty (schema) {
|
|
988
940
|
// eslint-disable-next-line
|
|
989
941
|
for (var key in schema) {
|
|
990
|
-
if (
|
|
942
|
+
if (Object.prototype.hasOwnProperty.call(schema, key) && schema[key] !== undefined) {
|
|
991
943
|
return false
|
|
992
944
|
}
|
|
993
945
|
}
|
|
@@ -998,9 +950,9 @@ module.exports = build
|
|
|
998
950
|
|
|
999
951
|
module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms
|
|
1000
952
|
|
|
1001
|
-
module.exports.restore = function ({ code,
|
|
953
|
+
module.exports.restore = function ({ code, validator }) {
|
|
1002
954
|
const serializer = new Serializer()
|
|
1003
955
|
// eslint-disable-next-line
|
|
1004
|
-
return (Function.apply(null, ['
|
|
1005
|
-
.apply(null, [
|
|
956
|
+
return (Function.apply(null, ['validator', 'serializer', code])
|
|
957
|
+
.apply(null, [validator, serializer]))
|
|
1006
958
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-json-stringify",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "Stringify your JSON at max speed",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://github.com/fastify/fast-json-stringify#readme",
|
|
35
35
|
"devDependencies": {
|
|
36
|
+
"@fastify/pre-commit": "^2.0.2",
|
|
36
37
|
"@sinclair/typebox": "^0.24.9",
|
|
37
38
|
"benchmark": "^2.1.4",
|
|
38
39
|
"cli-select": "^1.1.2",
|
|
39
40
|
"compile-json-stringify": "^0.1.2",
|
|
40
41
|
"is-my-json-valid": "^2.20.0",
|
|
41
|
-
"luxon": "^
|
|
42
|
-
"pre-commit": "^1.2.2",
|
|
42
|
+
"luxon": "^3.0.1",
|
|
43
43
|
"proxyquire": "^2.1.3",
|
|
44
44
|
"semver": "^7.1.0",
|
|
45
45
|
"simple-git": "^3.7.1",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"@fastify/deepmerge": "^1.0.0",
|
|
53
53
|
"ajv": "^8.10.0",
|
|
54
54
|
"ajv-formats": "^2.1.1",
|
|
55
|
+
"fast-deep-equal": "^3.1.3",
|
|
55
56
|
"fast-uri": "^2.1.0",
|
|
56
57
|
"rfdc": "^1.2.0"
|
|
57
58
|
},
|
package/ref-resolver.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const deepEqual = require('fast-deep-equal')
|
|
4
|
+
|
|
5
|
+
class RefResolver {
|
|
6
|
+
constructor () {
|
|
7
|
+
this.schemas = {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
addSchema (schema, schemaId) {
|
|
11
|
+
if (schema.$id !== undefined && schema.$id.charAt(0) !== '#') {
|
|
12
|
+
schemaId = schema.$id
|
|
13
|
+
}
|
|
14
|
+
if (this.getSchema(schemaId) === undefined) {
|
|
15
|
+
this.insertSchemaBySchemaId(schema, schemaId)
|
|
16
|
+
this.insertSchemaSubschemas(schema, schemaId)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getSchema (schemaId, jsonPointer = '#') {
|
|
21
|
+
const schema = this.schemas[schemaId]
|
|
22
|
+
if (schema === undefined) {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
if (schema.anchors[jsonPointer] !== undefined) {
|
|
26
|
+
return schema.anchors[jsonPointer]
|
|
27
|
+
}
|
|
28
|
+
return getDataByJSONPointer(schema.schema, jsonPointer)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
insertSchemaBySchemaId (schema, schemaId) {
|
|
32
|
+
if (
|
|
33
|
+
this.schemas[schemaId] !== undefined &&
|
|
34
|
+
!deepEqual(schema, this.schemas[schemaId].schema)
|
|
35
|
+
) {
|
|
36
|
+
throw new Error(`There is already another schema with id ${schemaId}`)
|
|
37
|
+
}
|
|
38
|
+
this.schemas[schemaId] = { schema, anchors: {} }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
insertSchemaByAnchor (schema, schemaId, anchor) {
|
|
42
|
+
const { anchors } = this.schemas[schemaId]
|
|
43
|
+
if (
|
|
44
|
+
anchors[anchor] !== undefined &&
|
|
45
|
+
!deepEqual(schema, anchors[anchor])
|
|
46
|
+
) {
|
|
47
|
+
throw new Error(`There is already another schema with id ${schemaId}#${anchor}`)
|
|
48
|
+
}
|
|
49
|
+
anchors[anchor] = schema
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
insertSchemaSubschemas (schema, rootSchemaId) {
|
|
53
|
+
const schemaId = schema.$id
|
|
54
|
+
if (schemaId !== undefined && typeof schemaId === 'string') {
|
|
55
|
+
if (schemaId.charAt(0) === '#') {
|
|
56
|
+
this.insertSchemaByAnchor(schema, rootSchemaId, schemaId)
|
|
57
|
+
} else {
|
|
58
|
+
this.insertSchemaBySchemaId(schema, schemaId)
|
|
59
|
+
rootSchemaId = schemaId
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const key in schema) {
|
|
64
|
+
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
65
|
+
this.insertSchemaSubschemas(schema[key], rootSchemaId)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getDataByJSONPointer (data, jsonPointer) {
|
|
72
|
+
const parts = jsonPointer.split('/')
|
|
73
|
+
let current = data
|
|
74
|
+
for (const part of parts) {
|
|
75
|
+
if (part === '' || part === '#') continue
|
|
76
|
+
if (typeof current !== 'object' || current === null) {
|
|
77
|
+
return undefined
|
|
78
|
+
}
|
|
79
|
+
current = current[part]
|
|
80
|
+
}
|
|
81
|
+
return current
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = RefResolver
|
package/serializer.js
CHANGED
|
@@ -74,6 +74,9 @@ module.exports = class Serializer {
|
|
|
74
74
|
if (date instanceof Date) {
|
|
75
75
|
return '"' + date.toISOString() + '"'
|
|
76
76
|
}
|
|
77
|
+
if (typeof date === 'string') {
|
|
78
|
+
return '"' + date + '"'
|
|
79
|
+
}
|
|
77
80
|
throw new Error(`The value "${date}" cannot be converted to a date-time.`)
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -86,6 +89,9 @@ module.exports = class Serializer {
|
|
|
86
89
|
if (date instanceof Date) {
|
|
87
90
|
return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"'
|
|
88
91
|
}
|
|
92
|
+
if (typeof date === 'string') {
|
|
93
|
+
return '"' + date + '"'
|
|
94
|
+
}
|
|
89
95
|
throw new Error(`The value "${date}" cannot be converted to a date.`)
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -98,6 +104,9 @@ module.exports = class Serializer {
|
|
|
98
104
|
if (date instanceof Date) {
|
|
99
105
|
return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"'
|
|
100
106
|
}
|
|
107
|
+
if (typeof date === 'string') {
|
|
108
|
+
return '"' + date + '"'
|
|
109
|
+
}
|
|
101
110
|
throw new Error(`The value "${date}" cannot be converted to a time.`)
|
|
102
111
|
}
|
|
103
112
|
|
package/standalone.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const path = require('path')
|
|
3
3
|
|
|
4
|
-
function buildStandaloneCode (options,
|
|
4
|
+
function buildStandaloneCode (options, validator, contextFunctionCode) {
|
|
5
5
|
const serializerCode = fs.readFileSync(path.join(__dirname, 'serializer.js')).toString()
|
|
6
6
|
let buildAjvCode = ''
|
|
7
7
|
let defaultAjvSchema = ''
|
|
8
|
-
const defaultMeta =
|
|
8
|
+
const defaultMeta = validator.ajv.defaultMeta()
|
|
9
9
|
if (typeof defaultMeta === 'string') {
|
|
10
10
|
defaultAjvSchema = defaultMeta
|
|
11
11
|
} else {
|
|
12
12
|
defaultAjvSchema = defaultMeta.$id || defaultMeta.id
|
|
13
13
|
}
|
|
14
|
-
const shouldUseAjv = contextFunctionCode.indexOf('
|
|
14
|
+
const shouldUseAjv = contextFunctionCode.indexOf('validator') !== -1
|
|
15
15
|
// we need to export the custom json schema
|
|
16
16
|
let ajvSchemasCode = ''
|
|
17
17
|
if (shouldUseAjv) {
|
|
18
|
-
ajvSchemasCode += `const
|
|
19
|
-
for (const [id, schema] of Object.entries(
|
|
18
|
+
ajvSchemasCode += `const validator = new Validator(${JSON.stringify(options.ajv || {})})\n`
|
|
19
|
+
for (const [id, schema] of Object.entries(validator.ajv.schemas)) {
|
|
20
20
|
// should skip ajv default schema
|
|
21
21
|
if (id === defaultAjvSchema) continue
|
|
22
|
-
ajvSchemasCode += `ajv.addSchema(${JSON.stringify(schema.schema)}, "${id}")\n`
|
|
22
|
+
ajvSchemasCode += `validator.ajv.addSchema(${JSON.stringify(schema.schema)}, "${id}")\n`
|
|
23
23
|
}
|
|
24
|
-
buildAjvCode = fs.readFileSync(path.join(__dirname, '
|
|
25
|
-
buildAjvCode = buildAjvCode.replace("'use strict'", '').replace('module.exports =
|
|
24
|
+
buildAjvCode = fs.readFileSync(path.join(__dirname, 'validator.js')).toString()
|
|
25
|
+
buildAjvCode = buildAjvCode.replace("'use strict'", '').replace('module.exports = SchemaValidator', '')
|
|
26
26
|
}
|
|
27
27
|
return `
|
|
28
28
|
'use strict'
|
package/test/const.test.js
CHANGED
|
@@ -24,6 +24,184 @@ test('schema with const string', (t) => {
|
|
|
24
24
|
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
+
test('schema with const string and different input', (t) => {
|
|
28
|
+
t.plan(2)
|
|
29
|
+
|
|
30
|
+
const schema = {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
foo: { const: 'bar' }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const validate = validator(schema)
|
|
38
|
+
const stringify = build(schema)
|
|
39
|
+
const output = stringify({
|
|
40
|
+
foo: 'baz'
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
t.equal(output, '{"foo":"bar"}')
|
|
44
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('schema with const string and different type input', (t) => {
|
|
48
|
+
t.plan(2)
|
|
49
|
+
|
|
50
|
+
const schema = {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
foo: { const: 'bar' }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const validate = validator(schema)
|
|
58
|
+
const stringify = build(schema)
|
|
59
|
+
const output = stringify({
|
|
60
|
+
foo: 1
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
t.equal(output, '{"foo":"bar"}')
|
|
64
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('schema with const string and no input', (t) => {
|
|
68
|
+
t.plan(2)
|
|
69
|
+
|
|
70
|
+
const schema = {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
foo: { const: 'bar' }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const validate = validator(schema)
|
|
78
|
+
const stringify = build(schema)
|
|
79
|
+
const output = stringify({})
|
|
80
|
+
|
|
81
|
+
t.equal(output, '{}')
|
|
82
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('schema with const number', (t) => {
|
|
86
|
+
t.plan(2)
|
|
87
|
+
|
|
88
|
+
const schema = {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
foo: { const: 1 }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const validate = validator(schema)
|
|
96
|
+
const stringify = build(schema)
|
|
97
|
+
const output = stringify({
|
|
98
|
+
foo: 1
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
t.equal(output, '{"foo":1}')
|
|
102
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('schema with const number and different input', (t) => {
|
|
106
|
+
t.plan(2)
|
|
107
|
+
|
|
108
|
+
const schema = {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
foo: { const: 1 }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const validate = validator(schema)
|
|
116
|
+
const stringify = build(schema)
|
|
117
|
+
const output = stringify({
|
|
118
|
+
foo: 2
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
t.equal(output, '{"foo":1}')
|
|
122
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('schema with const bool', (t) => {
|
|
126
|
+
t.plan(2)
|
|
127
|
+
|
|
128
|
+
const schema = {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
foo: { const: true }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const validate = validator(schema)
|
|
136
|
+
const stringify = build(schema)
|
|
137
|
+
const output = stringify({
|
|
138
|
+
foo: true
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
t.equal(output, '{"foo":true}')
|
|
142
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('schema with const number', (t) => {
|
|
146
|
+
t.plan(2)
|
|
147
|
+
|
|
148
|
+
const schema = {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
foo: { const: 1 }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const validate = validator(schema)
|
|
156
|
+
const stringify = build(schema)
|
|
157
|
+
const output = stringify({
|
|
158
|
+
foo: 1
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
t.equal(output, '{"foo":1}')
|
|
162
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('schema with const null', (t) => {
|
|
166
|
+
t.plan(2)
|
|
167
|
+
|
|
168
|
+
const schema = {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
foo: { const: null }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const validate = validator(schema)
|
|
176
|
+
const stringify = build(schema)
|
|
177
|
+
const output = stringify({
|
|
178
|
+
foo: null
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
t.equal(output, '{"foo":null}')
|
|
182
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('schema with const array', (t) => {
|
|
186
|
+
t.plan(2)
|
|
187
|
+
|
|
188
|
+
const schema = {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
foo: { const: [1, 2, 3] }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const validate = validator(schema)
|
|
196
|
+
const stringify = build(schema)
|
|
197
|
+
const output = stringify({
|
|
198
|
+
foo: [1, 2, 3]
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
t.equal(output, '{"foo":[1,2,3]}')
|
|
202
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
203
|
+
})
|
|
204
|
+
|
|
27
205
|
test('schema with const object', (t) => {
|
|
28
206
|
t.plan(2)
|
|
29
207
|
|
|
@@ -44,6 +222,56 @@ test('schema with const object', (t) => {
|
|
|
44
222
|
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
45
223
|
})
|
|
46
224
|
|
|
225
|
+
test('schema with const and null as type', (t) => {
|
|
226
|
+
t.plan(4)
|
|
227
|
+
|
|
228
|
+
const schema = {
|
|
229
|
+
type: 'object',
|
|
230
|
+
properties: {
|
|
231
|
+
foo: { type: ['string', 'null'], const: 'baz' }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const validate = validator(schema)
|
|
236
|
+
const stringify = build(schema)
|
|
237
|
+
const output = stringify({
|
|
238
|
+
foo: null
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
t.equal(output, '{"foo":null}')
|
|
242
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
243
|
+
|
|
244
|
+
const output2 = stringify({ foo: 'baz' })
|
|
245
|
+
t.equal(output2, '{"foo":"baz"}')
|
|
246
|
+
t.ok(validate(JSON.parse(output2)), 'valid schema')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('schema with const as nullable', (t) => {
|
|
250
|
+
t.plan(4)
|
|
251
|
+
|
|
252
|
+
const schema = {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
foo: { nullable: true, const: 'baz' }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const validate = validator(schema)
|
|
260
|
+
const stringify = build(schema)
|
|
261
|
+
const output = stringify({
|
|
262
|
+
foo: null
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
t.equal(output, '{"foo":null}')
|
|
266
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
267
|
+
|
|
268
|
+
const output2 = stringify({
|
|
269
|
+
foo: 'baz'
|
|
270
|
+
})
|
|
271
|
+
t.equal(output2, '{"foo":"baz"}')
|
|
272
|
+
t.ok(validate(JSON.parse(output2)), 'valid schema')
|
|
273
|
+
})
|
|
274
|
+
|
|
47
275
|
test('schema with const and invalid object', (t) => {
|
|
48
276
|
t.plan(2)
|
|
49
277
|
|
|
@@ -55,13 +283,12 @@ test('schema with const and invalid object', (t) => {
|
|
|
55
283
|
required: ['foo']
|
|
56
284
|
}
|
|
57
285
|
|
|
286
|
+
const validate = validator(schema)
|
|
58
287
|
const stringify = build(schema)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
t.ok(err)
|
|
66
|
-
}
|
|
288
|
+
const result = stringify({
|
|
289
|
+
foo: { foo: 'baz' }
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
t.equal(result, '{"foo":{"foo":"bar"}}')
|
|
293
|
+
t.ok(validate(JSON.parse(result)), 'valid schema')
|
|
67
294
|
})
|
package/test/debug-mode.test.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const test = require('tap').test
|
|
4
4
|
const fjs = require('..')
|
|
5
|
+
|
|
5
6
|
const Ajv = require('ajv').default
|
|
7
|
+
const Validator = require('../validator')
|
|
6
8
|
|
|
7
9
|
function build (opts) {
|
|
8
10
|
return fjs({
|
|
@@ -18,31 +20,34 @@ function build (opts) {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
test('activate debug mode', t => {
|
|
21
|
-
t.plan(
|
|
23
|
+
t.plan(4)
|
|
22
24
|
const debugMode = build({ debugMode: true })
|
|
23
25
|
|
|
24
26
|
t.type(debugMode, 'object')
|
|
25
27
|
t.ok(debugMode.ajv instanceof Ajv)
|
|
28
|
+
t.ok(debugMode.validator instanceof Validator)
|
|
26
29
|
t.type(debugMode.code, 'string')
|
|
27
30
|
})
|
|
28
31
|
|
|
29
32
|
test('activate debug mode truthy', t => {
|
|
30
|
-
t.plan(
|
|
33
|
+
t.plan(4)
|
|
31
34
|
|
|
32
35
|
const debugMode = build({ debugMode: 'yes' })
|
|
33
36
|
|
|
34
37
|
t.type(debugMode, 'object')
|
|
35
38
|
t.type(debugMode.code, 'string')
|
|
36
39
|
t.ok(debugMode.ajv instanceof Ajv)
|
|
40
|
+
t.ok(debugMode.validator instanceof Validator)
|
|
37
41
|
})
|
|
38
42
|
|
|
39
43
|
test('to string auto-consistent', t => {
|
|
40
|
-
t.plan(
|
|
44
|
+
t.plan(5)
|
|
41
45
|
const debugMode = build({ debugMode: 1 })
|
|
42
46
|
|
|
43
47
|
t.type(debugMode, 'object')
|
|
44
48
|
t.type(debugMode.code, 'string')
|
|
45
49
|
t.ok(debugMode.ajv instanceof Ajv)
|
|
50
|
+
t.ok(debugMode.validator instanceof Validator)
|
|
46
51
|
|
|
47
52
|
const compiled = fjs.restore(debugMode)
|
|
48
53
|
const tobe = JSON.stringify({ firstName: 'Foo' })
|
|
@@ -50,7 +55,7 @@ test('to string auto-consistent', t => {
|
|
|
50
55
|
})
|
|
51
56
|
|
|
52
57
|
test('to string auto-consistent with ajv', t => {
|
|
53
|
-
t.plan(
|
|
58
|
+
t.plan(5)
|
|
54
59
|
|
|
55
60
|
const debugMode = fjs({
|
|
56
61
|
title: 'object with multiple types field',
|
|
@@ -69,6 +74,7 @@ test('to string auto-consistent with ajv', t => {
|
|
|
69
74
|
t.type(debugMode, 'object')
|
|
70
75
|
t.type(debugMode.code, 'string')
|
|
71
76
|
t.ok(debugMode.ajv instanceof Ajv)
|
|
77
|
+
t.ok(debugMode.validator instanceof Validator)
|
|
72
78
|
|
|
73
79
|
const compiled = fjs.restore(debugMode)
|
|
74
80
|
const tobe = JSON.stringify({ str: 'Foo' })
|
package/test/nullable.test.js
CHANGED
|
@@ -444,3 +444,100 @@ test('large array of nullable booleans with default mechanism', (t) => {
|
|
|
444
444
|
t.same(result, JSON.stringify(data))
|
|
445
445
|
t.same(JSON.parse(result), data)
|
|
446
446
|
})
|
|
447
|
+
|
|
448
|
+
test('nullable type in the schema', (t) => {
|
|
449
|
+
t.plan(2)
|
|
450
|
+
|
|
451
|
+
const schema = {
|
|
452
|
+
type: ['object', 'null'],
|
|
453
|
+
properties: {
|
|
454
|
+
foo: {
|
|
455
|
+
type: 'string'
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const stringify = build(schema)
|
|
461
|
+
|
|
462
|
+
const data = { foo: 'bar' }
|
|
463
|
+
|
|
464
|
+
t.same(stringify(data), JSON.stringify(data))
|
|
465
|
+
t.same(stringify(null), JSON.stringify(null))
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test('throw an error if the value doesn\'t match the type', (t) => {
|
|
469
|
+
t.plan(2)
|
|
470
|
+
|
|
471
|
+
const schema = {
|
|
472
|
+
type: 'object',
|
|
473
|
+
additionalProperties: false,
|
|
474
|
+
required: ['data'],
|
|
475
|
+
properties: {
|
|
476
|
+
data: {
|
|
477
|
+
type: 'array',
|
|
478
|
+
minItems: 1,
|
|
479
|
+
items: {
|
|
480
|
+
oneOf: [
|
|
481
|
+
{
|
|
482
|
+
type: 'string'
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
type: 'number'
|
|
486
|
+
}
|
|
487
|
+
]
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const stringify = build(schema)
|
|
494
|
+
|
|
495
|
+
const validData = { data: [1, 'testing'] }
|
|
496
|
+
t.equal(stringify(validData), JSON.stringify(validData))
|
|
497
|
+
|
|
498
|
+
const invalidData = { data: [false, 'testing'] }
|
|
499
|
+
t.throws(() => stringify(invalidData))
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
test('nullable value in oneOf', (t) => {
|
|
503
|
+
t.plan(1)
|
|
504
|
+
|
|
505
|
+
const schema = {
|
|
506
|
+
type: 'object',
|
|
507
|
+
properties: {
|
|
508
|
+
data: {
|
|
509
|
+
oneOf: [
|
|
510
|
+
{
|
|
511
|
+
type: 'array',
|
|
512
|
+
items: {
|
|
513
|
+
type: 'object',
|
|
514
|
+
properties: {
|
|
515
|
+
id: { type: 'integer', minimum: 1 }
|
|
516
|
+
},
|
|
517
|
+
additionalProperties: false,
|
|
518
|
+
required: ['id']
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
type: 'array',
|
|
523
|
+
items: {
|
|
524
|
+
type: 'object',
|
|
525
|
+
properties: {
|
|
526
|
+
job: { type: 'string', nullable: true }
|
|
527
|
+
},
|
|
528
|
+
additionalProperties: false,
|
|
529
|
+
required: ['job']
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
]
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
required: ['data'],
|
|
536
|
+
additionalProperties: false
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const stringify = build(schema)
|
|
540
|
+
|
|
541
|
+
const data = { data: [{ job: null }] }
|
|
542
|
+
t.equal(stringify(data), JSON.stringify(data))
|
|
543
|
+
})
|
package/test/ref.test.js
CHANGED
|
@@ -1953,3 +1953,31 @@ test('should resolve absolute $refs', (t) => {
|
|
|
1953
1953
|
|
|
1954
1954
|
t.equal(output, JSON.stringify(object))
|
|
1955
1955
|
})
|
|
1956
|
+
|
|
1957
|
+
test('nested schema should overwrite anchor scope', (t) => {
|
|
1958
|
+
t.plan(2)
|
|
1959
|
+
|
|
1960
|
+
const externalSchema = {
|
|
1961
|
+
root: {
|
|
1962
|
+
$id: 'root',
|
|
1963
|
+
definitions: {
|
|
1964
|
+
subschema: {
|
|
1965
|
+
$id: 'subschema',
|
|
1966
|
+
definitions: {
|
|
1967
|
+
anchorSchema: {
|
|
1968
|
+
$id: '#anchor',
|
|
1969
|
+
type: 'string'
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
const data = 'test'
|
|
1978
|
+
const stringify = build({ $ref: 'subschema#anchor' }, { schema: externalSchema })
|
|
1979
|
+
const output = stringify(data)
|
|
1980
|
+
|
|
1981
|
+
t.equal(output, JSON.stringify(data))
|
|
1982
|
+
t.throws(() => build({ $ref: 'root#anchor' }, { schema: externalSchema }))
|
|
1983
|
+
})
|
package/test/typesArray.test.js
CHANGED
|
@@ -438,6 +438,35 @@ test('object that is simultaneously a string and a json switched', (t) => {
|
|
|
438
438
|
t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}')
|
|
439
439
|
})
|
|
440
440
|
|
|
441
|
+
test('class instance that is simultaneously a string and a json', (t) => {
|
|
442
|
+
t.plan(2)
|
|
443
|
+
|
|
444
|
+
const schema = {
|
|
445
|
+
type: 'object',
|
|
446
|
+
properties: {
|
|
447
|
+
simultaneously: {
|
|
448
|
+
type: ['string', 'object'],
|
|
449
|
+
properties: {
|
|
450
|
+
foo: { type: 'string' }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
class Test {
|
|
457
|
+
toString () { return 'hello' }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const likeObjectId = new Test()
|
|
461
|
+
|
|
462
|
+
const stringify = build(schema)
|
|
463
|
+
const valueStr = stringify({ simultaneously: likeObjectId })
|
|
464
|
+
t.equal(valueStr, '{"simultaneously":"hello"}')
|
|
465
|
+
|
|
466
|
+
const valueObj = stringify({ simultaneously: { foo: likeObjectId } })
|
|
467
|
+
t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}')
|
|
468
|
+
})
|
|
469
|
+
|
|
441
470
|
test('should throw an error when type is array and object is null', (t) => {
|
|
442
471
|
t.plan(1)
|
|
443
472
|
const schema = {
|
package/validator.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Ajv = require('ajv')
|
|
4
|
+
const fastUri = require('fast-uri')
|
|
5
|
+
const ajvFormats = require('ajv-formats')
|
|
6
|
+
const clone = require('rfdc')({ proto: true })
|
|
7
|
+
|
|
8
|
+
class Validator {
|
|
9
|
+
constructor (ajvOptions) {
|
|
10
|
+
this.ajv = new Ajv({
|
|
11
|
+
...ajvOptions,
|
|
12
|
+
strictSchema: false,
|
|
13
|
+
validateSchema: false,
|
|
14
|
+
allowUnionTypes: true,
|
|
15
|
+
uriResolver: fastUri
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
ajvFormats(this.ajv)
|
|
19
|
+
|
|
20
|
+
this.ajv.addKeyword({
|
|
21
|
+
keyword: 'fjs_type',
|
|
22
|
+
type: 'object',
|
|
23
|
+
errors: false,
|
|
24
|
+
validate: (type, date) => {
|
|
25
|
+
return date instanceof Date
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
addSchema (schema, schemaName) {
|
|
31
|
+
let schemaKey = schema.$id || schemaName
|
|
32
|
+
if (schema.$id !== undefined && schema.$id[0] === '#') {
|
|
33
|
+
schemaKey = schemaName + schema.$id // relative URI
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
this.ajv.refs[schemaKey] === undefined &&
|
|
38
|
+
this.ajv.schemas[schemaKey] === undefined
|
|
39
|
+
) {
|
|
40
|
+
const ajvSchema = clone(schema)
|
|
41
|
+
this.convertSchemaToAjvFormat(ajvSchema)
|
|
42
|
+
this.ajv.addSchema(ajvSchema, schemaKey)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
validate (schemaRef, data) {
|
|
47
|
+
return this.ajv.validate(schemaRef, data)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Ajv does not support js date format. In order to properly validate objects containing a date,
|
|
51
|
+
// it needs to replace all occurrences of the string date format with a custom keyword fjs_type.
|
|
52
|
+
// (see https://github.com/fastify/fast-json-stringify/pull/441)
|
|
53
|
+
convertSchemaToAjvFormat (schema) {
|
|
54
|
+
if (schema === null) return
|
|
55
|
+
|
|
56
|
+
if (schema.type === 'string') {
|
|
57
|
+
schema.fjs_type = 'string'
|
|
58
|
+
schema.type = ['string', 'object']
|
|
59
|
+
} else if (
|
|
60
|
+
Array.isArray(schema.type) &&
|
|
61
|
+
schema.type.includes('string') &&
|
|
62
|
+
!schema.type.includes('object')
|
|
63
|
+
) {
|
|
64
|
+
schema.fjs_type = 'string'
|
|
65
|
+
schema.type.push('object')
|
|
66
|
+
}
|
|
67
|
+
for (const property in schema) {
|
|
68
|
+
if (typeof schema[property] === 'object') {
|
|
69
|
+
this.convertSchemaToAjvFormat(schema[property])
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = Validator
|
package/ajv.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const Ajv = require('ajv')
|
|
4
|
-
const fastUri = require('fast-uri')
|
|
5
|
-
const ajvFormats = require('ajv-formats')
|
|
6
|
-
|
|
7
|
-
module.exports = buildAjv
|
|
8
|
-
|
|
9
|
-
function buildAjv (options) {
|
|
10
|
-
const ajvInstance = new Ajv({
|
|
11
|
-
...options,
|
|
12
|
-
strictSchema: false,
|
|
13
|
-
validateSchema: false,
|
|
14
|
-
allowUnionTypes: true,
|
|
15
|
-
uriResolver: fastUri
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
ajvFormats(ajvInstance)
|
|
19
|
-
|
|
20
|
-
ajvInstance.addKeyword({
|
|
21
|
-
keyword: 'fjs_type',
|
|
22
|
-
type: 'object',
|
|
23
|
-
errors: false,
|
|
24
|
-
validate: (type, date) => {
|
|
25
|
-
return date instanceof Date
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
return ajvInstance
|
|
30
|
-
}
|