├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── SECURITY.md ├── benchmark.js ├── example.js ├── index.js ├── lib ├── any.js ├── array.js ├── boolean.js ├── null.js ├── number.js ├── object.js ├── ops.js ├── property.js ├── schema.js ├── similar.js ├── string.js └── switch.js ├── package.json └── test ├── random.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sandbox.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 8 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # turbo-json-parse 2 | 3 | Turbocharged JSON.parse for type stable JSON data. 4 | 5 | ``` 6 | npm install turbo-json-parse 7 | ``` 8 | 9 | Experiment, but seems to work quite well already 10 | and is really fast assuming your JSON is type stable. 11 | 12 | ## Usage 13 | 14 | ``` js 15 | const compile = require('turbo-json-parse') 16 | 17 | // Pass in a JSON schema 18 | // Note that only a subset of the schema is supported at the moment 19 | // including all of the type information, but excluding stuff like anyOf 20 | // PR welcome to expand to support :) 21 | 22 | const parse = compile({ 23 | type: 'object', 24 | properties: { 25 | hello: {type: 'string'}, 26 | num: {type: 'number'}, 27 | flag: {type: 'boolean'}, 28 | flags: {type: 'array', items: {type: 'boolean'}}, 29 | nested: { 30 | type: 'object', 31 | properties: { 32 | more: {type: 'string'} 33 | } 34 | } 35 | } 36 | }) 37 | 38 | const ex = JSON.stringify({ 39 | hello: 'world' 40 | }) 41 | 42 | // will return {hello: 'world'} 43 | console.log(parse(ex)) 44 | ``` 45 | 46 | ## API 47 | 48 | #### `const parse = compile(schema, [options])` 49 | 50 | Make a new turbo charged JSON parser based on the type schema provided. 51 | The type schema should have a similar syntax to the above example. 52 | 53 | The parser is only able to parse objects that look like the schema, 54 | in terms of the types provided and the order of keys. 55 | 56 | Options include: 57 | 58 | ```js 59 | { 60 | buffer: false, // set to true if you are parsing from buffers instead of strings 61 | required: false, // set to true if all properties are required 62 | ordered: false, // set to true if your properties have the same order always 63 | validate: true, // set to false to disable extra type validation 64 | validateStrings: true, // set to false to disable extra type validation 65 | fullMatch: true, // set to false to do fastest match based on the schema (unsafe!) 66 | unescapeStrings: true, // set to false if you don't need to unescape \ chars 67 | defaults: true // set to false to disable setting of default properties 68 | prettyPrinted: false // set to true to parse json formatted with JSON.stringify(x, null, 2) 69 | } 70 | ``` 71 | 72 | If you trust your input setting `unsafe` to `true` will gain you extra performance, at the cost of some important validation logic. 73 | 74 | If you have the underlying buffer, set `buffer` to true and pass the buffer instead of the string to parse 75 | 76 | ```js 77 | const parse = compile(..., {buffer: true}) 78 | const data = parse(buffer) // parse buffer instead of string 79 | ``` 80 | 81 | This will speed up the parsing by 2-3x as well. 82 | 83 | #### `parse = compile.from(obj, [options])` 84 | 85 | Generate a parser based on the type information from an existing object. 86 | 87 | ## Performance 88 | 89 | If your JSON data follows the heuristics described above this parser can be very fast. 90 | 91 | On the included benchmark this is 5x faster than JSON parse on my machine, YMMV. 92 | 93 | ## How does this work? 94 | 95 | This works by taking the schema of the data and generating a specific JSON parser for exactly that schema. 96 | You can actually view the source code of the generated parser by doing `parse.toString()` after compiling it. 97 | 98 | This is much faster than parsing for a generic object, as the schema information helps the parser know what 99 | it is looking for, which is why this is faster to JSON.parse. 100 | 101 | ## Related 102 | 103 | See [jitson](https://github.com/mafintosh/jitson) for a Just-In-Time JSON.parse compiler 104 | that uses this module when the incoming JSON is stable and falls back to JSON.parse when not. 105 | 106 | ## License 107 | 108 | MIT 109 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the 4 | [Tidelift security contact](https://tidelift.com/security). 5 | Tidelift will coordinate the fix and disclosure. 6 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | const compile = require('./') 2 | 3 | const dave = { 4 | checked: true, 5 | checker: false, 6 | dimensions: { 7 | height: 10, 8 | width: 5 9 | }, 10 | id: 1, 11 | name: 'A green door', 12 | price: 12 13 | } 14 | 15 | const s = JSON.stringify(dave) 16 | const b = Buffer.from(s) 17 | const cnt = 1e7 18 | 19 | for (let o = 0; o < 3; o++) { 20 | const veryFast = o > 0 21 | const safe = o < 2 22 | const opts = {ordered: veryFast, required: veryFast, unescapeStrings: !veryFast, fullMatch: safe, validate: safe} 23 | const parseNoBuf = compile(compile.inferRawSchema(dave, opts), opts) 24 | const parse = compile(compile.inferRawSchema(dave, opts), Object.assign({}, opts, {buffer: true})) 25 | 26 | if (o) console.log() 27 | console.log('Compiling with', opts) 28 | console.log('One parse', parseNoBuf(s)) 29 | console.log('\nBenching from string\n') 30 | for (let r = 0; r < 2; r++) { 31 | console.log('Run ' + r) 32 | console.time('Benching turbo-json-parse from string') 33 | for (let i = 0; i < cnt; i++) { 34 | parseNoBuf(s) 35 | } 36 | console.timeEnd('Benching turbo-json-parse from string') 37 | console.time('Benching JSON.parse from string') 38 | for (let i = 0; i < cnt; i++) { 39 | JSON.parse(s) 40 | } 41 | console.timeEnd('Benching JSON.parse from string') 42 | } 43 | console.log('\nBenching from buffer\n') 44 | for (let r = 0; r < 2; r++) { 45 | console.log('Run ' + r) 46 | console.time('Benching turbo-json-parse from buffer') 47 | for (let i = 0; i < cnt; i++) { 48 | parse(b) 49 | } 50 | console.timeEnd('Benching turbo-json-parse from buffer') 51 | console.time('Benching JSON.parse from buffer') 52 | for (let i = 0; i < cnt; i++) { 53 | JSON.parse(b) 54 | } 55 | console.timeEnd('Benching JSON.parse from buffer') 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const compile = require('./') 2 | 3 | // pass in a type schema 4 | const parse = compile.from({ 5 | hello: 'string', 6 | num: 42, 7 | null: null, 8 | flag: true, 9 | flags: [true], 10 | nested: { 11 | more: 'string' 12 | } 13 | }) 14 | 15 | const ex = JSON.stringify({ 16 | hello: 'world' 17 | }) 18 | 19 | // will return {hello: 'world'} 20 | console.log(parse(ex)) 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const genfun = require('generate-function') 2 | const schema = require('./lib/schema') 3 | const anyDefaults = require('./lib/any') 4 | const ops = require('./lib/ops') 5 | 6 | exports = module.exports = compile 7 | exports.inferRawSchema = schema.inferRawSchema 8 | exports.jsonSchemaToRawSchema = schema.jsonSchemaToRawSchema 9 | exports.from = from 10 | 11 | function from (obj, opts) { 12 | return compile(schema.inferRawSchema(obj), opts) 13 | } 14 | 15 | function compile (jsonSchema, opts) { 16 | if (!opts) opts = {} 17 | 18 | const isRawSchema = typeof jsonSchema.type === 'number' 19 | const rawSchema = isRawSchema 20 | ? jsonSchema 21 | : schema.jsonSchemaToRawSchema(jsonSchema) 22 | 23 | const { name } = ops(opts) 24 | const any = anyDefaults(opts) 25 | 26 | const gen = genfun() 27 | gen.scope.console = console 28 | 29 | // just to reserve these symbols 30 | gen.sym(name) 31 | gen.sym('ptr') 32 | gen.sym('parseString') 33 | gen.sym('parseNumber') 34 | 35 | gen(`function parse (${name}, ptr) {`) 36 | gen('if (!ptr) ptr = 0') 37 | any(gen, null, rawSchema) 38 | gen('}') 39 | 40 | const parse = gen.toFunction() 41 | parse.pointer = 0 42 | return parse 43 | } 44 | -------------------------------------------------------------------------------- /lib/any.js: -------------------------------------------------------------------------------- 1 | const objectDefaults = require('./object') 2 | const numberDefaults = require('./number') 3 | const stringDefaults = require('./string') 4 | const booleanDefaults = require('./boolean') 5 | const arrayDefaults = require('./array') 6 | const nullDefaults = require('./null') 7 | const schema = require('./schema') 8 | 9 | module.exports = defaults 10 | 11 | function defaults (opts) { 12 | const compileString = stringDefaults(opts) 13 | const compileNumber = numberDefaults(opts) 14 | const compileBoolean = booleanDefaults(opts) 15 | const compileObject = objectDefaults(opts) 16 | const compileArray = arrayDefaults(opts) 17 | const compileNull = nullDefaults(opts) 18 | 19 | return compileAny 20 | 21 | function compileAny (gen, prop, rawSchema) { 22 | switch (rawSchema.type) { 23 | case schema.STRING: 24 | compileString(gen, prop) 25 | break 26 | 27 | case schema.NUMBER: 28 | compileNumber(gen, prop) 29 | break 30 | 31 | case schema.BOOLEAN: 32 | compileBoolean(gen, prop) 33 | break 34 | 35 | case schema.OBJECT: 36 | compileObject(gen, prop, rawSchema, compileAny) 37 | break 38 | 39 | case schema.NULL: 40 | compileNull(gen, prop) 41 | break 42 | 43 | case schema.ARRAY: 44 | compileArray(gen, prop, rawSchema, compileAny) 45 | break 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/array.js: -------------------------------------------------------------------------------- 1 | const ops = require('./ops') 2 | const Property = require('./property') 3 | 4 | module.exports = defaults 5 | 6 | function defaults (opts) { 7 | const { ch, code } = ops(opts) 8 | 9 | const allowEmptyArrays = opts.allowEmptyArrays !== false 10 | 11 | return compileArray 12 | 13 | function compileArray (gen, prop, rawSchema, compileAny) { 14 | const assigning = !prop || !prop.getable 15 | const allowEmpty = rawSchema.allowEmpty !== false && allowEmptyArrays 16 | 17 | const a = assigning ? gen.sym(rawSchema.name || 'arr') : prop.get() 18 | 19 | if (assigning) { 20 | gen(`const ${a} = []`) 21 | } 22 | 23 | if (allowEmpty) { 24 | gen(` 25 | if (${ch('ptr + 1')} === ${code(']')}) { 26 | ptr += 2 27 | } else { 28 | `) 29 | } 30 | 31 | gen(`while (${ch('ptr++')} !== ${code(']')}) {`) 32 | 33 | if (!rawSchema.items) { 34 | gen(`throw new Error('Unknown array type')`) 35 | } else { 36 | const arrProp = new Property(gen, a, rawSchema.items) 37 | compileAny(gen, arrProp, rawSchema.items) 38 | } 39 | 40 | gen('}') 41 | 42 | if (allowEmpty) { 43 | gen('}') 44 | } 45 | 46 | if (!prop) gen(`return ${a}`) 47 | else if (assigning) prop.set(a) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/boolean.js: -------------------------------------------------------------------------------- 1 | const ops = require('./ops') 2 | 3 | module.exports = defaults 4 | 5 | function defaults (opts) { 6 | const { ch, code } = ops(opts) 7 | const fullMatch = opts.fullMatch !== false 8 | 9 | return compileBoolean 10 | 11 | function compileBoolean (gen, prop) { 12 | if (fullMatch) { 13 | if (prop) { 14 | gen(` 15 | switch (${ch('ptr')}) { 16 | case ${code('t')}: 17 | ${prop.set('true', true)} 18 | ptr += 4 19 | break 20 | case ${code('f')}: 21 | ${prop.set('false', true)} 22 | ptr += 5 23 | break 24 | default: 25 | throw new Error("Unexpected token in boolean") 26 | } 27 | `) 28 | } else { 29 | gen(` 30 | switch (${ch('ptr')}) { 31 | case ${code('t')}: 32 | parse.pointer = ptr + 4 33 | return true 34 | case ${code('f')}: 35 | parse.pointer = ptr + 5 36 | return false 37 | default: 38 | throw new Error("Unexpected token in boolean") 39 | } 40 | `) 41 | } 42 | } else { 43 | if (prop) { 44 | gen(` 45 | if (${ch('ptr')} === ${code('t')}) { 46 | ${prop.set('true', true)} 47 | ptr += 4 48 | } else { 49 | ${prop.set('false', true)} 50 | ptr += 5 51 | } 52 | `) 53 | } else { 54 | gen(` 55 | if(${ch('ptr')} === ${code('t')}) { 56 | parse.pointer = ptr + 4 57 | return true 58 | } 59 | parse.pointer = ptr + 5 60 | return false 61 | `) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/null.js: -------------------------------------------------------------------------------- 1 | const ops = require('./ops') 2 | 3 | module.exports = defaults 4 | 5 | function defaults (opts) { 6 | const { ch, code } = ops(opts) 7 | 8 | return compileNull 9 | 10 | function compileNull (gen, prop) { 11 | if (prop) { 12 | gen(` 13 | if (${ch('ptr')} === ${code('null')}) { 14 | ${prop.set('null', true)} 15 | ptr += 4 16 | } else { 17 | throw new Error('Expected null') 18 | } 19 | `) 20 | } else { 21 | gen(` 22 | if(${ch('ptr')} === ${code('null')}) { 23 | parse.pointer = ptr + 4 24 | return null 25 | } 26 | throw new Error('Expected null') 27 | `) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/number.js: -------------------------------------------------------------------------------- 1 | const ops = require('./ops') 2 | 3 | parseNumberString.pointer = 0 4 | parseNumberBuffer.pointer = 0 5 | 6 | module.exports = defaults 7 | 8 | function defaults (opts) { 9 | const { buffer } = opts 10 | const { name } = ops(opts) 11 | 12 | return compileNumber 13 | 14 | function compileNumber (gen, prop) { 15 | if (!gen.scope.parseNumber) { 16 | gen.scope.parseNumber = buffer 17 | ? parseNumberBuffer 18 | : parseNumberString 19 | } 20 | 21 | if (prop) { 22 | prop.set(`parseNumber(${name}, ptr)`) 23 | gen('ptr = parseNumber.pointer') 24 | } else { 25 | const num = gen.sym('num') 26 | gen(` 27 | const ${num} = parseNumber(${name}, ptr) 28 | parse.pointer = parseNumber.pointer 29 | return ${num} 30 | `) 31 | } 32 | } 33 | } 34 | 35 | function parseFloatString (n, buf, ptr) { 36 | var num = 0 37 | var div = 1 38 | while (ptr < buf.length) { 39 | div *= 10 40 | switch (buf.charCodeAt(ptr++)) { 41 | case 48: 42 | num = num * 10 43 | continue 44 | case 49: 45 | num = num * 10 + 1 46 | continue 47 | case 50: 48 | num = num * 10 + 2 49 | continue 50 | case 51: 51 | num = num * 10 + 3 52 | continue 53 | case 52: 54 | num = num * 10 + 4 55 | continue 56 | case 53: 57 | num = num * 10 + 5 58 | continue 59 | case 54: 60 | num = num * 10 + 6 61 | continue 62 | case 55: 63 | num = num * 10 + 7 64 | continue 65 | case 56: 66 | num = num * 10 + 8 67 | continue 68 | case 57: 69 | num = num * 10 + 9 70 | continue 71 | case 10: 72 | case 32: 73 | case 44: 74 | case 93: 75 | case 125: 76 | parseNumberString.pointer = ptr - 1 77 | div /= 10 78 | return (n * div + num) / div 79 | } 80 | 81 | throw new Error('Unexpected token in number') 82 | } 83 | 84 | return (n * div + num) / div 85 | } 86 | 87 | function parseNumberString (buf, ptr) { 88 | var num = 0 89 | var sign = 1 90 | 91 | if (buf.charCodeAt(ptr) === 45) { 92 | sign = -1 93 | ptr++ 94 | } 95 | 96 | while (ptr < buf.length) { 97 | switch (buf.charCodeAt(ptr++)) { 98 | case 46: 99 | return sign * parseFloatString(num, buf, ptr) 100 | case 48: 101 | num = num * 10 102 | continue 103 | case 49: 104 | num = num * 10 + 1 105 | continue 106 | case 50: 107 | num = num * 10 + 2 108 | continue 109 | case 51: 110 | num = num * 10 + 3 111 | continue 112 | case 52: 113 | num = num * 10 + 4 114 | continue 115 | case 53: 116 | num = num * 10 + 5 117 | continue 118 | case 54: 119 | num = num * 10 + 6 120 | continue 121 | case 55: 122 | num = num * 10 + 7 123 | continue 124 | case 56: 125 | num = num * 10 + 8 126 | continue 127 | case 57: 128 | num = num * 10 + 9 129 | continue 130 | case 10: 131 | case 32: 132 | case 44: 133 | case 93: 134 | case 125: 135 | parseNumberString.pointer = ptr - 1 136 | return sign * num 137 | } 138 | 139 | throw new Error('Unexpected token in number') 140 | } 141 | 142 | return sign * num 143 | } 144 | 145 | function parseFloatBuffer (n, buf, ptr) { 146 | var num = 0 147 | var div = 1 148 | while (ptr < buf.length) { 149 | div *= 10 150 | switch (buf[ptr++]) { 151 | case 48: 152 | num = num * 10 153 | continue 154 | case 49: 155 | num = num * 10 + 1 156 | continue 157 | case 50: 158 | num = num * 10 + 2 159 | continue 160 | case 51: 161 | num = num * 10 + 3 162 | continue 163 | case 52: 164 | num = num * 10 + 4 165 | continue 166 | case 53: 167 | num = num * 10 + 5 168 | continue 169 | case 54: 170 | num = num * 10 + 6 171 | continue 172 | case 55: 173 | num = num * 10 + 7 174 | continue 175 | case 56: 176 | num = num * 10 + 8 177 | continue 178 | case 57: 179 | num = num * 10 + 9 180 | continue 181 | case 10: 182 | case 32: 183 | case 44: 184 | case 93: 185 | case 125: 186 | parseNumberBuffer.pointer = ptr - 1 187 | div /= 10 188 | return (n * div + num) / div 189 | } 190 | 191 | throw new Error('Unexpected token in number') 192 | } 193 | 194 | return (n * div + num) / div 195 | } 196 | 197 | function parseNumberBuffer (buf, ptr) { 198 | var num = 0 199 | var sign = 1 200 | 201 | if (buf[ptr] === 45) { 202 | sign = -1 203 | ptr++ 204 | } 205 | 206 | while (ptr < buf.length) { 207 | switch (buf[ptr++]) { 208 | case 46: 209 | return sign * parseFloatBuffer(num, buf, ptr) 210 | case 48: 211 | num = num * 10 212 | continue 213 | case 49: 214 | num = num * 10 + 1 215 | continue 216 | case 50: 217 | num = num * 10 + 2 218 | continue 219 | case 51: 220 | num = num * 10 + 3 221 | continue 222 | case 52: 223 | num = num * 10 + 4 224 | continue 225 | case 53: 226 | num = num * 10 + 5 227 | continue 228 | case 54: 229 | num = num * 10 + 6 230 | continue 231 | case 55: 232 | num = num * 10 + 7 233 | continue 234 | case 56: 235 | num = num * 10 + 8 236 | continue 237 | case 57: 238 | num = num * 10 + 9 239 | continue 240 | case 10: 241 | case 32: 242 | case 44: 243 | case 93: 244 | case 125: 245 | parseNumberBuffer.pointer = ptr - 1 246 | return sign * num 247 | } 248 | 249 | throw new Error('Unexpected token in number') 250 | } 251 | 252 | return sign * num 253 | } 254 | -------------------------------------------------------------------------------- /lib/object.js: -------------------------------------------------------------------------------- 1 | const switchDefaults = require('./switch') 2 | const opsDefaults = require('./ops') 3 | const Property = require('./property') 4 | const similar = require('./similar') 5 | const schema = require('./schema') 6 | 7 | module.exports = defaults 8 | 9 | function defaults (opts) { 10 | if (!opts) opts = {} 11 | 12 | const allowEmptyObjects = opts.allowEmptyObjects !== false 13 | const required = !!opts.allRequired 14 | const validate = opts.validate !== false 15 | const ordered = !!opts.ordered 16 | const fullMatch = opts.fullMatch !== false 17 | const defaults = opts.defaults !== false 18 | const prettyPrinted = !!opts.prettyPrinted 19 | 20 | const sw = switchDefaults(opts) 21 | const { eq, ch, code } = opsDefaults(opts) 22 | 23 | class Required { 24 | constructor (gen, o, fields) { 25 | this.name = o + 'Required' 26 | this.fields = validate ? fields.filter(isRequired) : [] 27 | this.count = this.fields.length 28 | this.gen = gen 29 | this.vars = [] 30 | } 31 | 32 | setup () { 33 | if (!this.count) return 34 | if (this.count === 1) { 35 | const sym = this.gen.sym(this.name) 36 | this.vars.push(sym) 37 | this.gen(`var ${sym} = false`) 38 | } else { 39 | const vars = Math.ceil(this.count / 32) 40 | for (var i = 0; i < vars; i++) { 41 | const sym = this.gen.sym(this.name) 42 | this.vars.push(sym) 43 | this.gen(`var ${sym} = 0`) 44 | } 45 | } 46 | } 47 | 48 | set (field) { 49 | if (!isRequired(field)) return 50 | const idx = this.fields.indexOf(field) 51 | const v = this.vars[Math.floor(idx / 32)] 52 | if (this.count === 1) { 53 | this.gen(`${v} = true`) 54 | } else { 55 | const mask = (1 << (idx & 31)) >>> 0 56 | this.gen(`${v} |= ${mask}`) 57 | } 58 | } 59 | 60 | mask (i) { 61 | const all = Math.pow(2, 32) - 1 62 | if (i < this.vars.length - 1) return all 63 | 64 | return this.count & 31 65 | ? (Math.pow(2, this.count & 31) - 1) >>> 0 66 | : all 67 | } 68 | 69 | validate () { 70 | if (!this.count) return 71 | if (this.count === 1) { 72 | this.gen(`if (!${this.vars[0]}) {`) 73 | } else { 74 | const vs = this.vars.map((v, i) => `${v} !== ${this.mask(i)}`).join(' || ') 75 | this.gen(`if (${vs}) {`) 76 | } 77 | this.gen(`throw new Error('Missing required key in object')`) 78 | this.gen('}') 79 | } 80 | } 81 | 82 | return compileObject 83 | 84 | function compileObject (gen, prop, rawSchema, genany) { 85 | const fields = rawSchema.fields || [] 86 | const assigning = !prop || !prop.getable 87 | const allowEmpty = !fields.some(isRequired) && 88 | rawSchema.allowEmpty !== false && 89 | allowEmptyObjects 90 | 91 | const o = assigning ? gen.sym(rawSchema.name || 'o') : prop.get() 92 | const reqs = new Required(gen, o, fields) 93 | 94 | if (assigning) { 95 | defaultObject(gen, o, rawSchema) 96 | } 97 | 98 | if (allowEmpty) { 99 | gen(` 100 | if (${ch('ptr + 1')} === ${code('}')}) { 101 | ptr += 2 102 | } else { 103 | `) 104 | } 105 | 106 | if (!fields.length) { 107 | gen(` 108 | throw new Error('Unexpected token in object') 109 | `) 110 | } else if (bool(rawSchema.ordered, ordered)) { 111 | genifs(gen, o, fields, genany) 112 | gen(` 113 | if (${ch('ptr++')} !== ${code('}')}) { 114 | throw new Error('Unexpected token in object') 115 | } 116 | `) 117 | } else { 118 | reqs.setup() 119 | gen(`while (${ch('ptr++')} !== ${code('}')}) {`) 120 | if (prettyPrinted) { 121 | gen(`if (${ch('ptr')} === ${code(' ')} || ${ch('ptr')} === ${code('\n')}) continue`) 122 | } 123 | 124 | sw(gen, 0, fields, genblk, gendef) 125 | gen('}') 126 | reqs.validate() 127 | } 128 | 129 | if (allowEmpty) { 130 | gen('}') 131 | } 132 | 133 | if (!prop) { 134 | gen(` 135 | parse.pointer = ptr 136 | return ${o} 137 | `) 138 | } else if (assigning) { 139 | prop.set(o) 140 | } 141 | 142 | function genblk (field, offset, gen) { 143 | gen(`ptr += ${offset + 1 + 1}`) 144 | if (prettyPrinted) { 145 | gen(`if (${ch('ptr')} === ${code(' ')} || ${ch('ptr')} === ${code('\n')}) ptr++`) 146 | } 147 | reqs.set(field) 148 | genany(gen, new Property(gen, o, field), field) 149 | if (prettyPrinted) { 150 | gen(`while (${ch('ptr')} === ${code(' ')} || ${ch('ptr')} === ${code('\n')}) ptr++`) 151 | } 152 | } 153 | } 154 | 155 | function gendef (gen) { 156 | gen(`throw new Error('Unexpected key in object')`) 157 | } 158 | 159 | function genif (gen, o, fields, i, genany) { 160 | const field = fields[i] 161 | var name = field.name + '"' 162 | 163 | if (!fullMatch) { 164 | var max = 1 165 | for (var n = i + 1; n < fields.length; n++) { 166 | const other = fields[n].name + '"' 167 | const sim = similar(name, other) 168 | if (sim > max) max = sim 169 | } 170 | name = name.slice(0, max) 171 | } 172 | 173 | gen(`if (${eq(name, 2)}) {`) 174 | gen(`ptr += ${1 + field.name.length + 2 + 1}`) 175 | genany(gen, new Property(gen, o, field), field) 176 | if (isRequired(field)) { 177 | gen(` 178 | } else { 179 | throw new Error("Missing required key: ${field.name}") 180 | } 181 | `) 182 | } else { 183 | gen('}') 184 | } 185 | } 186 | 187 | function genifs (gen, o, fields, genany) { 188 | for (var i = 0; i < fields.length; i++) genif(gen, o, fields, i, genany) 189 | } 190 | 191 | function isRequired (field) { 192 | return bool(field.required, required) 193 | } 194 | 195 | function defaultObject (gen, name, rawSchema) { 196 | if (!defaults) { 197 | gen(`const ${name} = {}`) 198 | } else { 199 | gen(`const ${name} = {`) 200 | defaultFields(gen, rawSchema.fields) 201 | gen('}') 202 | } 203 | } 204 | 205 | function defaultFields (gen, fields) { 206 | if (!fields) fields = [] 207 | 208 | for (var i = 0; i < fields.length; i++) { 209 | const f = fields[i] 210 | const s = i < fields.length - 1 ? ',' : '' 211 | 212 | switch (f.type) { 213 | case schema.STRING: 214 | gen(`${gen.property(f.name)}: ${JSON.stringify(f.default || '')}${s}`) 215 | break 216 | case schema.NUMBER: 217 | gen(`${gen.property(f.name)}: ${f.default || 0}${s}`) 218 | break 219 | case schema.BOOLEAN: 220 | gen(`${gen.property(f.name)}: ${f.default || false}${s}`) 221 | break 222 | case schema.NULL: 223 | gen(`${gen.property(f.name)}: ${f.default || null}${s}`) 224 | break 225 | case schema.ARRAY: 226 | if (isRequired(f)) { 227 | gen(`${gen.property(f.name)}: []${s}`) 228 | } else { 229 | gen(`${gen.property(f.name)}: undefined${s}`) 230 | } 231 | break 232 | case schema.OBJECT: 233 | if (isRequired(f)) { 234 | gen(`${gen.property(f.name)}: {`) 235 | defaultFields(gen, f.fields) 236 | gen(`}${s}`) 237 | } else { 238 | gen(`${gen.property(f.name)}: undefined${s}`) 239 | } 240 | break 241 | } 242 | } 243 | } 244 | } 245 | 246 | function bool (b, def) { 247 | if (b === undefined) return def 248 | return b 249 | } 250 | -------------------------------------------------------------------------------- /lib/ops.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = opts => opts.buffer ? exports.buffer : exports.string 2 | exports.buffer = create(true) 3 | exports.string = create(false) 4 | 5 | function create (buffer) { 6 | const name = buffer ? 'b' : 's' 7 | 8 | return {name, ptr, ptrInc, ch, code, eq, ne, stringSlice, indexOf} 9 | 10 | function indexOf (ch, start) { 11 | return buffer 12 | ? 'b.indexOf(' + ch.charCodeAt(0) + ', ' + start + ')' 13 | : 's.indexOf(' + JSON.stringify(ch) + ', ' + start + ')' 14 | } 15 | 16 | function stringSlice (start, end) { 17 | return buffer 18 | ? 'b.utf8Slice(' + start + ', ' + end + ')' 19 | : 's.slice(' + start + ', ' + end + ')' 20 | } 21 | 22 | function ptr (offset) { 23 | return offset ? 'ptr + ' + offset : 'ptr' 24 | } 25 | 26 | function ptrInc (inc) { 27 | return inc === 1 ? 'ptr++' : 'ptr += ' + inc 28 | } 29 | 30 | function ch (ptr) { 31 | if (buffer) return 'b[' + ptr + ']' 32 | return 's.charCodeAt(' + ptr + ')' 33 | } 34 | 35 | function code (val) { 36 | return val.charCodeAt(0) 37 | } 38 | 39 | function eq (val, offset) { 40 | return cmp(val, offset || 0, ' === ', ' && ') 41 | } 42 | 43 | function ne (val, offset) { 44 | return cmp(val, offset || 0, ' !== ', ' || ') 45 | } 46 | 47 | function cmp (val, offset, eq, join) { 48 | // 16 is arbitrary here but seems to work well 49 | if (buffer || val.length < 16) { 50 | const bytes = [] 51 | 52 | for (var i = 0; i < val.length; i++) { 53 | bytes.push(ch(ptr(offset++)) + eq + code(val[i])) 54 | } 55 | 56 | return bytes.join(join) 57 | } 58 | 59 | return `s.slice(${ptr(offset)}, ${ptr(offset + val.length)})${eq}${JSON.stringify(val)}` 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/property.js: -------------------------------------------------------------------------------- 1 | module.exports = class Property { 2 | constructor (gen, object, field) { 3 | this.name = field.name || null 4 | this.parent = object || null 5 | this.key = this.name ? gen.property(object, this.name) : null 6 | this.getable = field.required && !!this.key 7 | this.gen = gen 8 | } 9 | 10 | set (val, src) { 11 | const code = this.key ? `${this.key} = ${val}` : `${this.parent}.push(${val})` 12 | if (src) return code 13 | this.gen(code) 14 | } 15 | 16 | get () { 17 | if (!this.getable) throw new Error('Property is not getable') 18 | const sym = this.gen.sym(this.name) 19 | this.gen(`const ${sym} = ${this.key}`) 20 | return sym 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | const STRING = exports.STRING = 0 2 | const NUMBER = exports.NUMBER = 1 3 | const BOOLEAN = exports.BOOLEAN = 2 4 | const ARRAY = exports.ARRAY = 3 5 | const OBJECT = exports.OBJECT = 4 6 | const UNKNOWN = exports.UNKNOWN = 5 7 | const NULL = exports.NULL = 6 8 | 9 | exports.inferRawSchema = (obj, opts) => inferRawSchema(obj, opts || {}, null) 10 | exports.jsonSchemaToRawSchema = convertToRawSchema 11 | 12 | function type (val) { 13 | if (Array.isArray(val)) return ARRAY 14 | else if (val === null) return NULL 15 | switch (typeof val) { 16 | case 'string': return STRING 17 | case 'number': return NUMBER 18 | case 'boolean': return BOOLEAN 19 | case 'object': return val ? OBJECT : UNKNOWN 20 | } 21 | return UNKNOWN 22 | } 23 | 24 | function convertToRawSchema (jsons, opts) { 25 | const ordered = !!(opts && opts.ordered) 26 | const s = { 27 | type: 0, 28 | name: undefined, 29 | required: false, 30 | ordered: jsons.ordered || ordered, 31 | default: jsons.default || undefined, 32 | fields: undefined, 33 | items: undefined 34 | } 35 | 36 | switch (jsons.type) { 37 | case 'integer': 38 | case 'number': 39 | s.type = NUMBER 40 | break 41 | case 'string': 42 | s.type = STRING 43 | break 44 | case 'boolean': 45 | s.type = BOOLEAN 46 | break 47 | case 'null': 48 | s.type = NULL 49 | break 50 | case 'object': 51 | s.type = OBJECT 52 | s.fields = [] 53 | for (const k of Object.keys(jsons.properties)) { 54 | const f = convertToRawSchema(jsons.properties[k], opts) 55 | f.required = !!((jsons.required && jsons.required.indexOf(k) > -1) || jsons.properties[k].required) 56 | f.name = k 57 | s.fields.push(f) 58 | } 59 | break 60 | case 'array': 61 | s.type = ARRAY 62 | s.items = jsons.items ? convertToRawSchema(jsons.items, opts) : null 63 | break 64 | default: 65 | throw new Error('Unknown type: ' + jsons.type) 66 | } 67 | 68 | return s 69 | } 70 | 71 | function inferRawSchema (obj, opts, name) { 72 | const t = type(obj) 73 | const prop = { 74 | type: t, 75 | name: name || undefined, 76 | required: !!opts.required, 77 | ordered: !!opts.ordered, 78 | allowEmpty: opts.allowEmpty !== false, 79 | fields: undefined, 80 | items: undefined 81 | } 82 | 83 | if (t === OBJECT) { 84 | prop.fields = [] 85 | for (const key of Object.keys(obj)) { 86 | prop.fields.push(inferRawSchema(obj[key], opts, key)) 87 | } 88 | return prop 89 | } 90 | 91 | if (t === ARRAY) { 92 | prop.items = obj.length ? inferRawSchema(obj[0], opts, null) : null 93 | return prop 94 | } 95 | 96 | return prop 97 | } 98 | -------------------------------------------------------------------------------- /lib/similar.js: -------------------------------------------------------------------------------- 1 | module.exports = similar 2 | 3 | function similar (a, b) { 4 | const len = Math.min(a.length, b.length) 5 | 6 | for (var i = 0; i < len; i++) { 7 | if (a.charCodeAt(i) === b.charCodeAt(i)) continue 8 | return i 9 | } 10 | 11 | return len 12 | } 13 | -------------------------------------------------------------------------------- /lib/string.js: -------------------------------------------------------------------------------- 1 | const genfun = require('generate-function') 2 | const ops = require('./ops') 3 | 4 | module.exports = defaults 5 | 6 | function defaults (opts) { 7 | const { name } = ops(opts) 8 | const buffer = !!opts.buffer 9 | const unesc = opts.unescapeStrings !== false 10 | const validate = opts.validateStrings !== false || opts.validate !== false 11 | 12 | return compileString 13 | 14 | function compileString (gen, prop) { 15 | if (!gen.scope.parseString) { 16 | gen.scope.parseString = genparser(buffer, validate, unesc) 17 | } 18 | 19 | if (prop) { 20 | prop.set(`parseString(${name}, ptr)`) 21 | gen('ptr = parseString.pointer') 22 | } else { 23 | const str = gen.sym('str') 24 | gen(` 25 | const ${str} = parseString(${name}, ptr) 26 | parse.pointer = parseString.pointer 27 | return ${str} 28 | `) 29 | } 30 | } 31 | } 32 | 33 | function genparser (buffer, validate, unesc) { 34 | const {name, ch, indexOf, code, stringSlice} = ops({buffer}) 35 | const gen = genfun() 36 | 37 | gen(`function parseString (${name}, ptr) {`) 38 | 39 | if (validate) { 40 | gen(`if (${ch('ptr')} !== ${code('"')}) throw new Error('Unexpected token in string')`) 41 | } 42 | 43 | gen(`var i = ${indexOf('"', '++ptr')}`) 44 | 45 | if (validate) { 46 | gen(`if (i === -1) throw new Error('Unterminated string')`) 47 | } 48 | 49 | gen(` 50 | while (${ch('i - 1')} === ${code('\\')}) { 51 | var cnt = 1 52 | while (${ch('i - 1 - cnt')} === ${code('\\')}) cnt++ 53 | if ((cnt & 1) === 0) break 54 | i = ${indexOf('"', 'i + 1')} 55 | `) 56 | 57 | if (validate) { 58 | gen(`if (i === -1) throw new Error('Unterminated string')`) 59 | } 60 | 61 | gen(` 62 | } 63 | 64 | const slice = ${stringSlice('ptr', 'i')} 65 | parseString.pointer = i + 1 66 | `) 67 | 68 | if (unesc) { 69 | gen(`if (slice.indexOf('\\\\') > -1) return JSON.parse('"' + slice + '"')`) 70 | } 71 | 72 | gen('return slice') 73 | gen('}') 74 | 75 | const parseString = gen.toFunction() 76 | parseString.pointer = 0 77 | return parseString 78 | } 79 | -------------------------------------------------------------------------------- /lib/switch.js: -------------------------------------------------------------------------------- 1 | const opsDefaults = require('./ops') 2 | const similar = require('./similar') 3 | 4 | module.exports = defaults 5 | 6 | function defaults (opts) { 7 | if (!opts) opts = {} 8 | 9 | const { eq, ch, ptr, code } = opsDefaults(opts) 10 | const fullMatch = opts.fullMatch !== false 11 | 12 | return compileSwitch 13 | 14 | function compileSwitch (gen, off, fields, genblk, gendef) { 15 | var cnt = 0 16 | fields.sort(compare) 17 | visitSwitch(group(fields.map(toName)), '') 18 | 19 | function visitSwitch (tree, prefix) { 20 | var offset = off + prefix.length + 1 // + 1 is the " 21 | 22 | const sw = tree.length > 1 23 | 24 | if (sw) { 25 | gen(`switch (${ch(ptr(offset++))}) {`) 26 | } 27 | 28 | for (var i = 0; i < tree.length; i++) { 29 | const t = tree[i] 30 | const nested = Array.isArray(t) 31 | const match = nested ? t[0] : t 32 | const more = fullMatch && match.length > 1 33 | 34 | if (sw) { 35 | gen(`case ${code(match[0])}:`) 36 | if (more) gen(`if (${eq(match.slice(1), offset)}) {`) 37 | } else { 38 | if (fullMatch) gen(`if (${eq(match, offset)}) {`) 39 | } 40 | 41 | if (nested) visitSwitch(t[1], prefix + match) 42 | else genblk(fields[cnt++], off + prefix.length + match.length, gen) 43 | 44 | if (sw) { 45 | if (more) { 46 | if (gendef) { 47 | gen('} else {') 48 | gendef(gen) 49 | gen('}') 50 | } else { 51 | gen('}') 52 | } 53 | } 54 | gen('break') 55 | } else { 56 | if (fullMatch) { 57 | if (gendef) { 58 | gen('} else {') 59 | gendef(gen) 60 | gen('}') 61 | } else { 62 | gen('}') 63 | } 64 | } 65 | } 66 | } 67 | 68 | if (sw) { 69 | if (gendef) { 70 | gen('default:') 71 | gendef(gen) 72 | } 73 | gen('}') 74 | } 75 | } 76 | } 77 | } 78 | 79 | function compare (a, b) { 80 | return a.name < b.name ? -1 : a.name > b.name ? 1 : 0 81 | } 82 | 83 | function toName (field) { 84 | return field.name + '"' 85 | } 86 | 87 | function group (strings) { 88 | const tree = [] 89 | 90 | while (strings.length) { 91 | const s = strings[0] 92 | var min = -1 93 | 94 | for (var i = 1; i < strings.length; i++) { 95 | const c = similar(s, strings[i]) 96 | if (c === 0) break 97 | if (c < min || min === -1) min = c 98 | } 99 | 100 | if (min === -1) { 101 | tree.push(s) 102 | strings.shift() 103 | continue 104 | } 105 | tree.push([s.slice(0, min), group(strings.slice(0, i).map(s => s.slice(min)))]) 106 | strings = strings.slice(i) 107 | } 108 | return tree 109 | } 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "turbo-json-parse", 3 | "version": "2.3.0", 4 | "description": "Turbocharged JSON.parse for type stable JSON data", 5 | "main": "index.js", 6 | "dependencies": { 7 | "generate-function": "^2.3.1" 8 | }, 9 | "devDependencies": { 10 | "standard": "^11.0.1", 11 | "tape": "^4.9.1" 12 | }, 13 | "scripts": { 14 | "test": "standard && tape test/*.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/mafintosh/turbo-json-parse.git" 19 | }, 20 | "author": "Mathias Buus (@mafintosh)", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/mafintosh/turbo-json-parse/issues" 24 | }, 25 | "homepage": "https://github.com/mafintosh/turbo-json-parse" 26 | } 27 | -------------------------------------------------------------------------------- /test/random.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape') 2 | const compile = require('../') 3 | 4 | const ALPHA_NUMERIC = [] 5 | const ASCII = [] 6 | 7 | for (var i = 32; i < 127; i++) { 8 | const c = String.fromCharCode(i) 9 | if (/[a-z0-9]/i.test(c)) ALPHA_NUMERIC.push(c) 10 | ASCII.push(c) 11 | } 12 | 13 | const SOME_UTF8 = [].concat(ASCII, ['Æ', 'Ø', 'Å']) 14 | const ALPHA = ALPHA_NUMERIC.slice(10) 15 | 16 | tape('parse random objects', function (t) { 17 | for (var i = 0; i < 100; i++) { 18 | const o = random() 19 | const parse = compile.from(o) 20 | const parseBuf = compile.from(o, {buffer: true}) 21 | 22 | const objs = [o] 23 | while (objs.length < 5) objs.push(randomCopy(o, true)) 24 | while (objs.length < 10) objs.push(randomCopy(o, false)) 25 | 26 | objs.forEach(function (o) { 27 | try { 28 | t.ok(same(parse(JSON.stringify(o)), o), 'parsing random object from string') 29 | } catch (err) { 30 | t.fail('Could not parse object: ' + JSON.stringify(o)) 31 | t.end() 32 | return 33 | } 34 | try { 35 | t.ok(same(parseBuf(Buffer.from(JSON.stringify(o))), o), 'parssing random object from buffer') 36 | } catch (err) { 37 | t.fail('Could not parse object from buffer: ' + JSON.stringify(o)) 38 | t.end() 39 | } 40 | }) 41 | } 42 | 43 | t.end() 44 | }) 45 | 46 | function same (a, b) { 47 | if (JSON.stringify(a) === JSON.stringify(b)) return true 48 | require('fs').writeFileSync('bad.json', JSON.stringify([a, b])) 49 | return false 50 | } 51 | 52 | function randomCopy (o, optional) { 53 | if (Array.isArray(o)) { 54 | if (!o.length) return [] 55 | const n = new Array(Math.floor(Math.random() * 10)) 56 | for (var i = 0; i < n.length; i++) { 57 | n[i] = randomCopy(o[0], optional) 58 | } 59 | return n 60 | } 61 | if (typeof o === 'object') { 62 | const n = {} 63 | for (const k of Object.keys(o)) { 64 | if (optional && Math.random() < 0.10) { 65 | if (typeof o[k] === 'number') n[k] = 0 66 | if (typeof o[k] === 'boolean') n[k] = false 67 | if (typeof o[k] === 'string') n[k] = '' 68 | continue 69 | } 70 | n[k] = randomCopy(o[k], optional) 71 | } 72 | return n 73 | } 74 | if (typeof o === 'string') return random(0) 75 | if (typeof o === 'number') return random(1) 76 | return random(2) 77 | } 78 | 79 | function random (r, optional, depth) { 80 | if (!depth) depth = 0 81 | const len = depth < 10 ? 5 : 3 82 | switch (r === undefined ? Math.floor(Math.random() * len) : r) { 83 | case 0: 84 | return Math.random() < 0.5 85 | ? string(ASCII) 86 | : string(SOME_UTF8) 87 | case 1: 88 | return Math.random() < 0.33 89 | ? number() 90 | : Math.random() < 0.33 91 | ? Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) 92 | : Math.floor(Math.random() * Number.MIN_SAFE_INTEGER) 93 | case 2: 94 | return Math.random() < 0.5 95 | case 3: 96 | const obj = {} 97 | const fields = Math.floor(Math.random() * 10) 98 | for (var i = 0; i < fields; i++) { 99 | obj[string(ALPHA_NUMERIC, ALPHA)] = random(undefined, optional, depth + 1) 100 | } 101 | return obj 102 | case 4: 103 | const arr = new Array(Math.floor(Math.random() * 10)) 104 | if (!arr.length) return arr 105 | arr[0] = random(undefined, optional, depth + 1) 106 | for (var j = 1; j < arr.length; j++) { 107 | arr[j] = randomCopy(arr[0], optional) 108 | } 109 | return arr 110 | } 111 | } 112 | 113 | function number () { 114 | return Math.random() < 0.5 115 | ? Math.floor(Math.random() * 1e10) / 1e5 116 | : Math.floor(Math.random() * -1e10) / 1e5 117 | } 118 | 119 | function string (alpha, first) { 120 | if (!first) first = alpha 121 | const len = Math.floor(Math.random() * 100) + 1 122 | var s = '' 123 | for (var i = 0; i < len; i++) { 124 | s += first[Math.floor(Math.random() * first.length)] 125 | first = alpha 126 | } 127 | return s 128 | } 129 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tape') 4 | 5 | const tjp = require('../index') 6 | 7 | t.test('turbo-json-parse', t => { 8 | t.test('object', t => { 9 | t.test('string', t => { 10 | const parser = tjp({ 11 | type: 'object', 12 | properties: { 13 | key1: { type: 'string' } 14 | } 15 | }) 16 | t.deepEqual(parser('{"key1":"value1"}'), { 17 | key1: 'value1' 18 | }) 19 | t.deepEqual(parser('{"key1":"\\"val\\"ue1"}'), { 20 | key1: '"val"ue1' 21 | }) 22 | 23 | t.end() 24 | }) 25 | 26 | t.test('string,string', t => { 27 | const parser = tjp({ 28 | type: 'object', 29 | properties: { 30 | key1: { type: 'string' }, 31 | key2: { type: 'string' } 32 | } 33 | }) 34 | t.deepEqual(parser('{"key1":"\\"val\\"ue1","key2":"\\"val\\"ue2"}'), { 35 | key1: '"val"ue1', 36 | key2: '"val"ue2' 37 | }) 38 | 39 | t.end() 40 | }) 41 | 42 | t.test('number', t => { 43 | const parser = tjp({ 44 | type: 'object', 45 | properties: { 46 | key1: { type: 'number' } 47 | } 48 | }) 49 | t.deepEqual(parser('{"key1":42}'), { 50 | key1: 42 51 | }) 52 | t.deepEqual(parser('{"key1":42.3}'), { 53 | key1: 42.3 54 | }) 55 | 56 | t.end() 57 | }) 58 | 59 | t.test('number,number', t => { 60 | const parser = tjp({ 61 | type: 'object', 62 | properties: { 63 | key1: { type: 'number' }, 64 | key2: { type: 'number' } 65 | } 66 | }) 67 | t.deepEqual(parser('{"key1":42,"key2":33}'), { 68 | key1: 42, 69 | key2: 33 70 | }) 71 | t.deepEqual(parser('{"key1":42.3,"key2":33.3}'), { 72 | key1: 42.3, 73 | key2: 33.3 74 | }) 75 | 76 | t.end() 77 | }) 78 | 79 | t.test('string,number', t => { 80 | const parser = tjp({ 81 | type: 'object', 82 | properties: { 83 | key1: { type: 'string' }, 84 | key2: { type: 'number' } 85 | } 86 | }) 87 | t.deepEqual(parser('{"key1":"value1","key2":42}'), { 88 | key1: 'value1', 89 | key2: 42 90 | }) 91 | 92 | t.end() 93 | }) 94 | 95 | t.test('number,string', t => { 96 | const parser = tjp({ 97 | type: 'object', 98 | properties: { 99 | key1: { type: 'number' }, 100 | key2: { type: 'string' } 101 | } 102 | }) 103 | t.deepEqual(parser('{"key1":42,"key2":"value2"}'), { 104 | key1: 42, 105 | key2: 'value2' 106 | }) 107 | 108 | t.end() 109 | }) 110 | 111 | t.test('boolean', t => { 112 | const parser = tjp({ 113 | type: 'object', 114 | properties: { 115 | key1: { type: 'boolean' } 116 | } 117 | }) 118 | t.deepEqual(parser('{"key1":true}'), { 119 | key1: true 120 | }) 121 | t.deepEqual(parser('{"key1":false}'), { 122 | key1: false 123 | }) 124 | 125 | t.end() 126 | }) 127 | 128 | t.test('boolean,boolean', t => { 129 | const parser = tjp({ 130 | type: 'object', 131 | properties: { 132 | key1: { type: 'boolean' }, 133 | key2: { type: 'boolean' } 134 | } 135 | }) 136 | t.deepEqual(parser('{"key1":true,"key2":true}'), { 137 | key1: true, 138 | key2: true 139 | }) 140 | t.deepEqual(parser('{"key1":false,"key2":false}'), { 141 | key1: false, 142 | key2: false 143 | }) 144 | t.deepEqual(parser('{"key1":true,"key2":false}'), { 145 | key1: true, 146 | key2: false 147 | }) 148 | t.deepEqual(parser('{"key1":false,"key2":true}'), { 149 | key1: false, 150 | key2: true 151 | }) 152 | 153 | t.end() 154 | }) 155 | 156 | t.test('object', t => { 157 | t.test('string', t => { 158 | const parser = tjp({ 159 | type: 'object', 160 | properties: { 161 | key1: { 162 | type: 'object', 163 | properties: { 164 | key2: { type: 'string' } 165 | } 166 | } 167 | } 168 | }) 169 | t.deepEqual(parser('{"key1":{"key2":"value2"}}'), { 170 | key1: { key2: 'value2' } 171 | }) 172 | 173 | t.end() 174 | }) 175 | 176 | t.test('number', t => { 177 | const parser = tjp({ 178 | type: 'object', 179 | properties: { 180 | key1: { 181 | type: 'object', 182 | properties: { 183 | key2: { type: 'number' } 184 | } 185 | } 186 | } 187 | }) 188 | t.deepEqual(parser('{"key1":{"key2":42}}'), { 189 | key1: { key2: 42 } 190 | }) 191 | 192 | t.end() 193 | }) 194 | 195 | t.test() 196 | }) 197 | }) 198 | 199 | t.test('array', t => { 200 | t.test('string', t => { 201 | const parser = tjp({ 202 | type: 'array', 203 | items: { 204 | type: 'string' 205 | } 206 | }) 207 | t.deepEqual(parser('["value1"]'), [ 208 | 'value1' 209 | ]) 210 | t.deepEqual(parser('["value1","value2"]'), [ 211 | 'value1', 212 | 'value2' 213 | ]) 214 | t.deepEqual(parser('[]'), []) 215 | 216 | t.end() 217 | }) 218 | 219 | t.test('number', t => { 220 | const parser = tjp({ 221 | type: 'array', 222 | items: { 223 | type: 'number' 224 | } 225 | }) 226 | t.deepEqual(parser('[42]'), [ 227 | 42 228 | ]) 229 | t.deepEqual(parser('[42,33]'), [ 230 | 42, 231 | 33 232 | ]) 233 | t.deepEqual(parser('[42.4,33.3]'), [ 234 | 42.4, 235 | 33.3 236 | ]) 237 | t.deepEqual(parser('[]'), []) 238 | 239 | t.end() 240 | }) 241 | 242 | t.test('boolean', t => { 243 | const parser = tjp({ 244 | type: 'array', 245 | items: { 246 | type: 'boolean' 247 | } 248 | }) 249 | t.deepEqual(parser('[true]'), [ 250 | true 251 | ]) 252 | t.deepEqual(parser('[false]'), [ 253 | false 254 | ]) 255 | t.deepEqual(parser('[true,false]'), [ 256 | true, 257 | false 258 | ]) 259 | t.deepEqual(parser('[]'), []) 260 | 261 | t.end() 262 | }) 263 | 264 | t.test('array', t => { 265 | t.test('string', t => { 266 | const parser = tjp({ 267 | type: 'array', 268 | items: { 269 | type: 'array', 270 | items: { 271 | type: 'string' 272 | } 273 | } 274 | }) 275 | t.deepEqual(parser('[["value1"]]'), [ 276 | [ 277 | 'value1' 278 | ] 279 | ]) 280 | t.deepEqual(parser('[["value1","value2"]]'), [ 281 | [ 282 | 'value1', 283 | 'value2' 284 | ] 285 | ]) 286 | t.deepEqual(parser('[[],["value1","value2"]]'), [ 287 | [], 288 | [ 289 | 'value1', 290 | 'value2' 291 | ] 292 | ]) 293 | t.deepEqual(parser('[[],["value1","value2"],[]]'), [ 294 | [], 295 | [ 296 | 'value1', 297 | 'value2' 298 | ], 299 | [] 300 | ]) 301 | t.deepEqual(parser('[["value1"],["value2"],[]]'), [ 302 | [ 303 | 'value1' 304 | ], 305 | [ 306 | 'value2' 307 | ], 308 | [] 309 | ]) 310 | t.deepEqual(parser('[["value1"],["value2"],["value3"]]'), [ 311 | [ 312 | 'value1' 313 | ], 314 | [ 315 | 'value2' 316 | ], 317 | [ 318 | 'value3' 319 | ] 320 | ]) 321 | t.deepEqual(parser('[]'), []) 322 | 323 | t.end() 324 | }) 325 | 326 | t.end() 327 | }) 328 | 329 | t.test('object', t => { 330 | t.test('string', t => { 331 | const parser = tjp({ 332 | type: 'array', 333 | items: { 334 | type: 'object', 335 | properties: { 336 | key1: { type: 'string' } 337 | } 338 | } 339 | }) 340 | t.deepEqual(parser('[{"key1":"value1"}]'), [ 341 | { key1: 'value1' } 342 | ]) 343 | t.deepEqual(parser('[{"key1":"value1"},{"key1":"value2"}]'), [ 344 | { key1: 'value1' }, 345 | { key1: 'value2' } 346 | ]) 347 | t.deepEqual(parser('[]'), []) 348 | 349 | t.end() 350 | }) 351 | }) 352 | 353 | t.test('object', t => { 354 | t.test('protected keyword', t => { 355 | const parser = tjp({ 356 | type: 'object', 357 | properties: { 358 | default: { type: 'string' }, 359 | const: { type: 'string' } 360 | } 361 | }) 362 | t.deepEqual(parser('{"default":"value1","const":"value2"}'), { default: 'value1', const: 'value2' }) 363 | 364 | t.end() 365 | }) 366 | }) 367 | 368 | t.end() 369 | }) 370 | 371 | t.test('object', t => { 372 | t.test('numeric property name', t => { 373 | const parser = tjp({ 374 | type: 'object', 375 | properties: { 376 | 42: { type: 'string' } 377 | } 378 | }) 379 | t.deepEqual(parser('{"42":"value1"}'), { 42: 'value1' }) 380 | 381 | t.end() 382 | }) 383 | }) 384 | 385 | t.test('object', t => { 386 | t.test('property name with whitespace', t => { 387 | const parser = tjp({ 388 | type: 'object', 389 | properties: { 390 | // eslint-disable-next-line no-useless-computed-key 391 | ['hello world']: { 392 | type: 'object', 393 | properties: { 394 | key1: { 395 | type: 'string' 396 | } 397 | } 398 | } 399 | } 400 | }) 401 | const actual = parser('{"hello world":{"key1":"value1"}}') 402 | // eslint-disable-next-line no-useless-computed-key 403 | const expected = {['hello world']: {key1: 'value1'}} 404 | t.deepEqual(actual, expected) 405 | 406 | t.end() 407 | }) 408 | 409 | t.test('property name with whitespace', t => { 410 | const parser = tjp({ 411 | type: 'object', 412 | properties: { 413 | // eslint-disable-next-line no-useless-computed-key 414 | ['hello world']: { type: 'string' } 415 | } 416 | }) 417 | // eslint-disable-next-line no-useless-computed-key 418 | t.deepEqual(parser('{"hello world":"value1"}'), { ['hello world']: 'value1' }) 419 | 420 | t.end() 421 | }) 422 | }) 423 | 424 | t.test('null', t => { 425 | const parser = tjp({ 426 | type: 'object', 427 | properties: { 428 | key1: { type: 'null' } 429 | } 430 | }) 431 | t.deepEqual(parser('{"key1":null}'), { 432 | key1: null 433 | }) 434 | t.end() 435 | }) 436 | 437 | t.test('pretty print', t => { 438 | const parser = tjp({ 439 | type: 'object', 440 | properties: { 441 | key1: { type: 'null' }, 442 | key2: { type: 'string' } 443 | } 444 | }, { prettyPrinted: true }) 445 | t.deepEqual(parser(JSON.stringify({ 446 | key1: null, 447 | key2: 'test' 448 | }, null, 2)), { 449 | key1: null, 450 | key2: 'test' 451 | }) 452 | t.end() 453 | }) 454 | 455 | t.end() 456 | }) 457 | --------------------------------------------------------------------------------