fast-json-stringify 1.19.0 → 1.21.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 CHANGED
@@ -578,6 +578,31 @@ allowing user input to directly supply a schema.
578
578
  It can't be guaranteed that allowing user input for the schema couldn't feasibly expose an attack
579
579
  vector.
580
580
 
581
+ <a name="debug"></a>
582
+ ### Debug Mode
583
+
584
+ The debug mode can be activated during your development to understand what is going on when things do not
585
+ work as you expect.
586
+
587
+ ```js
588
+ const debugCompiled = fastJson({
589
+ title: 'default string',
590
+ type: 'object',
591
+ properties: {
592
+ firstName: {
593
+ type: 'string'
594
+ }
595
+ }
596
+ }, { debugMode: true })
597
+
598
+ console.log(debugCompiled) // it is an array of functions that can create your `stringify` function
599
+ console.log(debugCompiled.toString()) // print a "ready to read" string function, you can save it to a file
600
+
601
+ const rawString = debugCompiled.toString()
602
+ const stringify = fastJson.restore(rawString) // use the generated string to get back the `stringify` function
603
+ console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}'
604
+ ```
605
+
581
606
  <a name="acknowledgements"></a>
582
607
  ## Acknowledgements
583
608
 
package/index.js CHANGED
@@ -138,6 +138,14 @@ function build (schema, options) {
138
138
  }
139
139
 
140
140
  dependenciesName.push(code)
141
+
142
+ if (options.debugMode) {
143
+ dependenciesName.toString = function () {
144
+ return dependenciesName.join('\n')
145
+ }
146
+ return dependenciesName
147
+ }
148
+
141
149
  return (Function.apply(null, dependenciesName).apply(null, dependencies))
142
150
  }
143
151
 
