diff --git a/README.md b/README.md
index 596e6bd2..a49b4c7a 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,20 @@ JSON.stringify obj x 1,763,980 ops/sec ±1.30% (88 runs sampled)
fast-json-stringify obj x 5,085,148 ops/sec ±1.56% (89 runs sampled)
```
+#### Table of contents:
+- `Example`
+- `API`
+ - `fastJsonStringify`
+ - `Specific use cases`
+ - `Required`
+ - `Missing fields`
+ - `Pattern Properties`
+ - `Additional Properties`
+- `Acknowledgements`
+- `License`
+
+
+
## Example
```js
@@ -48,9 +62,9 @@ console.log(stringify({
reg: /"([^"]|\\")*"/
}))
```
-
+
## API
-
+
### fastJsonStringify(schema)
Build a `stringify()` function based on
@@ -68,6 +82,7 @@ Supported types:
And nested ones, too.
+
#### Specific use cases
| Instance | Serialized as |
@@ -75,6 +90,7 @@ And nested ones, too.
| `Date` | `string` via `toISOString()` |
| `RegExp` | `string` |
+
#### Required
You can set specific fields of an object as required in your schema, by adding the field name inside the `required` array in your schema.
Example:
@@ -95,6 +111,7 @@ const schema = {
```
If the object to stringify has not the required field(s), `fast-json-stringify` will throw an error.
+
#### Missing fields
If a field *is present* in the schema (and is not required) but it *is not present* in the object to stringify, `fast-json-stringify` will not write it in the final string.
Example:
@@ -109,8 +126,7 @@ const stringify = fastJson({
mail: {
type: 'string'
}
- },
- required: ['mail']
+ }
})
const obj = {
@@ -120,6 +136,7 @@ const obj = {
console.log(stringify(obj)) // '{"mail":"mail@example.com"}'
```
+
#### Pattern properties
`fast-json-stringify` supports pattern properties as defined inside JSON schema.
*patternProperties* must be an object, where the key is a valid regex and the value is an object, declared in this way: `{ type: 'type' }`.
@@ -151,13 +168,58 @@ const obj = {
matchnum: 3
}
-console.log(stringify(obj)) // '{"nickname":"nick","matchfoo":"42","otherfoo":"str","matchnum":3}'
+console.log(stringify(obj)) // '{"matchfoo":"42","otherfoo":"str","matchnum":3,"nickname":"nick"}'
+```
+
+
+#### Additional properties
+`fast-json-stringify` supports additional properties as defined inside JSON schema.
+*additionalProperties* must be an object or a boolean, declared in this way: `{ type: 'type' }`.
+*additionalProperties* will work only for the properties that are not explicitly listed in the *properties* and *patternProperties* objects.
+
+If *additionalProperties* is not present or is setted to false, every property that is not explicitly listed in the *properties* and *patternProperties* objects, will be ignored, as said in Missing fields.
+If *additionalProperties* is setted to *true*, it will be used `fast-safe-stringify` to stringify the additional properties. If you want to achieve maximum performances we strongly encourage you to use a fixed schema where possible.
+Example:
+```javascript
+const stringify = fastJson({
+ title: 'Example Schema',
+ type: 'object',
+ properties: {
+ nickname: {
+ type: 'string'
+ }
+ },
+ patternProperties: {
+ 'num': {
+ type: 'number'
+ },
+ '.*foo$': {
+ type: 'string'
+ }
+ },
+ additionalProperties: {
+ type: 'string'
+ }
+})
+
+const obj = {
+ nickname: 'nick',
+ matchfoo: 42,
+ otherfoo: 'str'
+ matchnum: 3,
+ nomatchstr: 'valar morghulis',
+ nomatchint: 313
+}
+
+console.log(stringify(obj)) // '{"matchfoo":"42","otherfoo":"str","matchnum":3,"nomatchstr":"valar morghulis",nomatchint:"313","nickname":"nick"}'
```
+
## Acknowledgements
This project was kindly sponsored by [nearForm](http://nearform.com).
+
## License
MIT
diff --git a/example.js b/example.js
index d6a60d6d..1835f52e 100644
--- a/example.js
+++ b/example.js
@@ -49,6 +49,9 @@ const stringify = fastJson({
'test': {
type: 'number'
}
+ },
+ additionalProperties: {
+ type: 'string'
}
})
@@ -63,5 +66,8 @@ console.log(stringify({
test: 42,
strtest: '23',
arr: [{ str: 'stark' }, { str: 'lannister' }],
- obj: { bool: true }
+ obj: { bool: true },
+ notmatch: 'valar morghulis',
+ notmatchobj: { a: true },
+ notmatchnum: 42
}))
diff --git a/index.js b/index.js
index 760f497b..f6fdbaf9 100644
--- a/index.js
+++ b/index.js
@@ -1,5 +1,7 @@
'use strict'
+const fastSafeStringify = require('fast-safe-stringify')
+
function build (schema) {
/* eslint no-new-func: "off" */
var code = `
@@ -51,7 +53,9 @@ function build (schema) {
;
return ${main}
`
-
+ if (schema.additionalProperties === true) {
+ return (new Function('fastSafeStringify', code))(fastSafeStringify)
+ }
return (new Function(code))()
}
@@ -137,7 +141,7 @@ function $asRegExp (reg) {
return '"' + reg + '"'
}
-function addPatternProperties (pp) {
+function addPatternProperties (pp, ap) {
let code = `
var keys = Object.keys(obj)
for (var i = 0; i < keys.length; i++) {
@@ -176,27 +180,87 @@ function addPatternProperties (pp) {
`
} else {
code += `
- throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ${type}')
+ throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ${type}')
`
}
+
code += `
+ continue
}
`
})
+ if (ap) {
+ code += additionalProperty(ap)
+ }
+
code += `
}
- if (Object.keys(properties).length === 0) json = json.substring(0, json.length - 1)
`
return code
}
+function additionalProperty (ap) {
+ let code = ''
+ if (ap === true) {
+ return `
+ json += $asString(keys[i]) + ':' + fastSafeStringify(obj[keys[i]]) + ','
+ `
+ }
+ let type = ap.type
+ if (type === 'object') {
+ code += buildObject(ap, '', 'buildObjectAP')
+ code += `
+ json += $asString(keys[i]) + ':' + buildObjectAP(obj[keys[i]]) + ','
+ `
+ } else if (type === 'array') {
+ code += buildArray(ap, '', 'buildArrayAP')
+ code += `
+ json += $asString(keys[i]) + ':' + buildArrayAP(obj[keys[i]]) + ','
+ `
+ } else if (type === 'null') {
+ code += `
+ json += $asString(keys[i]) +':null,'
+ `
+ } else if (type === 'string') {
+ code += `
+ json += $asString(keys[i]) + ':' + $asString(obj[keys[i]]) + ','
+ `
+ } else if (type === 'number' || type === 'integer') {
+ code += `
+ json += $asString(keys[i]) + ':' + $asNumber(obj[keys[i]]) + ','
+ `
+ } else if (type === 'boolean') {
+ code += `
+ json += $asString(keys[i]) + ':' + $asBoolean(obj[keys[i]]) + ','
+ `
+ } else {
+ code += `
+ throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ${type}')
+ `
+ }
+ return code
+}
+
+function addAdditionalProperties (ap) {
+ return `
+ var keys = Object.keys(obj)
+ for (var i = 0; i < keys.length; i++) {
+ if (properties[keys[i]]) continue
+ ${additionalProperty(ap)}
+ }
+ `
+}
+
function buildObject (schema, code, name) {
code += `
function ${name} (obj) {
var json = '{'
`
+
if (schema.patternProperties) {
- code += addPatternProperties(schema.patternProperties)
+ code += addPatternProperties(schema.patternProperties, schema.additionalProperties)
+ } else if (schema.additionalProperties && !schema.patternProperties) {
+ code += addAdditionalProperties(schema.additionalProperties)
}
var laterCode = ''
@@ -232,7 +296,9 @@ function buildObject (schema, code, name) {
`
})
+ // Removes the comma if is the last element of the string (in case there are not properties)
code += `
+ if (json[json.length - 1] === ',') json = json.substring(0, json.length - 1)
json += '}'
return json
}
diff --git a/package.json b/package.json
index ef86878a..7bcb6c9f 100644
--- a/package.json
+++ b/package.json
@@ -29,5 +29,8 @@
"pre-commit": "^1.1.3",
"standard": "^8.2.0",
"tap": "^7.1.2"
+ },
+ "dependencies": {
+ "fast-safe-stringify": "^1.1.0"
}
}
diff --git a/test/additionalProperties.test.js b/test/additionalProperties.test.js
new file mode 100644
index 00000000..783374e1
--- /dev/null
+++ b/test/additionalProperties.test.js
@@ -0,0 +1,182 @@
+'use strict'
+
+const test = require('tap').test
+const build = require('..')
+
+test('additionalProperties', (t) => {
+ t.plan(1)
+ const stringify = build({
+ title: 'additionalProperties',
+ type: 'object',
+ properties: {
+ str: {
+ type: 'string'
+ }
+ },
+ additionalProperties: {
+ type: 'string'
+ }
+ })
+
+ let obj = { str: 'test', foo: 42, ofoo: true, foof: 'string', objfoo: {a: true} }
+ t.equal('{"foo":"42","ofoo":"true","foof":"string","objfoo":"[object Object]","str":"test"}', stringify(obj))
+})
+
+test('additionalProperties should not change properties', (t) => {
+ t.plan(1)
+ const stringify = build({
+ title: 'patternProperties should not change properties',
+ type: 'object',
+ properties: {
+ foo: {
+ type: 'string'
+ }
+ },
+ additionalProperties: {
+ type: 'number'
+ }
+ })
+
+ const obj = { foo: '42', ofoo: 42 }
+ t.equal('{"ofoo":42,"foo":"42"}', stringify(obj))
+})
+
+test('additionalProperties should not change properties and patternProperties', (t) => {
+ t.plan(1)
+ const stringify = build({
+ title: 'patternProperties should not change properties',
+ type: 'object',
+ properties: {
+ foo: {
+ type: 'string'
+ }
+ },
+ patternProperties: {
+ foo: {
+ type: 'string'
+ }
+ },
+ additionalProperties: {
+ type: 'number'
+ }
+ })
+
+ const obj = { foo: '42', ofoo: 42, test: '42' }
+ t.equal('{"ofoo":"42","test":42,"foo":"42"}', stringify(obj))
+})
+
+test('additionalProperties set to true, use of fast-safe-stringify', (t) => {
+ t.plan(1)
+ const stringify = build({
+ title: 'check string coerce',
+ type: 'object',
+ properties: {},
+ additionalProperties: true
+ })
+
+ const obj = { foo: true, ofoo: 42, arrfoo: ['array', 'test'], objfoo: { a: 'world' } }
+ t.equal('{"foo":true,"ofoo":42,"arrfoo":["array","test"],"objfoo":{"a":"world"}}', stringify(obj))
+})
+
+test('additionalProperties - string coerce', (t) => {
+ t.plan(1)
+ const stringify = build({
+ title: 'check string coerce',
+ type: 'object',
+ properties: {},
+ additionalProperties: {
+ type: 'string'
+ }
+ })
+
+ const obj = { foo: true, ofoo: 42, arrfoo: ['array', 'test'], objfoo: { a: 'world' } }
+ t.equal('{"foo":"true","ofoo":"42","arrfoo":"array,test","objfoo":"[object Object]"}', stringify(obj))
+})
+
+test('additionalProperties - number coerce', (t) => {
+ t.plan(1)
+ const stringify = build({
+ title: 'check number coerce',
+ type: 'object',
+ properties: {},
+ additionalProperties: {
+ type: 'number'
+ }
+ })
+
+ const obj = { foo: true, ofoo: '42', xfoo: 'string', arrfoo: [1, 2], objfoo: { num: 42 } }
+ t.equal('{"foo":1,"ofoo":42,"xfoo":null,"arrfoo":null,"objfoo":null}', stringify(obj))
+})
+
+test('additionalProperties - boolean coerce', (t) => {
+ t.plan(1)
+ const stringify = build({
+ title: 'check boolean coerce',
+ type: 'object',
+ properties: {},
+ additionalProperties: {
+ type: 'boolean'
+ }
+ })
+
+ const obj = { foo: 'true', ofoo: 0, arrfoo: [1, 2], objfoo: { a: true } }
+ t.equal('{"foo":true,"ofoo":false,"arrfoo":true,"objfoo":true}', stringify(obj))
+})
+
+test('additionalProperties - object coerce', (t) => {
+ t.plan(1)
+ const stringify = build({
+ title: 'check object coerce',
+ type: 'object',
+ properties: {},
+ additionalProperties: {
+ type: 'object',
+ properties: {
+ answer: {
+ type: 'number'
+ }
+ }
+ }
+ })
+
+ const obj = { objfoo: { answer: 42 } }
+ t.equal('{"objfoo":{"answer":42}}', stringify(obj))
+})
+
+test('additionalProperties - array coerce', (t) => {
+ t.plan(1)
+ const stringify = build({
+ title: 'check array coerce',
+ type: 'object',
+ properties: {},
+ additionalProperties: {
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ }
+ })
+
+ const obj = { foo: 'true', ofoo: 0, arrfoo: [1, 2], objfoo: { tyrion: 'lannister' } }
+ t.equal('{"foo":["t","r","u","e"],"ofoo":[],"arrfoo":["1","2"],"objfoo":[]}', stringify(obj))
+})
+
+test('additionalProperties - throw on unknown type', (t) => {
+ t.plan(1)
+ const stringify = build({
+ title: 'check array coerce',
+ type: 'object',
+ properties: {},
+ additionalProperties: {
+ type: 'strangetype'
+ }
+ })
+
+ const obj = { foo: 'true', ofoo: 0, arrfoo: [1, 2], objfoo: { tyrion: 'lannister' } }
+ try {
+ stringify(obj)
+ t.fail()
+ } catch (e) {
+ t.pass()
+ }
+})