fast-json-stringify 5.7.0 → 5.9.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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Set default behavior to automatically convert line endings
2
+ * text=auto eol=lf
@@ -1,4 +1,4 @@
1
- name: Benchmark
1
+ name: Benchmark PR
2
2
 
3
3
  on:
4
4
  pull_request_target:
@@ -11,25 +11,18 @@ jobs:
11
11
  permissions:
12
12
  contents: read
13
13
  outputs:
14
- PR-BENCH-14: ${{ steps.benchmark-pr.outputs.BENCH_RESULT14 }}
15
- PR-BENCH-16: ${{ steps.benchmark-pr.outputs.BENCH_RESULT16 }}
16
- PR-BENCH-18: ${{ steps.benchmark-pr.outputs.BENCH_RESULT18 }}
17
- MAIN-BENCH-14: ${{ steps.benchmark-main.outputs.BENCH_RESULT14 }}
18
- MAIN-BENCH-16: ${{ steps.benchmark-main.outputs.BENCH_RESULT16 }}
19
- MAIN-BENCH-18: ${{ steps.benchmark-main.outputs.BENCH_RESULT18 }}
20
- strategy:
21
- matrix:
22
- node-version: [14, 16, 18]
14
+ PR-BENCH: ${{ steps.benchmark-pr.outputs.BENCH_RESULT }}
15
+ MASTER-BENCH: ${{ steps.benchmark-master.outputs.BENCH_RESULT }}
23
16
  steps:
24
- - uses: actions/checkout@v3
17
+ - uses: actions/checkout@v4
25
18
  with:
26
19
  persist-credentials: false
27
20
  ref: ${{github.event.pull_request.head.sha}}
28
21
  repository: ${{github.event.pull_request.head.repo.full_name}}
29
22
 
30
- - uses: actions/setup-node@v3
23
+ - uses: actions/setup-node@v4
31
24
  with:
32
- node-version: ${{ matrix.node-version }}
25
+ node-version: 18
33
26
 
34
27
  - name: Install
35
28
  run: |
@@ -38,15 +31,15 @@ jobs:
38
31
  - name: Run benchmark
39
32
  id: benchmark-pr
40
33
  run: |
41
- npm run --silent benchmark > ./bench-result
34
+ npm run --silent bench > ./bench-result
42
35
  content=$(cat ./bench-result)
43
36
  content="${content//'%'/'%25'}"
44
37
  content="${content//$'\n'/'%0A'}"
45
38
  content="${content//$'\r'/'%0D'}"
46
- echo "::set-output name=BENCH_RESULT${{matrix.node-version}}::$content"
39
+ echo "::set-output name=BENCH_RESULT::$content"
47
40
 
48
- # main benchmark
49
- - uses: actions/checkout@v3
41
+ # master benchmark
42
+ - uses: actions/checkout@v4
50
43
  with:
51
44
  ref: 'master'
52
45
 
@@ -57,14 +50,15 @@ jobs:
57
50
  - name: Run benchmark
58
51
  id: benchmark-master
59
52
  run: |
60
- npm run --silent benchmark > ./bench-result
53
+ npm run --silent bench > ./bench-result
61
54
  content=$(cat ./bench-result)
62
55
  content="${content//'%'/'%25'}"
63
56
  content="${content//$'\n'/'%0A'}"
64
57
  content="${content//$'\r'/'%0D'}"
65
- echo "::set-output name=BENCH_RESULT${{matrix.node-version}}::$content"
58
+ echo "::set-output name=BENCH_RESULT::$content"
66
59
 
67
60
  output-benchmark:
61
+ if: "always()"
68
62
  needs: [benchmark]
69
63
  runs-on: ubuntu-latest
70
64
  permissions:
@@ -75,38 +69,13 @@ jobs:
75
69
  with:
76
70
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
77
71
  message: |
78
- **Node**: 14
79
72
  **PR**:
80
73
  ```
81
- ${{ needs.benchmark.outputs.PR-BENCH-14 }}
74
+ ${{ needs.benchmark.outputs.PR-BENCH }}
82
75
  ```
