fast-json-stringify 5.0.6 → 5.3.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.
@@ -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
File without changes
@@ -18,14 +18,6 @@ module.exports = class Serializer {
18
18
  }
19
19
  }
20
20
 
21
- asAny (i) {
22
- return JSON.stringify(i)
23
- }
24
-
25
- asNull () {
26
- return 'null'
27
- }
28
-
29
21
  asInteger (i) {
30
22
  if (typeof i === 'bigint') {
31
23
  return i.toString()
@@ -42,10 +34,6 @@ module.exports = class Serializer {
42
34
  }
43
35
  }
44
36
 
45
- asIntegerNullable (i) {
46
- return i === null ? 'null' : this.asInteger(i)
47
- }
48
-
49
37
  asNumber (i) {
50
38
  const num = Number(i)
51
39
  if (Number.isNaN(num)) {
@@ -57,52 +45,41 @@ module.exports = class Serializer {
57
45
  }
58
46
  }
59
47
 
60
- asNumberNullable (i) {
61
- return i === null ? 'null' : this.asNumber(i)
62
- }
63
-
64
48
  asBoolean (bool) {
65
49
  return bool && 'true' || 'false' // eslint-disable-line
66
50
  }
67
51
 
68
- asBooleanNullable (bool) {
69
- return bool === null ? 'null' : this.asBoolean(bool)
70
- }
71
-
72
- asDatetime (date) {
73
- const quotes = '"'
52
+ asDateTime (date) {
53
+ if (date === null) return '""'
74
54
  if (date instanceof Date) {
75
- return quotes + date.toISOString() + quotes
55
+ return '"' + date.toISOString() + '"'
76
56
  }
77
- return this.asString(date)
78
- }
79
-
80
- asDatetimeNullable (date) {
81
- return date === null ? 'null' : this.asDatetime(date)
57
+ if (typeof date === 'string') {
58
+ return '"' + date + '"'
59
+ }
60
+ throw new Error(`The value "${date}" cannot be converted to a date-time.`)
82
61
  }
83
62
 
84
63
  asDate (date) {
85
- const quotes = '"'
64
+ if (date === null) return '""'
86
65
  if (date instanceof Date) {
87
- return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + quotes
66
+ return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"'
88
67
  }
89
- return this.asString(date)
90
- }
91
-
92
- asDateNullable (date) {
93
- return date === null ? 'null' : this.asDate(date)
68
+ if (typeof date === 'string') {
69
+ return '"' + date + '"'
70
+ }
71
+ throw new Error(`The value "${date}" cannot be converted to a date.`)
94
72
  }
95
73
 
96
74
  asTime (date) {
97
- const quotes = '"'
75
+ if (date === null) return '""'
98
76
  if (date instanceof Date) {
99
- return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + quotes
77
+ return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"'
100
78
  }
101
- return this.asString(date)
102
- }
103
-
104
- asTimeNullable (date) {
105
- return date === null ? 'null' : this.asTime(date)
79
+ if (typeof date === 'string') {
80
+ return '"' + date + '"'
81
+ }
82
+ throw new Error(`The value "${date}" cannot be converted to a time.`)
106
83
  }
107
84
 
108
85
  asString (str) {
@@ -124,10 +101,6 @@ module.exports = class Serializer {
124
101
  }
125
102
  }
126
103
 
127
- asStringNullable (str) {
128
- return str === null ? 'null' : this.asString(str)
129
- }
130
-
131
104
  // magically escape strings for json
132
105
  // relying on their charCodeAt
133
106
  // everything below 32 needs JSON.stringify()
@@ -1,28 +1,28 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
3
 
4
- function buildStandaloneCode (options, ajvInstance, contextFunctionCode) {
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 = ajvInstance.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('ajv') !== -1
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 ajv = buildAjv(${JSON.stringify(options.ajv || {})})\n`
19
- for (const [id, schema] of Object.entries(ajvInstance.schemas)) {
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, 'ajv.js')).toString()
25
- buildAjvCode = buildAjvCode.replace("'use strict'", '').replace('module.exports = buildAjv', '')
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'
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.0.6",
3
+ "version": "5.3.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -11,7 +11,7 @@
11
11
  "benchmark": "node ./benchmark/bench-cmp-lib.js",
12
12
  "lint:fix": "standard --fix",
13
13
  "test:lint": "standard",
14
- "test:typescript": "tsc --project ./test/types/tsconfig.json",
14
+ "test:typescript": "tsc --project ./test/types/tsconfig.json && tsd",
15
15
  "test:unit": "tap -J test/*.test.js test/**/*.test.js",
16
16
  "test": "npm run test:lint && npm run test:unit && npm run test:typescript"
17
17
  },
@@ -33,25 +33,27 @@
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": "^2.4.0",
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",
46
46
  "standard": "^17.0.0",
47
47
  "tap": "^16.0.1",
48
+ "tsd": "^0.22.0",
48
49
  "typescript": "^4.0.2",
49
50
  "webpack": "^5.40.0"
50
51
  },
51
52
  "dependencies": {
53
+ "@fastify/deepmerge": "^1.0.0",
52
54
  "ajv": "^8.10.0",
53
55
  "ajv-formats": "^2.1.1",
54
- "deepmerge": "^4.2.2",
56
+ "fast-deep-equal": "^3.1.3",
55
57
  "fast-uri": "^2.1.0",
56
58
  "rfdc": "^1.2.0"
57
59
  },
@@ -60,5 +62,8 @@
60
62
  "schema-validator.js"
61
63
  ]
62
64
  },
63
- "runkitExampleFilename": "example.js"
65
+ "runkitExampleFilename": "./examples/example.js",
66
+ "tsd": {
67
+ "directory": "test/types"
68
+ }
64
69
  }
@@ -327,6 +327,31 @@ test('object array with anyOf and symbol', (t) => {
327
327
  t.equal(value, '[{"name":"name-0","option":"Foo"},{"name":"name-1","option":"Bar"}]')
328
328
  })
329
329
 
330
+ test('different arrays with same item schemas', (t) => {
331
+ t.plan(1)
332
+
333
+ const schema = {
334
+ type: 'object',
335
+ properties: {
336
+ array1: {
337
+ type: 'array',
338
+ items: [{ type: 'string' }],
339
+ additionalItems: false
340
+ },
341
+ array2: {
342
+ type: 'array',
343
+ items: { $ref: '#/properties/array1/items' },
344
+ additionalItems: true
345
+ }
346
+ }
347
+ }
348
+
349
+ const stringify = build(schema)
350
+ const data = { array1: ['bar'], array2: ['foo', 'bar'] }
351
+
352
+ t.equal(stringify(data), '{"array1":["bar"],"array2":["foo","bar"]}')
353
+ })
354
+
330
355
  const largeArray = new Array(2e4).fill({ a: 'test', b: 1 })
331
356
  buildTest({
332
357
  title: 'large array with default mechanism',
@@ -359,3 +359,16 @@ test('render a single quote as JSON', (t) => {
359
359
  t.equal(output, JSON.stringify(toStringify))
360
360
  t.ok(validate(JSON.parse(output)), 'valid schema')
361
361
  })
362
+
363
+ test('returns JSON.stringify if schema type is boolean', t => {
364
+ t.plan(1)
365
+
366
+ const schema = {
367
+ type: 'array',
368
+ items: true
369
+ }
370
+
371
+ const array = [1, true, 'test']
372
+ const stringify = build(schema)
373
+ t.equal(stringify(array), JSON.stringify(array))
374
+ })
@@ -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
- try {
60
- stringify({
61
- foo: { foo: 'baz' }
62
- })
63
- } catch (err) {
64
- t.match(err.message, /^Item .* does not match schema definition/, 'Given object has invalid const value')
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
  })