fast-json-stringify 5.12.0 → 5.13.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/README.md +18 -0
- package/benchmark/bench.js +24 -0
- package/index.js +17 -1
- package/lib/serializer.js +8 -20
- package/package.json +1 -1
- package/test/basic.test.js +6 -0
- package/test/string.test.js +34 -0
- package/types/index.d.ts +5 -2
- package/types/index.test-d.ts +79 -57
package/README.md
CHANGED
|
@@ -630,6 +630,24 @@ integer-like values, such as:
|
|
|
630
630
|
- `'2e4'` - _note this will be converted to `2`, not `20000`_
|
|
631
631
|
- `1.5` - _note this will be converted to `1`_
|
|
632
632
|
|
|
633
|
+
<a name="unsafe"></a>
|
|
634
|
+
#### Unsafe string
|
|
635
|
+
By default, the library escapes all strings. With the 'unsafe' format, the string isn't escaped. This has a potentially dangerous security issue. You can use it only if you are sure that your data doesn't need escaping. The advantage is a significant performance improvement.
|
|
636
|
+
|
|
637
|
+
Example:
|
|
638
|
+
```javascript
|
|
639
|
+
const stringify = fastJson({
|
|
640
|
+
title: 'Example Schema',
|
|
641
|
+
type: 'object',
|
|
642
|
+
properties: {
|
|
643
|
+
'code': {
|
|
644
|
+
type: 'string',
|
|
645
|
+
format 'unsafe'
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
})
|
|
649
|
+
```
|
|
650
|
+
|
|
633
651
|
##### Benchmarks
|
|
634
652
|
|
|
635
653
|
For reference, here goes some benchmarks for comparison over the three
|
package/benchmark/bench.js
CHANGED
|
@@ -47,6 +47,14 @@ const benchmarks = [
|
|
|
47
47
|
},
|
|
48
48
|
input: 'hello world'
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
name: 'unsafe short string',
|
|
52
|
+
schema: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
format: 'unsafe'
|
|
55
|
+
},
|
|
56
|
+
input: 'hello world'
|
|
57
|
+
},
|
|
50
58
|
{
|
|
51
59
|
name: 'short string with double quote',
|
|
52
60
|
schema: {
|
|
@@ -61,6 +69,14 @@ const benchmarks = [
|
|
|
61
69
|
},
|
|
62
70
|
input: longSimpleString
|
|
63
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: 'unsafe long string without double quotes',
|
|
74
|
+
schema: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
format: 'unsafe'
|
|
77
|
+
},
|
|
78
|
+
input: longSimpleString
|
|
79
|
+
},
|
|
64
80
|
{
|
|
65
81
|
name: 'long string',
|
|
66
82
|
schema: {
|
|
@@ -68,6 +84,14 @@ const benchmarks = [
|
|
|
68
84
|
},
|
|
69
85
|
input: longString
|
|
70
86
|
},
|
|
87
|
+
{
|
|
88
|
+
name: 'unsafe long string',
|
|
89
|
+
schema: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
format: 'unsafe'
|
|
92
|
+
},
|
|
93
|
+
input: longString
|
|
94
|
+
},
|
|
71
95
|
{
|
|
72
96
|
name: 'number',
|
|
73
97
|
schema: {
|
package/index.js
CHANGED
|
@@ -725,8 +725,24 @@ function buildSingleTypeSerializer (context, location, input) {
|
|
|
725
725
|
return `json += serializer.asDate(${input})`
|
|
726
726
|
} else if (schema.format === 'time') {
|
|
727
727
|
return `json += serializer.asTime(${input})`
|
|
728
|
+
} else if (schema.format === 'unsafe') {
|
|
729
|
+
return `json += serializer.asUnsafeString(${input})`
|
|
728
730
|
} else {
|
|
729
|
-
return `
|
|
731
|
+
return `
|
|
732
|
+
if (typeof ${input} !== 'string') {
|
|
733
|
+
if (${input} === null) {
|
|
734
|
+
json += '""'
|
|
735
|
+
} else if (${input} instanceof Date) {
|
|
736
|
+
json += '"' + ${input}.toISOString() + '"'
|
|
737
|
+
} else if (${input} instanceof RegExp) {
|
|
738
|
+
json += serializer.asString(${input}.source)
|
|
739
|
+
} else {
|
|
740
|
+
json += serializer.asString(${input}.toString())
|
|
741
|
+
}
|
|
742
|
+
} else {
|
|
743
|
+
json += serializer.asString(${input})
|
|
744
|
+
}
|
|
745
|
+
`
|
|
730
746
|
}
|
|
731
747
|
}
|
|
732
748
|
case 'integer':
|
package/lib/serializer.js
CHANGED
|
@@ -99,29 +99,20 @@ module.exports = class Serializer {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
asString (str) {
|
|
102
|
-
if (typeof str !== 'string') {
|
|
103
|
-
if (str === null) {
|
|
104
|
-
return '""'
|
|
105
|
-
}
|
|
106
|
-
if (str instanceof Date) {
|
|
107
|
-
return '"' + str.toISOString() + '"'
|
|
108
|
-
}
|
|
109
|
-
if (str instanceof RegExp) {
|
|
110
|
-
str = str.source
|
|
111
|
-
} else {
|
|
112
|
-
str = str.toString()
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
102
|
if (str.length < 42) {
|
|
117
103
|
return this.asStringSmall(str)
|
|
118
|
-
} else if (STR_ESCAPE.test(str) === false) {
|
|
104
|
+
} else if (str.length < 5000 && STR_ESCAPE.test(str) === false) {
|
|
105
|
+
// Only use the regular expression for shorter input. The overhead is otherwise too much.
|
|
119
106
|
return '"' + str + '"'
|
|
120
107
|
} else {
|
|
121
108
|
return JSON.stringify(str)
|
|
122
109
|
}
|
|
123
110
|
}
|
|
124
111
|
|
|
112
|
+
asUnsafeString (str) {
|
|
113
|
+
return '"' + str + '"'
|
|
114
|
+
}
|
|
115
|
+
|
|
125
116
|
// magically escape strings for json
|
|
126
117
|
// relying on their charCodeAt
|
|
127
118
|
// everything below 32 needs JSON.stringify()
|
|
@@ -137,11 +128,8 @@ module.exports = class Serializer {
|
|
|
137
128
|
// eslint-disable-next-line
|
|
138
129
|
for (var i = 0; i < len; i++) {
|
|
139
130
|
point = str.charCodeAt(i)
|
|
140
|
-
if (point < 32) {
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
if (point >= 0xD800 && point <= 0xDFFF) {
|
|
144
|
-
// The current character is a surrogate.
|
|
131
|
+
if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
|
|
132
|
+
// The current character is non-printable characters or a surrogate.
|
|
145
133
|
return JSON.stringify(str)
|
|
146
134
|
}
|
|
147
135
|
if (
|
package/package.json
CHANGED
package/test/basic.test.js
CHANGED
package/test/string.test.js
CHANGED
|
@@ -48,3 +48,37 @@ test('serialize long string', (t) => {
|
|
|
48
48
|
t.equal(output, `"${new Array(2e4).fill('\\u0000').join('')}"`)
|
|
49
49
|
t.equal(JSON.parse(output), input)
|
|
50
50
|
})
|
|
51
|
+
|
|
52
|
+
test('unsafe string', (t) => {
|
|
53
|
+
t.plan(2)
|
|
54
|
+
|
|
55
|
+
const schema = {
|
|
56
|
+
type: 'string',
|
|
57
|
+
format: 'unsafe'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const input = 'abcd'
|
|
61
|
+
const stringify = build(schema)
|
|
62
|
+
const output = stringify(input)
|
|
63
|
+
|
|
64
|
+
t.equal(output, `"${input}"`)
|
|
65
|
+
t.equal(JSON.parse(output), input)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('unsafe unescaped string', (t) => {
|
|
69
|
+
t.plan(2)
|
|
70
|
+
|
|
71
|
+
const schema = {
|
|
72
|
+
type: 'string',
|
|
73
|
+
format: 'unsafe'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const input = 'abcd "abcd"'
|
|
77
|
+
const stringify = build(schema)
|
|
78
|
+
const output = stringify(input)
|
|
79
|
+
|
|
80
|
+
t.equal(output, `"${input}"`)
|
|
81
|
+
t.throws(function () {
|
|
82
|
+
JSON.parse(output)
|
|
83
|
+
})
|
|
84
|
+
})
|
package/types/index.d.ts
CHANGED
|
@@ -208,6 +208,9 @@ interface StandaloneOption extends build.Options {
|
|
|
208
208
|
mode: 'standalone'
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
type StringCoercible = string | Date | RegExp;
|
|
212
|
+
type IntegerCoercible = number | BigInt;
|
|
213
|
+
|
|
211
214
|
/**
|
|
212
215
|
* Build a stringify function using a schema of the documents that should be stringified
|
|
213
216
|
* @param schema The schema used to stringify values
|
|
@@ -217,8 +220,8 @@ declare function build(schema: build.AnySchema, options: DebugOption): { code: s
|
|
|
217
220
|
declare function build(schema: build.AnySchema, options: DeprecateDebugOption): { code: string, ajv: Ajv };
|
|
218
221
|
declare function build(schema: build.AnySchema, options: StandaloneOption): string;
|
|
219
222
|
declare function build(schema: build.AnySchema, options?: build.Options): <TDoc = any>(doc: TDoc) => any;
|
|
220
|
-
declare function build(schema: build.StringSchema, options?: build.Options): <TDoc extends
|
|
221
|
-
declare function build(schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): <TDoc extends
|
|
223
|
+
declare function build(schema: build.StringSchema, options?: build.Options): <TDoc extends StringCoercible = StringCoercible>(doc: TDoc) => string;
|
|
224
|
+
declare function build(schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): <TDoc extends IntegerCoercible = IntegerCoercible>(doc: TDoc) => string;
|
|
222
225
|
declare function build(schema: build.NullSchema, options?: build.Options): <TDoc extends null = null>(doc: TDoc) => "null";
|
|
223
226
|
declare function build(schema: build.BooleanSchema, options?: build.Options): <TDoc extends boolean = boolean>(doc: TDoc) => string;
|
|
224
227
|
declare function build(schema: build.ArraySchema | build.TupleSchema, options?: build.Options): <TDoc extends any[]= any[]>(doc: TDoc) => string;
|
package/types/index.test-d.ts
CHANGED
|
@@ -3,61 +3,62 @@ import build, { restore, Schema, validLargeArrayMechanisms } from '..'
|
|
|
3
3
|
import { expectError, expectType } from 'tsd'
|
|
4
4
|
|
|
5
5
|
// Number schemas
|
|
6
|
-
|
|
6
|
+
build({
|
|
7
7
|
type: 'number'
|
|
8
|
-
}
|
|
9
|
-
|
|
8
|
+
})(25)
|
|
9
|
+
build({
|
|
10
10
|
type: 'integer'
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
})(-5)
|
|
12
|
+
build({
|
|
13
|
+
type: 'integer'
|
|
14
|
+
})(5n)
|
|
15
15
|
|
|
16
|
-
build(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
build(
|
|
20
|
-
|
|
16
|
+
build({
|
|
17
|
+
type: 'number'
|
|
18
|
+
}, { rounding: 'ceil' })
|
|
19
|
+
build({
|
|
20
|
+
type: 'number'
|
|
21
|
+
}, { rounding: 'floor' })
|
|
22
|
+
build({
|
|
23
|
+
type: 'number'
|
|
24
|
+
}, { rounding: 'round' })
|
|
25
|
+
build({
|
|
26
|
+
type: 'number'
|
|
27
|
+
}, { rounding: 'trunc' })
|
|
28
|
+
expectError(build({
|
|
29
|
+
type: 'number'
|
|
30
|
+
}, { rounding: 'invalid' }))
|
|
21
31
|
|
|
22
32
|
// String schema
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
build(schema3)('foobar')
|
|
33
|
+
build({
|
|
34
|
+
type: 'string'
|
|
35
|
+
})('foobar')
|
|
28
36
|
|
|
29
37
|
// Boolean schema
|
|
30
|
-
|
|
38
|
+
build({
|
|
31
39
|
type: 'boolean'
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
build(schema4)(true)
|
|
40
|
+
})(true)
|
|
35
41
|
|
|
36
42
|
// Null schema
|
|
37
|
-
|
|
43
|
+
build({
|
|
38
44
|
type: 'null'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
build(schema5)(null)
|
|
45
|
+
})(null)
|
|
42
46
|
|
|
43
47
|
// Array schemas
|
|
44
|
-
|
|
48
|
+
build({
|
|
45
49
|
type: 'array',
|
|
46
50
|
items: { type: 'number' }
|
|
47
|
-
}
|
|
48
|
-
|
|
51
|
+
})([25])
|
|
52
|
+
build({
|
|
49
53
|
type: 'array',
|
|
50
54
|
items: [{ type: 'string'}, {type: 'integer'}]
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
build(schema6)([25])
|
|
54
|
-
build(schema7)(['hello', 42])
|
|
55
|
+
})(['hello', 42])
|
|
55
56
|
|
|
56
57
|
// Object schemas
|
|
57
|
-
|
|
58
|
+
build({
|
|
58
59
|
type: 'object'
|
|
59
|
-
}
|
|
60
|
-
|
|
60
|
+
})({})
|
|
61
|
+
build({
|
|
61
62
|
type: 'object',
|
|
62
63
|
properties: {
|
|
63
64
|
foo: { type: 'string' },
|
|
@@ -70,14 +71,24 @@ const schema9: Schema = {
|
|
|
70
71
|
additionalProperties: {
|
|
71
72
|
type: 'boolean'
|
|
72
73
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
})({ foo: 'bar' })
|
|
75
|
+
build({
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
foo: { type: 'string' },
|
|
79
|
+
bar: { type: 'integer' }
|
|
80
|
+
},
|
|
81
|
+
required: ['foo'],
|
|
82
|
+
patternProperties: {
|
|
83
|
+
'baz*': { type: 'null' }
|
|
84
|
+
},
|
|
85
|
+
additionalProperties: {
|
|
86
|
+
type: 'boolean'
|
|
87
|
+
}
|
|
88
|
+
}, { rounding: 'floor' })({ foo: 'bar' })
|
|
78
89
|
|
|
79
90
|
// Reference schemas
|
|
80
|
-
|
|
91
|
+
build({
|
|
81
92
|
title: 'Example Schema',
|
|
82
93
|
definitions: {
|
|
83
94
|
num: {
|
|
@@ -109,12 +120,10 @@ const schema10: Schema = {
|
|
|
109
120
|
additionalProperties: {
|
|
110
121
|
$ref: '#/definitions/def'
|
|
111
122
|
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
build(schema10)({ nickname: '', num: { int: 5 }, other: null })
|
|
123
|
+
})({ nickname: '', num: { int: 5 }, other: null })
|
|
115
124
|
|
|
116
125
|
// Conditional/Combined schemas
|
|
117
|
-
|
|
126
|
+
build({
|
|
118
127
|
title: 'Conditional/Combined Schema',
|
|
119
128
|
type: 'object',
|
|
120
129
|
properties: {
|
|
@@ -140,24 +149,37 @@ const schema11: Schema = {
|
|
|
140
149
|
somethingElse: { type: 'null' }
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
build(schema11)({ something: 'a string', somethingElse: 42 })
|
|
152
|
+
})({ something: 'a string', somethingElse: 42 })
|
|
146
153
|
|
|
147
154
|
// String schema with format
|
|
148
|
-
|
|
155
|
+
|
|
156
|
+
build({
|
|
149
157
|
type: 'string',
|
|
150
158
|
format: 'date-time'
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
build(schema12)(new Date())
|
|
159
|
+
})(new Date())
|
|
154
160
|
|
|
161
|
+
/*
|
|
162
|
+
This overload doesn't work yet -
|
|
163
|
+
TypeScript chooses the generic for the schema
|
|
164
|
+
before it chooses the overload for the options
|
|
165
|
+
parameter.
|
|
155
166
|
let str: string, ajv: Ajv
|
|
156
|
-
str = build(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
ajv = build(
|
|
160
|
-
|
|
167
|
+
str = build({
|
|
168
|
+
type: 'number'
|
|
169
|
+
}, { debugMode: true }).code
|
|
170
|
+
ajv = build({
|
|
171
|
+
type: 'number'
|
|
172
|
+
}, { debugMode: true }).ajv
|
|
173
|
+
str = build({
|
|
174
|
+
type: 'number'
|
|
175
|
+
}, { mode: 'debug' }).code
|
|
176
|
+
ajv = build({
|
|
177
|
+
type: 'number'
|
|
178
|
+
}, { mode: 'debug' }).ajv
|
|
179
|
+
str = build({
|
|
180
|
+
type: 'number'
|
|
181
|
+
}, { mode: 'standalone' })
|
|
182
|
+
*/
|
|
161
183
|
|
|
162
184
|
const debugCompiled = build({
|
|
163
185
|
title: 'default string',
|