83
- **MAIN**:
76
+ **MASTER**:
84
77
  ```
85
- ${{ needs.benchmark.outputs.MAIN-BENCH-14 }}
86
- ```
87
-
88
- ---
89
-
90
- **Node**: 16
91
- **PR**:
92
- ```
93
- ${{ needs.benchmark.outputs.PR-BENCH-16 }}
94
- ```
95
- **MAIN**:
96
- ```
97
- ${{ needs.benchmark.outputs.MAIN-BENCH-16 }}
98
- ```
99
-
100
- ---
101
-
102
- **Node**: 18
103
- **PR**:
104
- ```
105
- ${{ needs.benchmark.outputs.PR-BENCH-18 }}
106
- ```
107
- **MAIN**:
108
- ```
109
- ${{ needs.benchmark.outputs.MAIN-BENCH-18 }}
78
+ ${{ needs.benchmark.outputs.MASTER-BENCH }}
110
79
  ```
111
80
 
112
81
  - uses: actions-ecosystem/action-remove-labels@v1
@@ -2,6 +2,11 @@ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
+ branches:
6
+ - main
7
+ - master
8
+ - next
9
+ - 'v*'
5
10
  paths-ignore:
6
11
  - 'docs/**'
7
12
  - '*.md'
package/README.md CHANGED
@@ -692,7 +692,7 @@ The standalone mode is used to compile the code that can be directly run by `nod
692
692
  itself. You need to have `fast-json-stringify` installed for the standalone code to work.
693
693
 
694
694
  ```js
