fast-json-stringify 7.1.0 → 7.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -5
- package/benchmark/bench-cmp-lib.js +63 -38
- package/benchmark/bench-fixtures.js +54 -0
- package/benchmark/bench-large-array.js +64 -0
- package/benchmark/bench-thread.js +4 -2
- package/benchmark/bench-utils.js +41 -0
- package/benchmark/fixtures/api-responses.js +266 -0
- package/index.js +251 -59
- package/lib/location.js +2 -1
- package/lib/serializer.js +33 -0
- package/package.json +1 -1
- package/test/allof.test.js +51 -0
- package/test/array.test.js +147 -0
- package/test/schema-depth.test.js +14 -2
package/test/array.test.js
CHANGED
|
@@ -23,6 +23,31 @@ test('error on invalid largeArrayMechanism', (t) => {
|
|
|
23
23
|
}), Error('Unsupported large array mechanism invalid'))
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
+
test('does not leak largeArrayMechanism across build() calls', (t) => {
|
|
27
|
+
t.plan(2)
|
|
28
|
+
|
|
29
|
+
const users = []
|
|
30
|
+
for (let i = 0; i < 100; i++) users.push({ name: 'user' + i, password: 'secret' + i })
|
|
31
|
+
|
|
32
|
+
const schema = {
|
|
33
|
+
type: 'array',
|
|
34
|
+
items: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
name: { type: 'string' }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// An unrelated build() opts into json-stringify for its own schema.
|
|
43
|
+
const optedIn = build({ type: 'array' }, { largeArrayMechanism: 'json-stringify', largeArraySize: 100 })
|
|
44
|
+
t.assert.equal(optedIn(users).includes('password'), true)
|
|
45
|
+
|
|
46
|
+
// A later build() without options must not inherit the mechanism.
|
|
47
|
+
const stringify = build(schema)
|
|
48
|
+
t.assert.equal(stringify(users).includes('password'), false)
|
|
49
|
+
})
|
|
50
|
+
|
|
26
51
|
function buildTest (schema, toStringify, options) {
|
|
27
52
|
test(`render a ${schema.title} as JSON`, (t) => {
|
|
28
53
|
t.plan(3)
|
|
@@ -686,3 +711,125 @@ buildTest({
|
|
|
686
711
|
largeArraySize: '10000',
|
|
687
712
|
largeArrayMechanism: 'default'
|
|
688
713
|
})
|
|
714
|
+
|
|
715
|
+
buildTest({
|
|
716
|
+
title: 'string array whose items declare const',
|
|
717
|
+
type: 'array',
|
|
718
|
+
items: { type: 'string', const: 'a' }
|
|
719
|
+
}, ['a', 'a'])
|
|
720
|
+
|
|
721
|
+
buildTest({
|
|
722
|
+
title: 'string array whose items declare allOf',
|
|
723
|
+
type: 'array',
|
|
724
|
+
items: { type: 'string', allOf: [{ type: 'string' }] }
|
|
725
|
+
}, ['a', 'b'])
|
|
726
|
+
|
|
727
|
+
buildTest({
|
|
728
|
+
title: 'string array whose items declare anyOf',
|
|
729
|
+
type: 'array',
|
|
730
|
+
items: { type: 'string', anyOf: [{ type: 'string' }] }
|
|
731
|
+
}, ['a', 'b'])
|
|
732
|
+
|
|
733
|
+
buildTest({
|
|
734
|
+
title: 'string array whose items declare oneOf',
|
|
735
|
+
type: 'array',
|
|
736
|
+
items: { type: 'string', oneOf: [{ type: 'string' }] }
|
|
737
|
+
}, ['a', 'b'])
|
|
738
|
+
|
|
739
|
+
buildTest({
|
|
740
|
+
title: 'string array whose items declare if',
|
|
741
|
+
type: 'array',
|
|
742
|
+
items: { type: 'string', if: { type: 'string' }, then: { type: 'string' } }
|
|
743
|
+
}, ['a', 'b'])
|
|
744
|
+
|
|
745
|
+
test('string array coerces a non-string item', (t) => {
|
|
746
|
+
const stringify = build({
|
|
747
|
+
type: 'array',
|
|
748
|
+
items: { type: 'string' }
|
|
749
|
+
}, { largeArraySize: 2 })
|
|
750
|
+
const date = new Date('2020-01-02T03:04:05.006Z')
|
|
751
|
+
|
|
752
|
+
t.assert.equal(
|
|
753
|
+
stringify([1, 'a', null, date, /re/]),
|
|
754
|
+
'["1","a","","2020-01-02T03:04:05.006Z","re"]'
|
|
755
|
+
)
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
// Large number/integer arrays are copied and handed to JSON.stringify. The
|
|
759
|
+
// first element that is not already the right type switches to the per-item
|
|
760
|
+
// loop from that index on. 3e5 elements is the largest fast-path cutoff on
|
|
761
|
+
// any supported Node release (see PRIMITIVE_FAST_PATH_MIN_LENGTH in index.js),
|
|
762
|
+
// so these always exercise the resume path.
|
|
763
|
+
const FAST_PATH_LENGTH = 3e5
|
|
764
|
+
|
|
765
|
+
for (const [type, bad, expected] of [
|
|
766
|
+
['integer', 1.9, '1'],
|
|
767
|
+
['integer', '7', '7'],
|
|
768
|
+
['integer', null, '0'],
|
|
769
|
+
['number', '7.5', '7.5'],
|
|
770
|
+
['number', null, '0'],
|
|
771
|
+
['number', true, '1']
|
|
772
|
+
]) {
|
|
773
|
+
for (const [position, index] of [
|
|
774
|
+
['start', 0],
|
|
775
|
+
['middle', FAST_PATH_LENGTH >> 1],
|
|
776
|
+
['end', FAST_PATH_LENGTH - 1]
|
|
777
|
+
]) {
|
|
778
|
+
test(`large ${type} array falls back to the loop on ${JSON.stringify(bad)} at the ${position}`, (t) => {
|
|
779
|
+
const stringify = build({ type: 'array', items: { type } })
|
|
780
|
+
const input = Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i - 1000)
|
|
781
|
+
input[index] = bad
|
|
782
|
+
|
|
783
|
+
const reference = Array.from(input, (v, i) => i === index ? expected : String(v))
|
|
784
|
+
t.assert.equal(stringify(input), '[' + reference.join(',') + ']')
|
|
785
|
+
})
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
test('large integer array applies the rounding option after the fallback', (t) => {
|
|
790
|
+
const stringify = build({ type: 'array', items: { type: 'integer' } }, { rounding: 'ceil' })
|
|
791
|
+
const input = Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i)
|
|
792
|
+
input[10] = 1.1
|
|
793
|
+
input[FAST_PATH_LENGTH - 1] = 2.1
|
|
794
|
+
|
|
795
|
+
const output = stringify(input)
|
|
796
|
+
t.assert.equal(output.slice(0, 23), '[0,1,2,3,4,5,6,7,8,9,2,')
|
|
797
|
+
t.assert.equal(output.slice(-3), ',3]')
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
test('large number array still throws on a non-numeric string after the fallback', (t) => {
|
|
801
|
+
const stringify = build({ type: 'array', items: { type: 'number' } })
|
|
802
|
+
const input = Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i / 10)
|
|
803
|
+
input[FAST_PATH_LENGTH >> 1] = 'not a number'
|
|
804
|
+
|
|
805
|
+
t.assert.throws(() => stringify(input), new Error('The value "not a number" cannot be converted to a number.'))
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
// A real NaN is typeof 'number', so only the `v === v` half of the fast-path
|
|
809
|
+
// condition rejects it. Check it at every position the copy loop can stop at.
|
|
810
|
+
for (const [position, index] of [['start', 0], ['middle', FAST_PATH_LENGTH >> 1], ['end', FAST_PATH_LENGTH - 1]]) {
|
|
811
|
+
test(`large number array still throws on NaN at the ${position} after the fallback`, (t) => {
|
|
812
|
+
const stringify = build({ type: 'array', items: { type: 'number' } })
|
|
813
|
+
const input = Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i / 10)
|
|
814
|
+
input[index] = NaN
|
|
815
|
+
|
|
816
|
+
t.assert.throws(() => stringify(input), new Error('The value "NaN" cannot be converted to a number.'))
|
|
817
|
+
})
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
test('large number array reads each element once across the fallback', (t) => {
|
|
821
|
+
const stringify = build({ type: 'array', items: { type: 'number' } })
|
|
822
|
+
const input = Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i)
|
|
823
|
+
const reads = []
|
|
824
|
+
for (const index of [0, 1, FAST_PATH_LENGTH >> 1, FAST_PATH_LENGTH - 1]) {
|
|
825
|
+
Object.defineProperty(input, index, {
|
|
826
|
+
get () {
|
|
827
|
+
reads.push(index)
|
|
828
|
+
return index === 1 ? '1' : index
|
|
829
|
+
}
|
|
830
|
+
})
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
t.assert.equal(stringify(input), JSON.stringify(Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i)))
|
|
834
|
+
t.assert.deepEqual(reads, [0, 1, FAST_PATH_LENGTH >> 1, FAST_PATH_LENGTH - 1])
|
|
835
|
+
})
|
|
@@ -110,11 +110,23 @@ test('rejects invalid maximum schema depths', (t) => {
|
|
|
110
110
|
test('checks schema dependencies without treating property lists as schemas', (t) => {
|
|
111
111
|
t.assert.doesNotThrow(() => build({
|
|
112
112
|
type: 'object',
|
|
113
|
+
definitions: {
|
|
114
|
+
valueProp: { type: 'string' },
|
|
115
|
+
any: true
|
|
116
|
+
},
|
|
117
|
+
// Include a $ref so recursive-schema detection runs and walks dependency
|
|
118
|
+
// schema forms (property-list dependencies must still be ignored). Boolean
|
|
119
|
+
// schemas must also be visited so traverse's non-object early-return stays covered.
|
|
120
|
+
properties: {
|
|
121
|
+
link: { $ref: '#/definitions/valueProp' },
|
|
122
|
+
flag: { $ref: '#/definitions/any' },
|
|
123
|
+
open: true
|
|
124
|
+
},
|
|
113
125
|
dependencies: {
|
|
114
|
-
enabled: { properties: { value: {
|
|
126
|
+
enabled: { properties: { value: { $ref: '#/definitions/valueProp' } } },
|
|
115
127
|
value: ['enabled']
|
|
116
128
|
}
|
|
117
|
-
}, { maxDepth:
|
|
129
|
+
}, { maxDepth: 3 }))
|
|
118
130
|
})
|
|
119
131
|
|
|
120
132
|
test('checks external schemas before validating them recursively', (t) => {
|