@@ -271,7 +279,7 @@ function $asBooleanNullable (bool) {
271
279
  function $asDatetime (date) {
272
280
  if (date instanceof Date) {
273
281
  return '"' + date.toISOString() + '"'
274
- } else if (typeof date.toISOString === 'function') {
282
+ } else if (date && typeof date.toISOString === 'function') {
275
283
  return '"' + date.toISOString() + '"'
276
284
  } else {
277
285
  return $asString(date)
@@ -284,7 +292,7 @@ function $asDate (date) {
284
292
  var month = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
285
293
  var day = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
286
294
  return '"' + year + '-' + month + '-' + day + '"'
287
- } else if (typeof date.format === 'function') {
295
+ } else if (date && typeof date.format === 'function') {
288
296
  return '"' + date.format('YYYY-MM-DD') + '"'
289
297
  } else {
290
298
  return $asString(date)
@@ -297,7 +305,7 @@ function $asTime (date) {
297
305
  var minute = new Intl.DateTimeFormat('en', { minute: 'numeric' }).format(date)
298
306
  var second = new Intl.DateTimeFormat('en', { second: 'numeric' }).format(date)
299
307
  return '"' + $pad2Zeros(hour) + ':' + $pad2Zeros(minute) + ':' + $pad2Zeros(second) + '"'
300
- } else if (typeof date.format === 'function') {
308
+ } else if (date && typeof date.format === 'function') {
301
309
  return '"' + date.format('HH:mm:ss') + '"'
302
310
  } else {
303
311
  return $asString(date)
@@ -375,6 +383,11 @@ function addPatternProperties (schema, externalSchema, fullSchema) {
375
383
  var type = pp[regex].type
376
384
  var format = pp[regex].format
377
385
  var stringSerializer = getStringSerializer(format)
386
+ try {
387
+ RegExp(regex)
388
+ } catch (err) {
389
+ throw new Error(`${err.message}. Found at ${regex} matching ${JSON.stringify(pp[regex])}`)
390
+ }
378
391
  code += `
379
392
  if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {
380
393
  `
@@ -417,7 +430,7 @@ function addPatternProperties (schema, externalSchema, fullSchema) {
417
430
  `
418
431
  } else {
419
432
  code += `
420
- throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ${type}')
433
+ throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ' + ${JSON.stringify(type)})
421
434
  `
422
435
  }
423
436
 
@@ -510,7 +523,7 @@ function additionalProperty (schema, externalSchema, fullSchema) {
510
523
  `
511
524
  } else {
512
525
  code += `
513
- throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ${type}')
526
+ throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ' + ${JSON.stringify(type)})
514
527
  `
515
528
  }
516
529
  return code
@@ -579,7 +592,7 @@ function refFinder (ref, schema, externalSchema) {
579
592
  return dereferenced
580
593
  } else {
581
594
  for (var i = 1; i < walk.length; i++) {
582
- code += `['${sanitizeKey(walk[i])}']`
595
+ code += `[${JSON.stringify(walk[i])}]`
583
596
  }
584
597
  }
585
598
  }
@@ -595,35 +608,27 @@ function refFinder (ref, schema, externalSchema) {
595
608
 
596
609
  function findBadKey (obj, keys) {
597
610
  if (keys.length === 0) return null
598
- const key = sanitizeKey(keys.shift())
611
+ const key = keys.shift()
599
612
  if (obj[key] === undefined) {
600
613
  stringSimilarity = stringSimilarity || require('string-similarity')
601
614
  const { bestMatch } = stringSimilarity.findBestMatch(key, Object.keys(obj))
602
615
  if (bestMatch.rating >= 0.5) {
603
- throw new Error(`Cannot find reference '${key}', did you mean '${bestMatch.target}'?`)
616
+ throw new Error(`Cannot find reference ${JSON.stringify(key)}, did you mean ${JSON.stringify(bestMatch.target)}?`)
604
617
  } else {
605
- throw new Error(`Cannot find reference '${key}'`)
618
+ throw new Error(`Cannot find reference ${JSON.stringify(key)}`)
606
619
  }
607
620
  }
608
621
  return findBadKey(obj[key], keys)
609
622
  }
610
623
  }
611
624
 
612
- function sanitizeKey (key) {
613
- const rep = key.replace(/(\\*)'/g, function (match, p1) {
614
- var base = ''
615
- if (p1.length % 2 === 1) {
616
- base = p1.slice(2)
617
- } else {
618
- base = p1
619
- }
620
- var rep = base + '\\\''
621
- return rep
622
- })
623
- return rep
624
- }
625
-
626
625
  function buildCode (schema, code, laterCode, name, externalSchema, fullSchema) {
626
+ if (schema.$ref) {
627
+ schema = refFinder(schema.$ref, fullSchema, externalSchema)
628
+ }
629
+
630
+ var required = schema.required
631
+
627
632
  Object.keys(schema.properties || {}).forEach((key, i, a) => {
628
633
  if (schema.properties[key].$ref) {
629
634
  // if the schema object is deep in the tree, we must resolve the ref in the parent scope
@@ -636,14 +641,14 @@ function buildCode (schema, code, laterCode, name, externalSchema, fullSchema) {
636
641
 
637
642
  var type = schema.properties[key].type
638
643
  var nullable = schema.properties[key].nullable
639
- var sanitized = sanitizeKey(key)
640
- var asString = sanitizeKey($asString(key).replace(/\\/g, '\\\\'))
644
+ var sanitized = JSON.stringify(key)
645
+ var asString = JSON.stringify(sanitized)
641
646
 
642
647
  if (nullable) {
643
648
  code += `
644
- if (obj['${sanitized}'] === null) {
649
+ if (obj[${sanitized}] === null) {
645
650
  ${addComma}
646
- json += '${asString}:null'
651
+ json += ${asString} + ':null'
647
652
  var rendered = true
648
653
  } else {
649
654
  `
@@ -651,10 +656,10 @@ function buildCode (schema, code, laterCode, name, externalSchema, fullSchema) {
651
656
 
652
657
  if (type === 'number') {
653
658
  code += `
654
- var t = Number(obj['${sanitized}'])
659
+ var t = Number(obj[${sanitized}])
655
660
  if (!isNaN(t)) {
656
661
  ${addComma}
657
- json += '${asString}:' + t
662
+ json += ${asString} + ':' + t
658
663
  `
659
664
  } else if (type === 'integer') {
660
665
  code += `
@@ -662,25 +667,25 @@ function buildCode (schema, code, laterCode, name, externalSchema, fullSchema) {
662
667
  `
663
668
  if (isLong) {
664
669
  code += `
665
- if (isLong(obj['${sanitized}'])) {
670
+ if (isLong(obj[${sanitized}])) {
666
671
  ${addComma}
667
- json += '${asString}:' + obj['${sanitized}'].toString()
672
+ json += ${asString} + ':' + obj[${sanitized}].toString()
668
673
  rendered = true
669
674
  } else {
670
- var t = Number(obj['${sanitized}'])
675
+ var t = Number(obj[${sanitized}])
671
676
  if (!isNaN(t)) {
672
677
  ${addComma}
673
- json += '${asString}:' + t
678
+ json += ${asString} + ':' + t
674
679
  rendered = true
675
680
  }
676
681
  }
677
682
  `
678
683
  } else {
679
684
  code += `
680
- var t = Number(obj['${sanitized}'])
685
+ var t = Number(obj[${sanitized}])
681
686
  if (!isNaN(t)) {
682
687
  ${addComma}
683
- json += '${asString}:' + t
688
+ json += ${asString} + ':' + t
684
689
  rendered = true
685
690
  }
686
691
  `
@@ -690,24 +695,26 @@ function buildCode (schema, code, laterCode, name, externalSchema, fullSchema) {
690
695
  `
691
696
  } else {
692
697
  code += `
693
- if (obj['${sanitized}'] !== undefined) {
698
+ if (obj[${sanitized}] !== undefined) {
694
699
  ${addComma}
695
- json += '${asString}:'
700
+ json += ${asString} + ':'
696
701
  `
697
702
 
698
- var result = nested(laterCode, name, key, schema.properties[key], externalSchema, fullSchema)
703
+ var result = nested(laterCode, name, key, schema.properties[key], externalSchema, fullSchema, undefined, false)
699
704
  code += result.code
700
705
  laterCode = result.laterCode
701
706
  }
702
707
 
703
708
  var defaultValue = schema.properties[key].default
704
709
  if (defaultValue !== undefined) {
710
+ required = filterRequired(schema.required, key)
705
711
  code += `
706
712
  } else {
707
713
  ${addComma}
708
- json += '${asString}:${sanitizeKey(JSON.stringify(defaultValue).replace(/\\/g, '\\\\'))}'
714
+ json += ${asString} + ':' + ${JSON.stringify(JSON.stringify(defaultValue))}
709
715
  `
710
716
  } else if (schema.required && schema.required.indexOf(key) !== -1) {
717
+ required = filterRequired(schema.required, key)
711
718
  code += `
712
719
  } else {
713
720
  throw new Error('${sanitized} is required!')
@@ -724,9 +731,33 @@ function buildCode (schema, code, laterCode, name, externalSchema, fullSchema) {
724
731
  `
725
732
  }
726
733
  })
734
+
735
+ if (required && required.length > 0) {
736
+ code += 'var required = ['
737
+ for (var i = 0; i < required.length; i++) {
738
+ if (i > 0) {
739
+ code += ','
740
+ }
741
+ code += `${JSON.stringify(required[i])}`
742
+ }
743
+ code += ']'
744
+ code += `
745
+ for (var i = 0; i < required.length; i++) {
746
+ if (obj[required[i]] === undefined) throw new Error('"' + required[i] + '" is required!')
747
+ }
748
+ `
749
+ }
750
+
727
751
  return { code: code, laterCode: laterCode }
728
752
  }
729
753
 
754
+ function filterRequired (required, key) {
755
+ if (!required) {
756
+ return required
757
+ }
758
+ return required.filter(k => k !== key)
759
+ }
760
+
730
761
  function buildCodeWithAllOfs (schema, code, laterCode, name, externalSchema, fullSchema) {
731
762
  if (schema.allOf) {
732
763
  schema.allOf.forEach((ss) => {
@@ -882,7 +913,7 @@ function buildArray (schema, code, name, externalSchema, fullSchema) {
882
913
  if (Array.isArray(schema.items)) {
883
914
  result = schema.items.reduce((res, item, i) => {
884
915
  var accessor = '[i]'
885
- const tmpRes = nested(laterCode, name, accessor, item, externalSchema, fullSchema, i)
916
+ const tmpRes = nested(laterCode, name, accessor, item, externalSchema, fullSchema, i, true)
886
917
  var condition = `i === ${i} && ${buildArrayTypeCondition(item.type, accessor)}`
887
918
  return {
888
919
  code: `${res.code}
@@ -899,7 +930,7 @@ function buildArray (schema, code, name, externalSchema, fullSchema) {
899
930
  }
900
931
  `
901
932
  } else {
902
- result = nested(laterCode, name, '[i]', schema.items, externalSchema, fullSchema)
933
+ result = nested(laterCode, name, '[i]', schema.items, externalSchema, fullSchema, undefined, true)
903
934
  }
904
935
 
905
936
  code += `
@@ -973,7 +1004,21 @@ function dereferenceOfRefs (schema, externalSchema, fullSchema, type) {
973
1004
  })
974
1005
  }
975
1006
 
976
- function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKey) {
1007
+ var strNameCounter = 0
1008
+ function asFuncName (str) {
1009
+ // only allow chars that can work
1010
+ var rep = str.replace(/[^a-zA-Z0-9$_]/g, '')
1011
+
1012
+ if (rep.length === 0) {
1013
+ return 'anan' + strNameCounter++
1014
+ } else if (rep !== str) {
1015
+ rep += strNameCounter++
1016
+ }
1017
+
1018
+ return rep
1019
+ }
1020
+
1021
+ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKey, isArray) {
977
1022
  var code = ''
978
1023
  var funcName
979
1024
 
@@ -993,7 +1038,8 @@ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKe
993
1038
  var type = schema.type
994
1039
  var nullable = schema.nullable === true
995
1040
 
996
- var accessor = key.indexOf('[') === 0 ? sanitizeKey(key) : `['${sanitizeKey(key)}']`
1041
+ var accessor = isArray ? key : `[${JSON.stringify(key)}]`
1042
+
997
1043
  switch (type) {
998
1044
  case 'null':
999
1045
  code += `
@@ -1014,14 +1060,14 @@ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKe
1014
1060
  code += nullable ? `json += obj${accessor} === null ? null : $asBoolean(obj${accessor})` : `json += $asBoolean(obj${accessor})`
1015
1061
  break
1016
1062
  case 'object':
1017
- funcName = (name + key + subKey).replace(/[-.\[\] ]/g, '') // eslint-disable-line
1063
+ funcName = asFuncName(name + key + subKey)
1018
1064
  laterCode = buildObject(schema, laterCode, funcName, externalSchema, fullSchema)
1019
1065
  code += `
1020
1066
  json += ${funcName}(obj${accessor})
1021
1067
  `
1022
1068
  break
1023
1069
  case 'array':
1024
- funcName = '$arr' + (name + key + subKey).replace(/[-.\[\] ]/g, '') // eslint-disable-line
1070
+ funcName = asFuncName('$arr' + name + key + subKey) // eslint-disable-line
1025
1071
  laterCode = buildArray(schema, laterCode, funcName, externalSchema, fullSchema)
1026
1072
  code += `
1027
1073
  json += ${funcName}(obj${accessor})
@@ -1031,7 +1077,7 @@ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKe
1031
1077
  if ('anyOf' in schema) {
1032
1078
  dereferenceOfRefs(schema, externalSchema, fullSchema, 'anyOf')
1033
1079
  schema.anyOf.forEach((s, index) => {
1034
- var nestedResult = nested(laterCode, name, key, s, externalSchema, fullSchema, subKey !== '' ? subKey : 'i' + index)
1080
+ var nestedResult = nested(laterCode, name, key, s, externalSchema, fullSchema, subKey !== '' ? subKey : 'i' + index, isArray)
1035
1081
  code += `
1036
1082
  ${index === 0 ? 'if' : 'else if'}(ajv.validate(${require('util').inspect(s, { depth: null })}, obj${accessor}))
1037
1083
  ${nestedResult.code}
@@ -1044,7 +1090,7 @@ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKe
1044
1090
  } else if ('oneOf' in schema) {
1045
1091
  dereferenceOfRefs(schema, externalSchema, fullSchema, 'oneOf')
1046
1092
  schema.oneOf.forEach((s, index) => {
1047
- var nestedResult = nested(laterCode, name, key, s, externalSchema, fullSchema, subKey !== '' ? subKey : 'i' + index)
1093
+ var nestedResult = nested(laterCode, name, key, s, externalSchema, fullSchema, subKey !== '' ? subKey : 'i' + index, isArray)
1048
1094
  code += `
1049
1095
  ${index === 0 ? 'if' : 'else if'}(ajv.validate(${require('util').inspect(s, { depth: null })}, obj${accessor}))
1050
1096
  ${nestedResult.code}
@@ -1068,10 +1114,11 @@ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKe
1068
1114
  const sortedTypes = nullIndex !== -1 ? [type[nullIndex]].concat(type.slice(0, nullIndex)).concat(type.slice(nullIndex + 1)) : type
1069
1115
  sortedTypes.forEach((type, index) => {
1070
1116
  var tempSchema = Object.assign({}, schema, { type })
1071
- var nestedResult = nested(laterCode, name, key, tempSchema, externalSchema, fullSchema, subKey)
1117
+ var nestedResult = nested(laterCode, name, key, tempSchema, externalSchema, fullSchema, subKey, isArray)
1118
+
1072
1119
  if (type === 'string') {
1073
1120
  code += `
1074
- ${index === 0 ? 'if' : 'else if'}(typeof obj${accessor} === "${type}" || obj${accessor} instanceof Date || typeof obj${accessor}.toISOString === "function" || obj${accessor} instanceof RegExp)
1121
+ ${index === 0 ? 'if' : 'else if'}(obj${accessor} === null || typeof obj${accessor} === "${type}" || obj${accessor} instanceof Date || typeof obj${accessor}.toISOString === "function" || obj${accessor} instanceof RegExp)
1075
1122
  ${nestedResult.code}
1076
1123
  `
1077
1124
  } else if (type === 'null') {
@@ -1156,3 +1203,16 @@ function isEmpty (schema) {
1156
1203
  }
1157
1204
 
1158
1205
  module.exports = build
1206
+
1207
+ module.exports.restore = function (debugModeStr, options = {}) {
1208
+ const dependencies = [debugModeStr]
1209
+ const args = []
1210
+ if (debugModeStr.startsWith('ajv')) {
1211
+ dependencies.unshift('ajv')
1212
+ args.push(new Ajv(options.ajv))
1213
+ }
1214
+
1215
+ // eslint-disable-next-line
1216
+ return (Function.apply(null, ['ajv', debugModeStr])
1217
+ .apply(null, args))
1218
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -44,7 +44,7 @@ test('object with allOf and multiple schema on the allOf', (t) => {
44
44
  id: 1
45
45
  })
46
46
  } catch (e) {
47
- t.is(e.message, 'name is required!')
47
+ t.is(e.message, '"name" is required!')
48
48
  }
49
49
 
50
50
  try {
@@ -52,7 +52,7 @@ test('object with allOf and multiple schema on the allOf', (t) => {
52
52
  name: 'string'
53
53
  })
54
54
  } catch (e) {
55
- t.is(e.message, 'id is required!')
55
+ t.is(e.message, '"id" is required!')
56
56
  }
57
57
 
58
58
  try {
@@ -135,7 +135,7 @@ test('object with nested allOfs', (t) => {
135
135
  allOf: [
136
136
  {
137
137
  required: [
138
- 'id'
138
+ 'id1'
139
139
  ],
140
140
  type: 'object',
141
141
  properties: {
@@ -184,3 +184,200 @@ test('object with nested allOfs', (t) => {
184
184
  t.fail()
185
185
  }
186
186
  })
187
+
188
+ test('object with $ref in allOf', (t) => {
189
+ t.plan(1)
190
+
191
+ const schema = {
192
+ title: 'object with $ref in allOf',
193
+ type: 'object',
194
+ definitions: {
195
+ id1: {
196
+ type: 'object',
197
+ properties: {
198
+ id1: {
199
+ type: 'integer'
200
+ }
201
+ }
202
+ }
203
+ },
204
+ allOf: [
205
+ {
206
+ $ref: '#/definitions/id1'
207
+ }
208
+ ]
209
+ }
210
+
211
+ try {
212
+ const stringify = build(schema)
213
+ try {
214
+ const value = stringify({
215
+ id1: 1,
216
+ id2: 2 // extra prop shouldn't be in result
217
+ })
218
+ t.is(value, '{"id1":1}')
219
+ } catch (e) {
220
+ t.fail()
221
+ }
222
+ } catch (e) {
223
+ t.fail()
224
+ }
225
+ })
226
+
227
+ test('object with $ref and other object in allOf', (t) => {
228
+ t.plan(1)
229
+
230
+ const schema = {
231
+ title: 'object with $ref in allOf',
232
+ type: 'object',
233
+ definitions: {
234
+ id1: {
235
+ type: 'object',
236
+ properties: {
237
+ id1: {
238
+ type: 'integer'
239
+ }
240
+ }
241
+ }
242
+ },
243
+ allOf: [
244
+ {
245
+ $ref: '#/definitions/id1'
246
+ },
247
+ {
248
+ type: 'object',
249
+ properties: {
250
+ id2: {
251
+ type: 'integer'
252
+ }
253
+ }
254
+ }
255
+ ]
256
+ }
257
+
258
+ try {
259
+ const stringify = build(schema)
260
+ try {
261
+ const value = stringify({
262
+ id1: 1,
263
+ id2: 2,
264
+ id3: 3 // extra prop shouldn't be in result
265
+ })
266
+ t.is(value, '{"id1":1,"id2":2}')
267
+ } catch (e) {
268
+ t.fail()
269
+ }
270
+ } catch (e) {
271
+ t.fail()
272
+ }
273
+ })
274
+
275
+ test('object with multiple $refs in allOf', (t) => {
276
+ t.plan(1)
277
+
278
+ const schema = {
279
+ title: 'object with $ref in allOf',
280
+ type: 'object',
281
+ definitions: {
282
+ id1: {
283
+ type: 'object',
284
+ properties: {
285
+ id1: {
286
+ type: 'integer'
287
+ }
288
+ }
289
+ },
290
+ id2: {
291
+ type: 'object',
292
+ properties: {
293
+ id2: {
294
+ type: 'integer'
295
+ }
296
+ }
297
+ }
298
+ },
299
+ allOf: [
300
+ {
301
+ $ref: '#/definitions/id1'
302
+ },
303
+ {
304
+ $ref: '#/definitions/id2'
305
+ }
306
+ ]
307
+ }
308
+
309
+ try {
310
+ const stringify = build(schema)
311
+ try {
312
+ const value = stringify({
313
+ id1: 1,
314
+ id2: 2,
315
+ id3: 3 // extra prop shouldn't be in result
316
+ })
317
+ t.is(value, '{"id1":1,"id2":2}')
318
+ } catch (e) {
319
+ t.fail()
320
+ }
321
+ } catch (e) {
322
+ t.fail()
323
+ }
324
+ })
325
+
326
+ test('object with external $refs in allOf', (t) => {
327
+ t.plan(1)
328
+
329
+ const externalSchema = {
330
+ first: {
331
+ definitions: {
332
+ id1: {
333
+ type: 'object',
334
+ properties: {
335
+ id1: {
336
+ type: 'integer'
337
+ }
338
+ }
339
+ }
340
+ }
341
+ },
342
+ second: {
343
+ id2: {
344
+ $id: '#id2',
345
+ type: 'object',
346
+ properties: {
347
+ id2: {
348
+ type: 'integer'
349
+ }
350
+ }
351
+ }
352
+ }
353
+ }
354
+
355
+ const schema = {
356
+ title: 'object with $ref in allOf',
357
+ type: 'object',
358
+ allOf: [
359
+ {
360
+ $ref: 'first#/definitions/id1'
361
+ },
362
+ {
363
+ $ref: 'second#id2'
364
+ }
365
+ ]
366
+ }
367
+
368
+ try {
369
+ const stringify = build(schema, { schema: externalSchema })
370
+ try {
371
+ const value = stringify({
372
+ id1: 1,
373
+ id2: 2,
374
+ id3: 3 // extra prop shouldn't be in result
375
+ })
376
+ t.is(value, '{"id1":1,"id2":2}')
377
+ } catch (e) {
378
+ t.fail()
379
+ }
380
+ } catch (e) {
381
+ t.fail()
382
+ }
383
+ })
@@ -142,6 +142,21 @@ buildTest({
142
142
  }
143
143
  }, { args: [{ a: 'test' }, { b: 1 }] })
144
144
 
145
+ buildTest({
146
+ title: 'array with weird key',
147
+ type: 'object',
148
+ properties: {
149
+ '@data': {
150
+ type: 'array',
151
+ items: {
152
+ type: 'string'
153
+ }
154
+ }
155
+ }
156
+ }, {
157
+ '@data': ['test']
158
+ })
159
+
145
160
  test('invalid items throw', (t) => {
146
161
  t.plan(1)
147
162
  const schema = {
@@ -129,6 +129,25 @@ buildTest({
129
129
  '@version': 1
130
130
  })
131
131
 
132
+ buildTest({
133
+ title: 'deep object with weird keys of type object',
134
+ type: 'object',
135
+ properties: {
136
+ '@data': {
137
+ type: 'object',
138
+ properties: {
139
+ id: {
140
+ type: 'string'
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }, {
146
+ '@data': {
147
+ id: 'string'
148
+ }
149
+ })
150
+
132
151
  buildTest({
133
152
  title: 'deep object with spaces in key',
134
153
  type: 'object',
package/test/date.test.js CHANGED
@@ -91,7 +91,6 @@ test('render a date in a string when format is time as HH:mm:ss', (t) => {
91
91
  const output = stringify(toStringify)
92
92
 
93
93
  validate(JSON.parse(output))
94
- console.log(validate.errors)
95
94
 
96
95
  t.equal(output, `"${moment(toStringify).format('HH:mm:ss')}"`)
97
96
  t.ok(validate(JSON.parse(output)), 'valid schema')
@@ -112,7 +111,6 @@ test('verify padding for rendered date in a string when format is time', (t) =>
112
111
  const output = stringify(toStringify)
113
112
 
114
113
  validate(JSON.parse(output))
115
- console.log(validate.errors)
116
114
 
117
115
  t.equal(output, `"${moment(toStringify).format('HH:mm:ss')}"`)
118
116
  t.ok(validate(JSON.parse(output)), 'valid schema')
@@ -194,3 +192,211 @@ test('render a nested object in a string when type is date-format as ISOString',
194
192
  t.equal(output, JSON.stringify(toStringify))
195
193
  t.ok(validate(JSON.parse(output)), 'valid schema')
196
194
  })
195
+
196
+ test('serializing null value', t => {
197
+ const input = { updatedAt: null }
198
+
199
+ function createSchema (properties) {
200
+ return {
201
+ title: 'an object in a string',
202
+ type: 'object',
203
+ properties
204
+ }
205
+ }
206
+
207
+ function serialize (schema, input) {
208
+ const validate = validator(schema)
209
+ const stringify = build(schema)
210
+ const output = stringify(input)
211
+
212
+ return {
213
+ validate,
214
+ output
215
+ }
216
+ }
217
+
218
+ t.plan(3)
219
+
220
+ t.test('type::string', t => {
221
+ t.plan(3)
222
+
223
+ t.test('format::date-time', t => {
224
+ t.plan(2)
225
+
226
+ const prop = {
227
+ updatedAt: {
228
+ type: 'string',
229
+ format: 'date-time'
230
+ }
231
+ }
232
+
233
+ const {
234
+ output,
235
+ validate
236
+ } = serialize(createSchema(prop), input)
237
+
238
+ t.equal(output, '{"updatedAt":""}')
239
+ t.notOk(validate(JSON.parse(output)), 'an empty string is not a date-time format')
240
+ })
241
+
242
+ t.test('format::date', t => {
243
+ t.plan(2)
244
+
245
+ const prop = {
246
+ updatedAt: {
247
+ type: 'string',
248
+ format: 'date'
249
+ }
250
+ }
251
+
252
+ const {
253
+ output,
254
+ validate
255
+ } = serialize(createSchema(prop), input)
256
+
257
+ t.equal(output, '{"updatedAt":""}')
258
+ t.notOk(validate(JSON.parse(output)), 'an empty string is not a date format')
259
+ })
260
+
261
+ t.test('format::time', t => {
262
+ t.plan(2)
263
+
264
+ const prop = {
265
+ updatedAt: {
266
+ type: 'string',
267
+ format: 'time'
268
+ }
269
+ }
270
+
271
+ const {
272
+ output,
273
+ validate
274
+ } = serialize(createSchema(prop), input)
275
+
276
+ t.equal(output, '{"updatedAt":""}')
277
+ t.notOk(validate(JSON.parse(output)), 'an empty string is not a time format')
278
+ })
279
+ })
280
+
281
+ t.test('type::array', t => {
282
+ t.plan(3)
283
+
284
+ t.test('format::date-time', t => {
285
+ t.plan(2)
286
+
287
+ const prop = {
288
+ updatedAt: {
289
+ type: ['string'],
290
+ format: 'date-time'
291
+ }
292
+ }
293
+
294
+ const {
295
+ output,
296
+ validate
297
+ } = serialize(createSchema(prop), input)
298
+
299
+ t.equal(output, '{"updatedAt":""}')
300
+ t.notOk(validate(JSON.parse(output)), 'an empty string is not a date-time format')
301
+ })
302
+
303
+ t.test('format::date', t => {
304
+ t.plan(2)
305
+
306
+ const prop = {
307
+ updatedAt: {
308
+ type: ['string'],
309
+ format: 'date'
310
+ }
311
+ }
312
+
313
+ const {
314
+ output,
315
+ validate
316
+ } = serialize(createSchema(prop), input)
317
+
318
+ t.equal(output, '{"updatedAt":""}')
319
+ t.notOk(validate(JSON.parse(output)), 'an empty string is not a date format')
320
+ })
321
+
322
+ t.test('format::time', t => {
323
+ t.plan(2)
324
+
325
+ const prop = {
326
+ updatedAt: {
327
+ type: ['string'],
328
+ format: 'time'
329
+ }
330
+ }
331
+
332
+ const {
333
+ output,
334
+ validate
335
+ } = serialize(createSchema(prop), input)
336
+
337
+ t.equal(output, '{"updatedAt":""}')
338
+ t.notOk(validate(JSON.parse(output)), 'an empty string is not a time format')
339
+ })
340
+ })
341
+
342
+ t.test('type::array::nullable', t => {
343
+ t.plan(3)
344
+
345
+ t.test('format::date-time', t => {
346
+ t.plan(2)
347
+
348
+ const prop = {
349
+ updatedAt: {
350
+ type: ['string', 'null'],
351
+ format: 'date-time'
352
+ }
353
+ }
354
+
355
+ const {
356
+ output,
357
+ validate
358
+ } = serialize(createSchema(prop), input)
359
+
360
+ t.equal(output, '{"updatedAt":null}')
361
+ t.ok(validate(JSON.parse(output)), 'valid schema')
362
+ })
363
+
364
+ t.test('format::date', t => {
365
+ t.plan(2)
366
+
367
+ const prop = {
368
+ updatedAt: {
369
+ type: ['string', 'null'],
370
+ format: 'date'
371
+ }
372
+ }
373
+
374
+ const {
375
+ output,
376
+ validate
377
+ } = serialize(createSchema(prop), input)
378
+
379
+ t.equal(output, '{"updatedAt":null}')
380
+ t.ok(validate(JSON.parse(output)), 'valid schema')
381
+ })
382
+
383
+ t.test('format::time', t => {
384
+ t.plan(2)
385
+
386
+ const prop = {
387
+ updatedAt: {
388
+ type: ['string', 'null'],
389
+ format: 'time'
390
+ }
391
+ }
392
+
393
+ const {
394
+ output,
395
+ validate
396
+ } = serialize(createSchema(prop), input)
397
+
398
+ t.equal(output, '{"updatedAt":null}')
399
+ t.ok(validate(JSON.parse(output)), 'valid schema')
400
+ })
401
+ })
402
+ })
@@ -0,0 +1,65 @@
1
+ 'use strict'
2
+
3
+ const test = require('tap').test
4
+ const fjs = require('..')
5
+
6
+ function build (opts) {
7
+ return fjs({
8
+ title: 'default string',
9
+ type: 'object',
10
+ properties: {
11
+ firstName: {
12
+ type: 'string'
13
+ }
14
+ },
15
+ required: ['firstName']
16
+ }, opts)
17
+ }
18
+
19
+ test('activate debug mode', t => {
20
+ t.plan(2)
21
+ const debugMode = build({ debugMode: true })
22
+ t.type(debugMode, Array)
23
+ t.like(debugMode.toString.toString(), 'join', 'to string override')
24
+ })
25
+
26
+ test('activate debug mode truthy', t => {
27
+ t.plan(2)
28
+ const debugMode = build({ debugMode: 'yes' })
29
+ t.type(debugMode, Array)
30
+ t.like(debugMode.toString.toString(), 'join', 'to string override')
31
+ })
32
+
33
+ test('to string auto-consistent', t => {
34
+ t.plan(2)
35
+ const debugMode = build({ debugMode: 1 })
36
+ t.type(debugMode, Array)
37
+
38
+ const str = debugMode.toString()
39
+ const compiled = fjs.restore(str)
40
+ const tobe = JSON.stringify({ firstName: 'Foo' })
41
+ t.deepEquals(compiled({ firstName: 'Foo', surname: 'bar' }), tobe, 'surname evicted')
42
+ })
43
+
44
+ test('to string auto-consistent with ajv', t => {
45
+ t.plan(2)
46
+ const debugMode = fjs({
47
+ title: 'object with multiple types field',
48
+ type: 'object',
49
+ properties: {
50
+ str: {
51
+ anyOf: [{
52
+ type: 'string'
53
+ }, {
54
+ type: 'boolean'
55
+ }]
56
+ }
57
+ }
58
+ }, { debugMode: 1 })
59
+ t.type(debugMode, Array)
60
+
61
+ const str = debugMode.toString()
62
+ const compiled = fjs.restore(str)
63
+ const tobe = JSON.stringify({ str: 'Foo' })
64
+ t.deepEquals(compiled({ str: 'Foo', void: 'me' }), tobe)
65
+ })
package/test/ref.test.js CHANGED
@@ -820,7 +820,7 @@ test('ref external to relative definition', (t) => {
820
820
 
821
821
  const schema = {
822
822
  type: 'object',
823
- required: ['foo'],
823
+ required: ['fooParent'],
824
824
  properties: {
825
825
  fooParent: { $ref: 'relative:to:local' }
826
826
  }
@@ -934,7 +934,7 @@ test('Bad key', t => {
934
934
  })
935
935
  t.fail('Should throw')
936
936
  } catch (err) {
937
- t.is(err.message, "Cannot find reference 'porjectId', did you mean 'projectId'?")
937
+ t.is(err.message, 'Cannot find reference "porjectId", did you mean "projectId"?')
938
938
  }
939
939
  })
940
940
 
@@ -959,7 +959,7 @@ test('Bad key', t => {
959
959
  })
960
960
  t.fail('Should throw')
961
961
  } catch (err) {
962
- t.is(err.message, "Cannot find reference 'foobar'")
962
+ t.is(err.message, 'Cannot find reference "foobar"')
963
963
  }
964
964
  })
965
965
 
@@ -989,7 +989,7 @@ test('Bad key', t => {
989
989
  })
990
990
  t.fail('Should throw')
991
991
  } catch (err) {
992
- t.is(err.message, "Cannot find reference 'porjectId', did you mean 'projectId'?")
992
+ t.is(err.message, 'Cannot find reference "porjectId", did you mean "projectId"?')
993
993
  }
994
994
  })
995
995
 
@@ -1019,7 +1019,7 @@ test('Bad key', t => {
1019
1019
  })
1020
1020
  t.fail('Should throw')
1021
1021
  } catch (err) {
1022
- t.is(err.message, "Cannot find reference 'foobar'")
1022
+ t.is(err.message, 'Cannot find reference "foobar"')
1023
1023
  }
1024
1024
  })
1025
1025
 
@@ -1049,7 +1049,7 @@ test('Bad key', t => {
1049
1049
  })
1050
1050
  t.fail('Should throw')
1051
1051
  } catch (err) {
1052
- t.is(err.message, "Cannot find reference 'deifnitions', did you mean 'definitions'?")
1052
+ t.is(err.message, 'Cannot find reference "deifnitions", did you mean "definitions"?')
1053
1053
  }
1054
1054
  })
1055
1055
 
@@ -1074,7 +1074,7 @@ test('Bad key', t => {
1074
1074
  })
1075
1075
  t.fail('Should throw')
1076
1076
  } catch (err) {
1077
- t.is(err.message, "Cannot find reference 'deifnitions', did you mean 'definitions'?")
1077
+ t.is(err.message, 'Cannot find reference "deifnitions", did you mean "definitions"?')
1078
1078
  }
1079
1079
  })
1080
1080
 
@@ -1104,7 +1104,7 @@ test('Bad key', t => {
1104
1104
  })
1105
1105
  t.fail('Should throw')
1106
1106
  } catch (err) {
1107
- t.is(err.message, "Cannot find reference 'extrenal', did you mean 'external'?")
1107
+ t.is(err.message, 'Cannot find reference "extrenal", did you mean "external"?')
1108
1108
  }
1109
1109
  })
1110
1110
 
@@ -36,11 +36,186 @@ test('object with required field', (t) => {
36
36
  })
37
37
  t.fail()
38
38
  } catch (e) {
39
- t.is(e.message, 'str is required!')
39
+ t.is(e.message, '"str" is required!')
40
40
  t.pass()
41
41
  }
42
42
  })
43
43
 
44
+ test('object with required field not in properties schema', (t) => {
45
+ t.plan(4)
46
+
47
+ const schema = {
48
+ title: 'object with required field',
49
+ type: 'object',
50
+ properties: {
51
+ num: {
52
+ type: 'integer'
53
+ }
54
+ },
55
+ required: ['str']
56
+ }
57
+ const stringify = build(schema)
58
+
59
+ try {
60
+ stringify({})
61
+ t.fail()
62
+ } catch (e) {
63
+ t.is(e.message, '"str" is required!')
64
+ t.pass()
65
+ }
66
+
67
+ try {
68
+ stringify({
69
+ num: 42
70
+ })
71
+ t.fail()
72
+ } catch (e) {
73
+ t.is(e.message, '"str" is required!')
74
+ t.pass()
75
+ }
76
+ })
77
+
78
+ test('object with required field not in properties schema with additional properties true', (t) => {
79
+ t.plan(4)
80
+
81
+ const schema = {
82
+ title: 'object with required field',
83
+ type: 'object',
84
+ properties: {
85
+ num: {
86
+ type: 'integer'
87
+ }
88
+ },
89
+ additionalProperties: true,
90
+ required: ['str']
91
+ }
92
+ const stringify = build(schema)
93
+
94
+ try {
95
+ stringify({})
96
+ t.fail()
97
+ } catch (e) {
98
+ t.is(e.message, '"str" is required!')
99
+ t.pass()
100
+ }
101
+
102
+ try {
103
+ stringify({
104
+ num: 42
105
+ })
106
+ t.fail()
107
+ } catch (e) {
108
+ t.is(e.message, '"str" is required!')
109
+ t.pass()
110
+ }
111
+ })
112
+
113
+ test('object with multiple required field not in properties schema', (t) => {
114
+ t.plan(6)
115
+
116
+ const schema = {
117
+ title: 'object with required field',
118
+ type: 'object',
119
+ properties: {
120
+ num: {
121
+ type: 'integer'
122
+ }
123
+ },
124
+ additionalProperties: true,
125
+ required: ['num', 'key1', 'key2']
126
+ }
127
+ const stringify = build(schema)
128
+
129
+ try {
130
+ stringify({})
131
+ t.fail()
132
+ } catch (e) {
133
+ t.is(e.message, '"num" is required!')
134
+ t.pass()
135
+ }
136
+
137
+ try {
138
+ stringify({
139
+ num: 42
140
+ })
141
+ t.fail()
142
+ } catch (e) {
143
+ t.is(e.message, '"key1" is required!')
144
+ t.pass()
145
+ }
146
+
147
+ try {
148
+ stringify({
149
+ num: 42,
150
+ key1: 'some'
151
+ })
152
+ t.fail()
153
+ } catch (e) {
154
+ t.is(e.message, '"key2" is required!')
155
+ t.pass()
156
+ }
157
+ })
158
+
159
+ test('object with required bool', (t) => {
160
+ t.plan(3)
161
+
162
+ const schema = {
163
+ title: 'object with required field',
164
+ type: 'object',
165
+ properties: {
166
+ num: {
167
+ type: 'integer'
168
+ }
169
+ },
170
+ additionalProperties: true,
171
+ required: ['bool']
172
+ }
173
+ const stringify = build(schema)
174
+
175
+ try {
176
+ stringify({})
177
+ t.fail()
178
+ } catch (e) {
179
+ t.is(e.message, '"bool" is required!')
180
+ t.pass()
181
+ }
182
+
183
+ try {
184
+ stringify({
185
+ bool: false
186
+ })
187
+ t.pass()
188
+ } catch (e) {
189
+ t.fail(e.message)
190
+ }
191
+ })
192
+
193
+ test('required nullable', (t) => {
194
+ t.plan(1)
195
+
196
+ const schema = {
197
+ title: 'object with required field',
198
+ type: 'object',
199
+ properties: {
200
+ num: {
201
+ type: ['integer']
202
+ }
203
+ },
204
+ additionalProperties: true,
205
+ required: ['null']
206
+ }
207
+ const stringify = build(schema)
208
+
209
+ try {
210
+ stringify({
211
+ null: null
212
+ })
213
+ t.pass()
214
+ } catch (e) {
215
+ t.fail(e.message)
216
+ }
217
+ })
218
+
44
219
  test('required numbers', (t) => {
45
220
  t.plan(3)
46
221
 
@@ -74,7 +249,7 @@ test('required numbers', (t) => {
74
249
  })
75
250
  t.fail()
76
251
  } catch (e) {
77
- t.is(e.message, 'num is required!')
252
+ t.is(e.message, '"num" is required!')
78
253
  t.pass()
79
254
  }
80
255
  })
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ const t = require('tap')
4
+ const build = require('..')
5
+
6
+ const payload = '(throw "pwoned")'
7
+
8
+ const stringify = build({
9
+ properties: {
10
+ [`*///\\\\\\']);${payload};{/*`]: {
11
+ type: 'number'
12
+ }
13
+ }
14
+ })
15
+
16
+ stringify({})
17
+
18
+ t.pass('no crashes')
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ const t = require('tap')
4
+ const build = require('..')
5
+
6
+ const stringify = build({
7
+ $defs: {
8
+ type: 'foooo"bar'
9
+ },
10
+ patternProperties: {
11
+ x: { $ref: '#/$defs' }
12
+ }
13
+ })
14
+
15
+ t.throws(() => {
16
+ stringify({ x: 0 })
17
+ }, 'Cannot coerce 0 to "foo"bar"')
@@ -0,0 +1,14 @@
1
+ 'use strict'
2
+
3
+ const t = require('tap')
4
+ const build = require('..')
5
+
6
+ const payload = '(throw "pwoned")'
7
+
8
+ const stringify = build({
9
+ required: [`"];${payload}//`]
10
+ })
11
+
12
+ t.throws(() => {
13
+ stringify({})
14
+ }, 'Error: ""];(throw "pwoned")//" is required!')
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ const t = require('tap')
4
+ const build = require('..')
5
+
6
+ const payload = '(throw "pwoned")'
7
+
8
+ const expected = 'Error: Invalid regular expression: /*/: Nothing to repeat. Found at * matching {"type":"*/(throw \\"pwoned\\")){//"}'
9
+
10
+ t.throws(() => {
11
+ build({
12
+ patternProperties: {
13
+ '*': { type: `*/${payload}){//` }
14
+ }
15
+ })
16
+ }, expected)
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ const t = require('tap')
4
+ const build = require('..')
5
+
6
+ const payload = '(throw "pwoned")'
7
+
8
+ const stringify = build({
9
+ type: 'object',
10
+ properties: {
11
+ '/*': { type: 'object' },
12
+ x: {
13
+ type: 'object',
14
+ properties: {
15
+ a: { type: 'string', default: `*/}${payload};{//` }
16
+ }
17
+ }
18
+ }
19
+ })
20
+ stringify({})
21
+
22
+ t.pass('no crashes')