695
- const fs = require('fs')
695
+ const fs = require('node:fs')
696
696
  const code = fastJson({
697
697
  title: 'default string',
698
698
  type: 'object',
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { spawn } = require('child_process')
3
+ const { spawn } = require('node:child_process')
4
4
 
5
5
  const cliSelect = require('cli-select')
6
6
  const simpleGit = require('simple-git')
@@ -1,9 +1,9 @@
1
1
  'use strict'
2
2
 
3
- const { workerData: benchmark, parentPort } = require('worker_threads')
3
+ const { workerData: benchmark, parentPort } = require('node:worker_threads')
4
4
 
5
5
  const Benchmark = require('benchmark')
6
- Benchmark.options.minSamples = 500
6
+ Benchmark.options.minSamples = 100
7
7
 
8
8
  const suite = Benchmark.Suite()
9
9
 
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const path = require('path')
4
- const { Worker } = require('worker_threads')
3
+ const path = require('node:path')
4
+ const { Worker } = require('node:worker_threads')
5
5
 
6
6
  const BENCH_THREAD_PATH = path.join(__dirname, 'bench-thread.js')
7
7
 
@@ -18,6 +18,11 @@ function getRandomInt (max) {
18
18
  return Math.floor(Math.random() * max)
19
19
  }
20
20
 
21
+ let longSimpleString = ''
22
+ for (let i = 0; i < LONG_STRING_LENGTH; i++) {
23
+ longSimpleString += i
24
+ }
25
+
21
26
  let longString = ''
22
27
  for (let i = 0; i < LONG_STRING_LENGTH; i++) {
23
28
  longString += i
@@ -42,6 +47,20 @@ const benchmarks = [
42
47
  },
43
48
  input: 'hello world'
44
49
  },
50
+ {
51
+ name: 'short string with double quote',
52
+ schema: {
53
+ type: 'string'
54
+ },
55
+ input: 'hello " world'
56
+ },
57
+ {
58
+ name: 'long string without double quotes',
59
+ schema: {
60
+ type: 'string'
61
+ },
62
+ input: longSimpleString
63
+ },
45
64
  {
46
65
  name: 'long string',
47
66
  schema: {
@@ -235,6 +254,28 @@ const benchmarks = [
235
254
  },
236
255
  input: { firstName: 'Max', lastName: 'Power', age: 22 }
237
256
  },
257
+ {
258
+ name: 'simple object with required fields',
259
+ schema: {
260
+ title: 'Example Schema',
261
+ type: 'object',
262
+ properties: {
263
+ firstName: {
264
+ type: 'string'
265
+ },
266
+ lastName: {
267
+ type: ['string', 'null']
268
+ },
269
+ age: {
270
+ description: 'Age in years',
271
+ type: 'integer',
272
+ minimum: 0
273
+ }
274
+ },
275
+ required: ['firstName', 'lastName', 'age']
276
+ },
277
+ input: { firstName: 'Max', lastName: 'Power', age: 22 }
278
+ },
238
279
  {
239
280
  name: 'object with const string property',
240
281
  schema: {
@@ -3,8 +3,8 @@
3
3
  const Ajv = require('ajv')
4
4
  const standaloneCode = require('ajv/dist/standalone').default
5
5
  const ajvFormats = require('ajv-formats')
6
- const fs = require('fs')
7
- const path = require('path')
6
+ const fs = require('node:fs')
7
+ const path = require('node:path')
8
8
 
9
9
  const ajv = new Ajv({
10
10
  addUsedSchema: false,
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const fastJson = require('..')
2
4
  const stringify = fastJson({
3
5
  title: 'Example Schema',
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const http = require('http')
3
+ const http = require('node:http')
4
4
 
5
5
  const stringify = require('fast-json-stringify')({
6
6
  type: 'object',
package/index.js CHANGED
@@ -4,14 +4,16 @@
4
4
 
5
5
  const merge = require('@fastify/deepmerge')()
6
6
  const clone = require('rfdc')({ proto: true })
7
- const { randomUUID } = require('crypto')
7
+ const { randomUUID } = require('node:crypto')
8
+ const { RefResolver } = require('json-schema-ref-resolver')
8
9
 
9
10
  const validate = require('./lib/schema-validator')
10
11
  const Serializer = require('./lib/serializer')
11
12
  const Validator = require('./lib/validator')
12
- const RefResolver = require('./lib/ref-resolver')
13
13
  const Location = require('./lib/location')
14
14
 
15
+ const SINGLE_TICK = /'/g
16
+
15
17
  let largeArraySize = 2e4
16
18
  let largeArrayMechanism = 'default'
17
19
 
@@ -53,8 +55,7 @@ function resolveRef (context, location, ref) {
53
55
  const jsonPointer = ref.slice(hashIndex) || '#'
54
56
 
55
57
  const schema = context.refResolver.getSchema(schemaId, jsonPointer)
56
-
57
- if (schema === undefined) {
58
+ if (schema === null) {
58
59
  throw new Error(`Cannot find reference "${ref}"`)
59
60
  }
60
61
 
@@ -66,6 +67,13 @@ function resolveRef (context, location, ref) {
66
67
  return newLocation
67
68
  }
68
69
 
70
+ function getSchemaId (schema, rootSchemaId) {
71
+ if (schema.$id && schema.$id.charAt(0) !== '#') {
72
+ return schema.$id
73
+ }
74
+ return rootSchemaId
75
+ }
76
+
69
77
  function build (schema, options) {
70
78
  isValidSchema(schema)
71
79
 
@@ -76,17 +84,25 @@ function build (schema, options) {
76
84
  functionsCounter: 0,
77
85
  functionsNamesBySchema: new Map(),
78
86
  options,
87
+ wrapObjects: true,
79
88
  refResolver: new RefResolver(),
80
89
  rootSchemaId: schema.$id || randomUUID(),
81
90
  validatorSchemasIds: new Set()
82
91
  }
83
92
 
84
- context.refResolver.addSchema(schema, context.rootSchemaId)
93
+ const schemaId = getSchemaId(schema, context.rootSchemaId)
94
+ if (!context.refResolver.hasSchema(schemaId)) {
95
+ context.refResolver.addSchema(schema, context.rootSchemaId)
96
+ }
85
97
 
86
98
  if (options.schema) {
87
- for (const key of Object.keys(options.schema)) {
88
- isValidSchema(options.schema[key], key)
89
- context.refResolver.addSchema(options.schema[key], key)
99
+ for (const key in options.schema) {
100
+ const schema = options.schema[key]
101
+ const schemaId = getSchemaId(schema, key)
102
+ if (!context.refResolver.hasSchema(schemaId)) {
103
+ isValidSchema(schema, key)
104
+ context.refResolver.addSchema(schema, key)
105
+ }
90
106
  }
91
107
  }
92
108
 
@@ -123,7 +139,7 @@ function build (schema, options) {
123
139
 
124
140
  // If we have only the invocation of the 'anonymous0' function, we would
125
141
  // basically just wrap the 'anonymous0' function in the 'main' function and
126
- // and the overhead of the intermediate variabe 'json'. We can avoid the
142
+ // and the overhead of the intermediate variable 'json'. We can avoid the
127
143
  // wrapping and the unnecessary memory allocation by aliasing 'anonymous0' to
128
144
  // 'main'
129
145
  if (code === 'json += anonymous0(input)') {
@@ -297,59 +313,96 @@ function buildExtraObjectPropertiesSerializer (context, location) {
297
313
  }
298
314
 
299
315
  function buildInnerObject (context, location) {
316
+ let code = ''
300
317
  const schema = location.schema
301
318
  const required = schema.required || []
302
319
 
303
- let code = ''
304
-
305
320
  const propertiesLocation = location.getPropertyLocation('properties')
306
- Object.keys(schema.properties || {}).forEach((key) => {
307
- let propertyLocation = propertiesLocation.getPropertyLocation(key)
308
- if (propertyLocation.schema.$ref) {
309
- propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref)
310
- }
311
321
 
312
- const sanitized = JSON.stringify(key)
322
+ const requiredWithDefault = []
323
+ const requiredWithoutDefault = []
324
+ if (schema.properties) {
325
+ for (const key of Object.keys(schema.properties)) {
326
+ if (required.indexOf(key) === -1) {
327
+ continue
328
+ }
329
+ let propertyLocation = propertiesLocation.getPropertyLocation(key)
330
+ if (propertyLocation.schema.$ref) {
331
+ propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref)
332
+ }
313
333
 
314
- // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
315
- // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
334
+ const sanitizedKey = JSON.stringify(key)
316
335
 
317
- code += `
318
- if (obj[${sanitized}] !== undefined) {
319
- ${addComma}
320
- json += ${JSON.stringify(sanitized + ':')}
321
- `
336
+ // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
337
+ // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
338
+ const defaultValue = propertyLocation.schema.default
339
+ if (defaultValue === undefined) {
340
+ code += `if (obj[${sanitizedKey}] === undefined) throw new Error('${sanitizedKey} is required!')\n`
341
+ requiredWithoutDefault.push(key)
342
+ }
343
+ requiredWithDefault.push(key)
344
+ }
345
+ }
322
346
 
323
- code += buildValue(context, propertyLocation, `obj[${sanitized}]`)
347
+ // handle extraneous required fields
348
+ for (const requiredProperty of required) {
349
+ if (requiredWithDefault.indexOf(requiredProperty) !== -1) continue
350
+ code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
351
+ }
324
352
 
325
- const defaultValue = propertyLocation.schema.default
326
- if (defaultValue !== undefined) {
327
- code += `
328
- } else {
353
+ code += `
354
+ let addComma = false
355
+ let json = '${context.wrapObjects ? '{' : ''}'
356
+ `
357
+ const wrapObjects = context.wrapObjects
358
+ context.wrapObjects = true
359
+
360
+ if (schema.properties) {
361
+ for (const key of Object.keys(schema.properties)) {
362
+ let propertyLocation = propertiesLocation.getPropertyLocation(key)
363
+ if (propertyLocation.schema.$ref) {
364
+ propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref)
365
+ }
366
+
367
+ const sanitizedKey = JSON.stringify(key)
368
+
369
+ if (requiredWithoutDefault.indexOf(key) !== -1) {
370
+ code += `
329
371
  ${addComma}
330
- json += ${JSON.stringify(sanitized + ':' + JSON.stringify(defaultValue))}
372
+ json += ${JSON.stringify(sanitizedKey + ':')}
373
+ ${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
331
374
  `
332
- } else if (required.includes(key)) {
333
- code += `
334
375
  } else {
335
- throw new Error('${sanitized} is required!')
336
- `
337
- }
338
-
339
- code += `
376
+ // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
377
+ // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
378
+ code += `
379
+ if (obj[${sanitizedKey}] !== undefined) {
380
+ ${addComma}
381
+ json += ${JSON.stringify(sanitizedKey + ':')}
382
+ ${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
383
+ }
384
+ `
385
+ const defaultValue = propertyLocation.schema.default
386
+ if (defaultValue !== undefined) {
387
+ code += `
388
+ else {
389
+ ${addComma}
390
+ json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
391
+ }
392
+ `
393
+ }
340
394
  }
341
- `
342
- })
343
-
344
- for (const requiredProperty of required) {
345
- if (schema.properties && schema.properties[requiredProperty] !== undefined) continue
346
- code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
395
+ }
347
396
  }
348
397
 
349
398
  if (schema.patternProperties || schema.additionalProperties) {
350
399
  code += buildExtraObjectPropertiesSerializer(context, location)
351
400
  }
352
401
 
402
+ context.wrapObjects = wrapObjects
403
+ code += `
404
+ return json${context.wrapObjects ? ' + \'}\'' : ''}
405
+ `
353
406
  return code
354
407
  }
355
408
 
@@ -504,19 +557,13 @@ function buildObject (context, location) {
504
557
  }
505
558
 
506
559
  let functionCode = `
507
- function ${functionName} (input) {
508
- // ${schemaRef}
509
560
  `
510
561
 
511
562
  functionCode += `
563
+ // ${schemaRef}
564
+ function ${functionName} (input) {
512
565
  const obj = ${toJSON('input')}
513
- let json = '{'
514
- let addComma = false
515
- `
516
-
517
- functionCode += buildInnerObject(context, location)
518
- functionCode += `
519
- return json + '}'
566
+ ${buildInnerObject(context, location)}
520
567
  }
521
568
  `
522
569
 
@@ -789,7 +836,7 @@ function buildConstSerializer (location, input) {
789
836
  `
790
837
  }
791
838
 
792
- code += `json += '${JSON.stringify(schema.const)}'`
839
+ code += `json += '${JSON.stringify(schema.const).replace(SINGLE_TICK, "\\'")}'`
793
840
 
794
841
  if (hasNullType) {
795
842
  code += `
@@ -832,9 +879,19 @@ function buildValue (context, location, input) {
832
879
 
833
880
  let code = ''
834
881
 
835
- if (type === undefined && (schema.anyOf || schema.oneOf)) {
882
+ if ((type === undefined || type === 'object') && (schema.anyOf || schema.oneOf)) {
836
883
  context.validatorSchemasIds.add(location.getSchemaId())
837
884
 
885
+ if (schema.type === 'object') {
886
+ context.wrapObjects = false
887
+ const funcName = buildObject(context, location)
888
+ code += `
889
+ json += '{'
890
+ json += ${funcName}(${input})
891
+ json += ','
892
+ `
893
+ }
894
+
838
895
  const type = schema.anyOf ? 'anyOf' : 'oneOf'
839
896
  const anyOfLocation = location.getPropertyLocation(type)
840
897
 
@@ -856,6 +913,12 @@ function buildValue (context, location, input) {
856
913
  code += `
857
914
  else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
858
915
  `
916
+ if (schema.type === 'object') {
917
+ code += `
918
+ json += '}'
919
+ `
920
+ context.wrapObjects = true
921
+ }
859
922
  return code
860
923
  }
861
924
 
package/lib/serializer.js CHANGED
@@ -113,11 +113,10 @@ module.exports = class Serializer {
113
113
  }
114
114
  }
115
115
 
116
- // Fast escape chars check
117
- if (!STR_ESCAPE.test(str)) {
118
- return '"' + str + '"'
119
- } else if (str.length < 42) {
116
+ if (str.length < 42) {
120
117
  return this.asStringSmall(str)
118
+ } else if (STR_ESCAPE.test(str) === false) {
119
+ return '"' + str + '"'
121
120
  } else {
122
121
  return JSON.stringify(str)
123
122
  }
@@ -130,32 +129,32 @@ module.exports = class Serializer {
130
129
  // 34 and 92 happens all the time, so we
131
130
  // have a fast case for them
132
131
  asStringSmall (str) {
133
- const l = str.length
132
+ const len = str.length
134
133
  let result = ''
135
- let last = 0
136
- let found = false
137
- let surrogateFound = false
134
+ let last = -1
138
135
  let point = 255
136
+
139
137
  // eslint-disable-next-line
140
- for (var i = 0; i < l && point >= 32; i++) {
138
+ for (var i = 0; i < len; i++) {
141
139
  point = str.charCodeAt(i)
140
+ if (point < 32) {
141
+ return JSON.stringify(str)
142
+ }
142
143
  if (point >= 0xD800 && point <= 0xDFFF) {
143
144
  // The current character is a surrogate.
144
- surrogateFound = true
145
+ return JSON.stringify(str)
145
146
  }
146
- if (point === 34 || point === 92) {
147
+ if (
148
+ point === 0x22 || // '"'
149
+ point === 0x5c // '\'
150
+ ) {
151
+ last === -1 && (last = 0)
147
152
  result += str.slice(last, i) + '\\'
148
153
  last = i
149
- found = true
150
154
  }
151
155
  }
152
156
 
153
- if (!found) {
154
- result = str
155
- } else {
156
- result += str.slice(last)
157
- }
158
- return ((point < 32) || (surrogateFound === true)) ? JSON.stringify(str) : '"' + result + '"'
157
+ return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
159
158
  }
160
159
 
161
160
  getState () {
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.7.0",
3
+ "version": "5.9.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
+ "type": "commonjs",
6
7
  "types": "types/index.d.ts",
7
8
  "scripts": {
8
9
  "bench": "node ./benchmark/bench.js",
@@ -34,7 +35,7 @@
34
35
  "homepage": "https://github.com/fastify/fast-json-stringify#readme",
35
36
  "devDependencies": {
36
37
  "@fastify/pre-commit": "^2.0.2",
37
- "@sinclair/typebox": "^0.26.5",
38
+ "@sinclair/typebox": "^0.31.1",
38
39
  "benchmark": "^2.1.4",
39
40
  "cli-select": "^1.1.2",
40
41
  "compile-json-stringify": "^0.1.2",
@@ -42,7 +43,7 @@
42
43
  "simple-git": "^3.7.1",
43
44
  "standard": "^17.0.0",
44
45
  "tap": "^16.0.1",
45
- "tsd": "^0.28.0",
46
+ "tsd": "^0.29.0",
46
47
  "webpack": "^5.40.0",
47
48
  "fast-json-stringify": "."
48
49
  },
@@ -52,7 +53,8 @@
52
53
  "ajv-formats": "^2.1.1",
53
54
  "fast-deep-equal": "^3.1.3",
54
55
  "fast-uri": "^2.1.0",
55
- "rfdc": "^1.2.0"
56
+ "rfdc": "^1.2.0",
57
+ "json-schema-ref-resolver": "^1.0.1"
56
58
  },
57
59
  "standard": {
58
60
  "ignore": [
@@ -220,6 +220,75 @@ test('object with nested allOfs', (t) => {
220
220
  t.equal(value, '{"id1":1,"id2":2,"id3":3}')
221
221
  })
222
222
 
223
+ test('object with anyOf nested inside allOf', (t) => {
224
+ t.plan(1)
225
+
226
+ const schema = {
227
+ title: 'object with anyOf nested inside allOf',
228
+ type: 'object',
229
+ allOf: [
230
+ {
231
+ required: ['id1', 'obj'],
232
+ type: 'object',
233
+ properties: {
234
+ id1: {
235
+ type: 'integer'
236
+ },
237
+ obj: {
238
+ type: 'object',
239
+ properties: {
240
+ nested: { type: 'string' }
241
+ }
242
+ }
243
+ }
244
+ },
245
+ {
246
+ anyOf: [
247
+ {
248
+ type: 'object',
249
+ properties: {
250
+ id2: { type: 'string' }
251
+ },
252
+ required: ['id2']
253
+ },
254
+ {
255
+ type: 'object',
256
+ properties: {
257
+ id3: {
258
+ type: 'integer'
259
+ },
260
+ nestedObj: {
261
+ type: 'object',
262
+ properties: {
263
+ nested: { type: 'string' }
264
+ }
265
+ }
266
+ },
267
+ required: ['id3']
268
+ },
269
+ {
270
+ type: 'object',
271
+ properties: {
272
+ id4: { type: 'integer' }
273
+ },
274
+ required: ['id4']
275
+ }
276
+ ]
277
+ }
278
+ ]
279
+ }
280
+
281
+ const stringify = build(schema)
282
+ const value = stringify({
283
+ id1: 1,
284
+ id3: 3,
285
+ id4: 4, // extra prop shouldn't be in result
286
+ obj: { nested: 'yes' },
287
+ nestedObj: { nested: 'yes' }
288
+ })
289
+ t.equal(value, '{"id1":1,"obj":{"nested":"yes"},"id3":3,"nestedObj":{"nested":"yes"}}')
290
+ })
291
+
223
292
  test('object with $ref in allOf', (t) => {
224
293
  t.plan(1)
225
294
 
@@ -344,7 +344,7 @@ test('patternProperties - throw on unknown type', (t) => {
344
344
  }
345
345
  })
346
346
 
347
- test('render a single quote as JSON', (t) => {
347
+ test('render a double quote as JSON /1', (t) => {
348
348
  t.plan(2)
349
349
 
350
350
  const schema = {
@@ -360,6 +360,38 @@ test('render a single quote as JSON', (t) => {
360
360
  t.ok(validate(JSON.parse(output)), 'valid schema')
361
361
  })
362
362
 
363
+ test('render a double quote as JSON /2', (t) => {
364
+ t.plan(2)
365
+
366
+ const schema = {
367
+ type: 'string'
368
+ }
369
+ const toStringify = 'double quote " 2'
370
+
371
+ const validate = validator(schema)
372
+ const stringify = build(schema)
373
+ const output = stringify(toStringify)
374
+
375
+ t.equal(output, JSON.stringify(toStringify))
376
+ t.ok(validate(JSON.parse(output)), 'valid schema')
377
+ })
378
+
379
+ test('render a long string', (t) => {
380
+ t.plan(2)
381
+
382
+ const schema = {
383
+ type: 'string'
384
+ }
385
+ const toStringify = 'the Ultimate Question of Life, the Universe, and Everything.'
386
+
387
+ const validate = validator(schema)
388
+ const stringify = build(schema)
389
+ const output = stringify(toStringify)
390
+
391
+ t.equal(output, JSON.stringify(toStringify))
392
+ t.ok(validate(JSON.parse(output)), 'valid schema')
393
+ })
394
+
363
395
  test('returns JSON.stringify if schema type is boolean', t => {
364
396
  t.plan(1)
365
397
 
@@ -82,6 +82,26 @@ test('schema with const string and no input', (t) => {
82
82
  t.ok(validate(JSON.parse(output)), 'valid schema')
83
83
  })
84
84
 
85
+ test('schema with const string that contains \'', (t) => {
86
+ t.plan(2)
87
+
88
+ const schema = {
89
+ type: 'object',
90
+ properties: {
91
+ foo: { const: "'bar'" }
92
+ }
93
+ }
94
+
95
+ const validate = validator(schema)
96
+ const stringify = build(schema)
97
+ const output = stringify({
98
+ foo: "'bar'"
99
+ })
100
+
101
+ t.equal(output, '{"foo":"\'bar\'"}')
102
+ t.ok(validate(JSON.parse(output)), 'valid schema')
103
+ })
104
+
85
105
  test('schema with const number', (t) => {
86
106
  t.plan(2)
87
107
 
package/test/ref.test.js CHANGED
@@ -1377,7 +1377,10 @@ test('Bad key', t => {
1377
1377
  })
1378
1378
  t.fail('Should throw')
1379
1379
  } catch (err) {
1380
- t.equal(err.message, 'Cannot find reference "extrenal#/definitions/projectId"')
1380
+ t.equal(
1381
+ err.message,
1382
+ 'Cannot resolve ref "extrenal#/definitions/projectId". Schema with id "extrenal" is not found.'
1383
+ )
1381
1384
  }
1382
1385
  })
1383
1386
 
@@ -2076,7 +2079,11 @@ test('should throw an Error if two non-identical schemas with same id are provid
2076
2079
  ]
2077
2080
  }
2078
2081
 
2079
- t.throws(() => build(schema), new Error('There is already another schema with id inner_schema'))
2082
+ try {
2083
+ build(schema)
2084
+ } catch (err) {
2085
+ t.equal(err.message, 'There is already another schema with id "inner_schema".')
2086
+ }
2080
2087
  })
2081
2088
 
2082
2089
  test('ref internal - throw if schema has definition twice with different shape', (t) => {
@@ -2115,5 +2122,9 @@ test('ref internal - throw if schema has definition twice with different shape',
2115
2122
  }
2116
2123
  }
2117
2124
 
2118
- t.throws(() => build(schema), Error('There is already another schema with id test##uri'))
2125
+ try {
2126
+ build(schema)
2127
+ } catch (err) {
2128
+ t.equal(err.message, 'There is already another anchor "#uri" in a schema "test".')
2129
+ }
2119
2130
  })
@@ -2,8 +2,8 @@
2
2
 
3
3
  const test = require('tap').test
4
4
  const fjs = require('..')
5
- const fs = require('fs')
6
- const path = require('path')
5
+ const fs = require('node:fs')
6
+ const path = require('node:path')
7
7
 
8
8
  function build (opts, schema) {
9
9
  return fjs(schema || {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const test = require('tap').test
4
4
  const webpack = require('webpack')
5
- const path = require('path')
5
+ const path = require('node:path')
6
6
 
7
7
  test('the library should work with webpack', async (t) => {
8
8
  t.plan(1)
@@ -1,104 +0,0 @@
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
- getSchemaDependencies (schemaId, dependencies = {}) {
32
- const schema = this.schemas[schemaId]
33
-
34
- for (const dependencySchemaId of schema.dependencies) {
35
- if (dependencies[dependencySchemaId] !== undefined) continue
36
- dependencies[dependencySchemaId] = this.getSchema(dependencySchemaId)
37
- this.getSchemaDependencies(dependencySchemaId, dependencies)
38
- }
39
-
40
- return dependencies
41
- }
42
-
43
- insertSchemaBySchemaId (schema, schemaId) {
44
- if (
45
- this.schemas[schemaId] !== undefined &&
46
- !deepEqual(schema, this.schemas[schemaId].schema)
47
- ) {
48
- throw new Error(`There is already another schema with id ${schemaId}`)
49
- }
50
- this.schemas[schemaId] = { schema, anchors: {}, dependencies: [] }
51
- }
52
-
53
- insertSchemaByAnchor (schema, schemaId, anchor) {
54
- const { anchors } = this.schemas[schemaId]
55
- if (
56
- anchors[anchor] !== undefined &&
57
- !deepEqual(schema, anchors[anchor])
58
- ) {
59
- throw new Error(`There is already another schema with id ${schemaId}#${anchor}`)
60
- }
61
- anchors[anchor] = schema
62
- }
63
-
64
- insertSchemaSubschemas (schema, rootSchemaId) {
65
- const schemaId = schema.$id
66
- if (schemaId !== undefined && typeof schemaId === 'string') {
67
- if (schemaId.charAt(0) === '#') {
68
- this.insertSchemaByAnchor(schema, rootSchemaId, schemaId)
69
- } else {
70
- this.insertSchemaBySchemaId(schema, schemaId)
71
- rootSchemaId = schemaId
72
- }
73
- }
74
-
75
- const ref = schema.$ref
76
- if (ref !== undefined && typeof ref === 'string') {
77
- if (ref.charAt(0) !== '#') {
78
- const dependencySchemaId = ref.split('#')[0]
79
- this.schemas[rootSchemaId].dependencies.push(dependencySchemaId)
80
- }
81
- }
82
-
83
- for (const key in schema) {
84
- if (typeof schema[key] === 'object' && schema[key] !== null) {
85
- this.insertSchemaSubschemas(schema[key], rootSchemaId)
86
- }
87
- }
88
- }
89
- }
90
-
91
- function getDataByJSONPointer (data, jsonPointer) {
92
- const parts = jsonPointer.split('/')
93
- let current = data
94
- for (const part of parts) {
95
- if (part === '' || part === '#') continue
96
- if (typeof current !== 'object' || current === null) {
97
- return undefined
98
- }
99
- current = current[part]
100
- }
101
- return current
102
- }
103
-
104
- module.exports = RefResolver