├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json ├── rollup.config.js ├── src ├── identity.js ├── pathRule.js └── transform.js └── test ├── identitySpec.js ├── pathRuleSpec.js ├── scenarioTestSpec.js ├── support └── jasmine.json └── transformSpec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false }] 4 | ], 5 | "plugins": ["external-helpers"] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "env": { 4 | "jasmine": true 5 | }, 6 | "rules": { 7 | "semi": [1, "always"], 8 | "indent": [2, 2], 9 | "space-before-function-paren": [2, "never"], 10 | "padded-blocks": 0, 11 | "no-multi-spaces": 0, 12 | "no-unused-vars": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | before_install: 11 | - npm i -g npm@^2.0.0 12 | before_script: 13 | - npm prune 14 | after_success: 15 | - npm run semantic-release 16 | branches: 17 | except: 18 | - /^v\d+\.\d+\.\d+$/ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Scott Logic Ltd. 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 | # JSON Transforms 2 | 3 | Provides a recursive, pattern-matching approach to transforming JSON data. Transformations are defined as a set of rules which match the structure of a JSON object. When a match occurs, the rule emits the transformed data, optionally recursing to transform child objects. 4 | 5 | This framework makes use of [JSPath](https://github.com/dfilatov/jspath), a domain-specific language for querying JSON objects. It is alse heavily inspired by [XSLT](https://en.wikipedia.org/wiki/XSLT), a language for transforming XML documents. 6 | 7 | For more information about this project, see the associated blog post: 8 | 9 | http://blog.scottlogic.com/2016/06/22/xslt-inspired-ast-transforms.html 10 | 11 | ## Usage 12 | 13 | The following examples show how to transform this JSON object: 14 | 15 | ```javascript 16 | const json = { 17 | "automobiles": [ 18 | { "maker": "Nissan", "model": "Teana", "year": 2011 }, 19 | { "maker": "Honda", "model": "Jazz", "year": 2010 }, 20 | { "maker": "Honda", "model": "Civic", "year": 2007 }, 21 | { "maker": "Toyota", "model": "Yaris", "year": 2008 }, 22 | { "maker": "Honda", "model": "Accord", "year": 2011 } 23 | ] 24 | }; 25 | ``` 26 | 27 | Into the following structure, which just includes those automobiles made by 'Honda', with the 'maker' property 28 | removed: 29 | 30 | ```javascript 31 | { 32 | "Honda": [ 33 | { "model": "Jazz", "year": 2010 }, 34 | { "model": "Civic", "year": 2007 }, 35 | { "model": "Accord", "year": 2011 } 36 | ] 37 | } 38 | ``` 39 | 40 | ### Node 41 | 42 | Install via npm: 43 | 44 | ```bash 45 | npm install json-transforms --save 46 | ``` 47 | 48 | The following code demonstrates how to perform the transform described above, within a Node environment: 49 | 50 | ```javascript 51 | const jsont = require('json-transforms'); 52 | 53 | const json = { ... }; 54 | 55 | const rules = [ 56 | jsont.pathRule( 57 | '.automobiles{.maker === "Honda"}', d => ({ 58 | Honda: d.runner() 59 | }) 60 | ), 61 | jsont.pathRule( 62 | '.{.maker}', d => ({ 63 | model: d.match.model, 64 | year: d.match.year 65 | }) 66 | ) 67 | ]; 68 | 69 | const transformed = jsont.transform(json, rules); 70 | ``` 71 | 72 | ### Browser 73 | 74 | The **json-transforms** framework is exposed as a global variable `jsont`. The project also depends on [JSPath](https://github.com/dfilatov/jspath), so both must be included in order to run the above example: 75 | 76 | ```html 77 | 78 | 79 | ``` 80 | 81 | With these scripts loaded, the above example will also run in the browser. 82 | 83 | 84 | ### Modern JavaScript 85 | 86 | The examples in this documentation all use 'modern' JavaScript syntax (arrow functions, constants, etc ...), however, the npm module is transpiled to ES2015, so if you are in a browser environment that lacks ES2016 support, **json-transforms** will still work just fine: 87 | 88 | ```javascript 89 | var rules = [ 90 | jsont.pathRule( 91 | '.automobiles{.maker === "Honda"}', function(d) { 92 | return { honda: d.runner()} 93 | } 94 | ), 95 | jsont.pathRule( 96 | '.{.maker}', function(d) { 97 | return { 98 | model: d.match.model, 99 | year: d.match.year 100 | } 101 | } 102 | ) 103 | ]; 104 | 105 | var transformed = jsont.transform(json, rules); 106 | ``` 107 | 108 | ## Tutorial 109 | 110 | The following tutorial demonstrates the **json-transforms** API through a series of examples. The tutorial will use the following example JSON structure, transforming it into various different forms: 111 | 112 | ```javascript 113 | const json = { 114 | "automobiles": [ 115 | { "maker": "Nissan", "model": "Teana", "year": 2011 }, 116 | { "maker": "Honda", "model": "Jazz", "year": 2010 }, 117 | { "maker": "Honda", "model": "Civic", "year": 2007 }, 118 | { "maker": "Toyota", "model": "Yaris", "year": 2008 }, 119 | { "maker": "Honda", "model": "Accord", "year": 2011 } 120 | ] 121 | }; 122 | ``` 123 | 124 | ### Identity transformation 125 | 126 | JSON transformation is performed by the `transform` function which takes two arguments, the JSON object being transformed, and an array of rules. The transform function iterates over the list of rules, in the order given, to determine whether any return a value other than `null`, which indicates a match. 127 | 128 | For most transformations you will want to make use of the `identity` rule, which iterates over all the properties of an object, recursively invoking the transform function for all properties that are objects or arrays, and simply returns the property values for all others. 129 | 130 | If you transform a JSON object via the identity: 131 | 132 | ```javascript 133 | const rules = [ jsont.identity ]; 134 | const transformed = jsont.transform(json, rules); 135 | ``` 136 | 137 | You get an exact duplicate of the object back again! Useful. 138 | 139 | ### A simple path rule 140 | 141 | The `pathRule` function creates a rule that uses JSPath to match a pattern within the JSON sub-tree passed to the rule. If a match occurs, the associated function is invoked. Here's a quick illustration: 142 | 143 | ```javascript 144 | const rules = [ 145 | jsont.pathRule( 146 | '.automobiles', d => ({ 147 | 'count': d.match.length 148 | }) 149 | ) 150 | ]; 151 | ``` 152 | 153 | Which outputs the following when applied to the example JSON: 154 | 155 | ```javascript 156 | { count: 5 } 157 | ``` 158 | 159 | This path rule, which has the path `.automobiles` matches any object with an `automobiles` property. If a match occurs, it emits a JSON object with a `count` property. The `match` property of the object passed to this function contains the array of objects that match this path. In this case, it is the array of 5 automobiles, hence `d.match.length` returns 5. 160 | 161 | Because of the recursive nature of the identity transform, this rule will match any object with an `automobiles`, regardless of its location within the JSON data. 162 | 163 | For example, if the input JSON was changed to the following: 164 | 165 | ```javascript 166 | const json = { 167 | 'UK' : { 168 | 'automobiles': [ 169 | { 'maker': 'Nissan', 'model': 'Teana', 'year': 2011 }, 170 | { 'maker': 'Honda', 'model': 'Jazz', 'year': 2010 }, 171 | ] 172 | }, 173 | 'USA' : { 174 | 'automobiles': [ 175 | { 'maker': 'Honda', 'model': 'Civic', 'year': 2007 }, 176 | { 'maker': 'Toyota', 'model': 'Yaris', 'year': 2008 }, 177 | { 'maker': 'Honda', 'model': 'Accord', 'year': 2011 } 178 | ] 179 | } 180 | }; 181 | ``` 182 | 183 | The identity transform would emit `UK` and `USA`, recursively applying rules, to give the following totals: 184 | 185 | ```javascript 186 | { 187 | "UK": { 188 | "count": 2 189 | }, 190 | "USA": { 191 | "count": 3 192 | } 193 | } 194 | ``` 195 | 196 | **NOTE: Rule order matters!** - the current transform iteration stops on the first matching rule. Therefore, if you put the identity rule before the path rule in the current example, the `.automobiles` rule will never be reached! 197 | 198 | ### JSPath Syntax 199 | 200 | For detailed documentation of the JSPath syntax, visit the [project website](https://github.com/dfilatov/jspath). The documentation really is great! 201 | 202 | The JSPath syntax is easy to understand, here are a few quick examples: 203 | 204 | - `.automobiles` - match an object with an automobiles property, returning the value of this property. 205 | - `.automobiles.year` - match the year of each automobile, this would return an array of years. 206 | - `..year` - the single-dot syntax matches objects with the given property, the double-dot is a 'deep' match, finding any objects nested within the JSON structure. With **json-transforms** you typically use the identity transform, which avoids the need for deep matching. 207 | - `.automobiles{.maker === "Honda" && .year > 2009}.model` - find the model of any automobile made by Honda, with a year greater than 2009. 208 | 209 | As you can see, JSPath is *very* powerful. 210 | 211 | ### Match context 212 | 213 | The above examples have demonstrated the use of the `match` property, which contains the objects that match the given path. It also has a `context` property, which is the object being matched on. An easy way to see the difference between them is to create a transform that outputs both: 214 | 215 | ```javascript 216 | const rules = [ 217 | jsont.pathRule( 218 | '.maker', d => ({ 219 | context: d.context, 220 | match: d.match 221 | }) 222 | ) 223 | ]; 224 | ``` 225 | 226 | Which outputs the following: 227 | 228 | ```javascript 229 | { 230 | "automobiles": [ 231 | { 232 | "context": { 233 | "maker": "Nissan", 234 | "model": "Teana", 235 | "year": 2011 236 | }, 237 | "match": "Nissan" 238 | }, 239 | ... 240 | } 241 | ``` 242 | 243 | You can see that the `.maker` path matches objects that have the `maker` property, with the match being the value of this property. Whereas the context is the object that was matched. 244 | 245 | ### Recursive matches 246 | 247 | In the current example, the path rule outputs the number of items that match the given path. However, it's also possible to continue matching rules in a recursive fashion. 248 | 249 | To see this in action, we'll start with a simple rule that matches objects with a `.maker` property, outputting a formatted description: 250 | 251 | ```javascript 252 | const rules = [ 253 | jsont.pathRule( 254 | '.maker', d => ({ 255 | text: `The ${d.context.model} was made in ${d.context.year}` 256 | }) 257 | ) 258 | ]; 259 | ``` 260 | 261 | Which outputs the following: 262 | 263 | ```javascript 264 | { 265 | "automobiles": [ 266 | { 267 | "text": "Teana was made in 2011" 268 | }, 269 | { 270 | "text": "Jazz was made in 2010" 271 | }, 272 | { 273 | "text": "Civic was made in 2007" 274 | }, 275 | { 276 | "text": "Yaris was made in 2008" 277 | }, 278 | { 279 | "text": "Accord was made in 2011" 280 | } 281 | ] 282 | } 283 | ``` 284 | 285 | If you just wanted to output the result for Honda automobiles, you could add a new rule with a path that matches Honda cars, then recurse, by invoking the `runner` function: 286 | 287 | ```javascript 288 | const rules = [ 289 | jsont.pathRule( 290 | '.automobiles{.maker === "Honda"}', d => ({ 291 | automobiles: d.runner() 292 | }) 293 | ), 294 | jsont.pathRule( 295 | '.maker', d => ({ 296 | text: `The ${d.context.model} was made in ${d.context.year}` 297 | }) 298 | ) 299 | ]; 300 | ``` 301 | 302 | Which gives the following: 303 | 304 | ```javascript 305 | { 306 | "automobiles": [ 307 | { 308 | "text": "The Jazz was made in 2010" 309 | }, 310 | { 311 | "text": "The Civic was made in 2007" 312 | }, 313 | { 314 | "text": "The Accord was made in 2011" 315 | } 316 | ] 317 | } 318 | ``` 319 | 320 | This is a *very* powerful feature of the framework, allowing you to construct complex transforms that are composed of a number of simpler transformations. 321 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { default as identity } from './src/identity'; 2 | export { default as transform } from './src/transform'; 3 | export { default as pathRule } from './src/pathRule'; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-transforms", 3 | "description": "Provides a recursive, pattern-matching approach to transforming JSON data.", 4 | "main": "build/json-transforms.js", 5 | "jsnext:main": "index", 6 | "dependencies": { 7 | "jspath": "^0.3.3" 8 | }, 9 | "homepage": "https://github.com/ColinEberhardt/json-transforms", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/ColinEberhardt/json-transforms" 13 | }, 14 | "devDependencies": { 15 | "babel-plugin-external-helpers": "^6.8.0", 16 | "babel-preset-es2015": "^6.14.0", 17 | "eslint": "^2.2.0", 18 | "eslint-config-standard": "^5.1.0", 19 | "eslint-plugin-promise": "^1.3.2", 20 | "eslint-plugin-standard": "^1.3.2", 21 | "jasmine": "^2.4.1", 22 | "rollup": "^0.34.12", 23 | "rollup-plugin-babel": "^2.6.1", 24 | "semantic-release": "^4.3.5" 25 | }, 26 | "scripts": { 27 | "lint": "eslint src/**/*.js", 28 | "test": "npm run lint && npm run bundle && jasmine JASMINE_CONFIG_PATH=test/support/jasmine.json", 29 | "bundle": "rollup -c", 30 | "semantic-release": "semantic-release pre && npm run bundle && npm publish && semantic-release post" 31 | }, 32 | "author": "", 33 | "license": "MIT", 34 | "version": "1.1.0" 35 | } 36 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | 3 | export default { 4 | entry: 'index.js', 5 | moduleName: 'jsont', 6 | format: 'umd', 7 | plugins: [babel()], 8 | dest: 'build/json-transforms.js', 9 | globals: { 10 | jspath: 'JSPath' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/identity.js: -------------------------------------------------------------------------------- 1 | const identity = (json, runner) => { 2 | if (typeof json !== 'object') { 3 | return json; 4 | } else if (Array.isArray(json)) { 5 | return json.map(d => runner(d)); 6 | } else { 7 | var out = {}; 8 | for (var prop in json) { 9 | const value = json[prop]; 10 | if (Array.isArray(value)) { 11 | out[prop] = value.map(d => runner(d)); 12 | } else { 13 | out[prop] = runner(value); 14 | } 15 | } 16 | return out; 17 | } 18 | }; 19 | 20 | export default identity; 21 | -------------------------------------------------------------------------------- /src/pathRule.js: -------------------------------------------------------------------------------- 1 | import JSPath from 'jspath'; 2 | 3 | const pathRule = (path, ifMatch) => 4 | (json, runner) => { 5 | const match = JSPath.apply(path, json); 6 | const unwrappedMatch = match.length === 1 ? match[0] : match; 7 | const rootMatch = unwrappedMatch === json; 8 | 9 | if (match.length > 0) { 10 | // add recursion checks around the runner 11 | const guardedRunner = function(leaf) { 12 | if ((arguments.length === 0 && rootMatch) || 13 | (arguments.length === 1 && json === leaf)) { 14 | console.warn('Warning: un-bounded recursion detected'); 15 | return {}; 16 | } else { 17 | return leaf ? runner(leaf) : runner(unwrappedMatch); 18 | } 19 | }; 20 | 21 | return ifMatch({ 22 | context: json, 23 | match: unwrappedMatch, 24 | runner: guardedRunner 25 | }); 26 | } else { 27 | return null; 28 | } 29 | }; 30 | 31 | export default pathRule; 32 | -------------------------------------------------------------------------------- /src/transform.js: -------------------------------------------------------------------------------- 1 | const transform = (json, rules) => { 2 | 3 | const runner = match => { 4 | for (let i = 0; i < rules.length; i++) { 5 | const rule = rules[i]; 6 | const res = rule(match, adaptedRunner); 7 | if (res !== null) { 8 | return res; 9 | } 10 | } 11 | }; 12 | 13 | const adaptedRunner = ast => { 14 | if (Array.isArray(ast)) { 15 | return ast.map(r => runner(r)); 16 | } else { 17 | return runner(ast); 18 | } 19 | }; 20 | 21 | return adaptedRunner(json); 22 | }; 23 | 24 | export default transform; 25 | -------------------------------------------------------------------------------- /test/identitySpec.js: -------------------------------------------------------------------------------- 1 | const transformer = require('./../build/json-transforms'); 2 | const identity = transformer.identity; 3 | 4 | describe('Identity rule', () => { 5 | const spy = { 6 | runner: () => {} 7 | }; 8 | 9 | it('should visit every object property', () => { 10 | const json = { 11 | foo: 'bar', 12 | bar: 23 13 | }; 14 | 15 | spyOn(spy, 'runner'); 16 | identity(json, spy.runner); 17 | 18 | expect(spy.runner).toHaveBeenCalledWith('bar'); 19 | expect(spy.runner).toHaveBeenCalledWith(23); 20 | }); 21 | 22 | it('should visit every array item', () => { 23 | const json = [45, 'fish']; 24 | 25 | spyOn(spy, 'runner'); 26 | identity(json, spy.runner); 27 | 28 | expect(spy.runner).toHaveBeenCalledWith(45); 29 | expect(spy.runner).toHaveBeenCalledWith('fish'); 30 | }); 31 | 32 | it('should individually visit each item within an array property', () => { 33 | const json = { 34 | data: [45, 'fish'] 35 | }; 36 | 37 | spyOn(spy, 'runner'); 38 | identity(json, spy.runner); 39 | 40 | expect(spy.runner).toHaveBeenCalledWith(45); 41 | expect(spy.runner).toHaveBeenCalledWith('fish'); 42 | }); 43 | 44 | it('should return the results of the runner for each object property', () => { 45 | const json = { 46 | foo: 'bar', 47 | bar: 23 48 | }; 49 | 50 | spyOn(spy, 'runner').and.callFake(value => { 51 | return value === 'bar' ? 'moo' : 'cat'; 52 | }); 53 | const result = identity(json, spy.runner); 54 | 55 | expect(result).toEqual({ foo: 'moo', bar: 'cat' }); 56 | }); 57 | 58 | it('should visit every array property', () => { 59 | const json = [ 60 | 45, 'fish' 61 | ]; 62 | 63 | spyOn(spy, 'runner').and.callFake(value => { 64 | return value === 45 ? 'moo' : 'cat'; 65 | }); 66 | const result = identity(json, spy.runner); 67 | 68 | expect(result).toEqual(['moo', 'cat']); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/pathRuleSpec.js: -------------------------------------------------------------------------------- 1 | const transformer = require('./../build/json-transforms'); 2 | const pathRule = transformer.pathRule; 3 | 4 | describe('pathRule', () => { 5 | 6 | it('returns null if there is no match', () => { 7 | const json = { 8 | foo: 'bar' 9 | }; 10 | 11 | const rule = pathRule('.{.bar}', () => {}); 12 | const result = rule(json); 13 | expect(result).toBeNull(); 14 | }); 15 | 16 | it('invokes ifMatch if the path matches', () => { 17 | const json = { 18 | cat: [ 19 | { foo: 1 }, 20 | { foo: 2 } 21 | ] 22 | }; 23 | 24 | const spy = { 25 | ifMatch: () => {} 26 | }; 27 | spyOn(spy, 'ifMatch'); 28 | 29 | const rule = pathRule('..{.foo}', spy.ifMatch); 30 | const result = rule(json, {}); 31 | 32 | expect(spy.ifMatch.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({ 33 | match: [ 34 | { foo: 1 }, 35 | { foo: 2 } 36 | ] 37 | })); 38 | }); 39 | 40 | it('supplies the context to the ifMatch function', () => { 41 | const json = { 42 | cat: [ 43 | { foo: 1 }, 44 | { foo: 2 } 45 | ] 46 | }; 47 | 48 | const spy = { 49 | ifMatch: () => {} 50 | }; 51 | spyOn(spy, 'ifMatch'); 52 | 53 | const rule = pathRule('..{.foo}', spy.ifMatch); 54 | const result = rule(json, {}); 55 | 56 | expect(spy.ifMatch.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({ 57 | context: json 58 | })); 59 | }); 60 | 61 | it('unwraps single element matches', () => { 62 | const json = { 63 | foo: 1 64 | }; 65 | 66 | const spy = { 67 | ifMatch: () => {} 68 | }; 69 | spyOn(spy, 'ifMatch'); 70 | 71 | const rule = pathRule('.{.foo}', spy.ifMatch); 72 | const result = rule(json, {}); 73 | 74 | expect(spy.ifMatch.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({ 75 | match: { foo: 1 } 76 | })); 77 | }); 78 | 79 | it('allow the runner to be invoked with a json object', () => { 80 | const json = { 81 | cat: { 82 | foo: { 83 | bar: 2 84 | } 85 | } 86 | }; 87 | 88 | const spy = { 89 | runner: () => {} 90 | }; 91 | spyOn(spy, 'runner'); 92 | 93 | const rule = pathRule('..{.foo}', d => d.runner(d.match.foo)); 94 | const result = rule(json, spy.runner); 95 | 96 | expect(spy.runner).toHaveBeenCalledWith({ 97 | bar: 2 98 | }); 99 | }); 100 | 101 | it('allow the runner to be invoked without any argument', () => { 102 | const json = { 103 | cat: { 104 | foo: { 105 | bar: 2 106 | } 107 | } 108 | }; 109 | 110 | const spy = { 111 | runner: () => {} 112 | }; 113 | spyOn(spy, 'runner'); 114 | 115 | const rule = pathRule('..{.foo}', d => d.runner()); 116 | const result = rule(json, spy.runner); 117 | 118 | expect(spy.runner).toHaveBeenCalledWith({ 119 | foo: { 120 | bar: 2 121 | } 122 | }); 123 | }); 124 | 125 | it('warns if recursion detected, and halt the runner, when runner invoked with a json object', () => { 126 | const json = { 127 | foo: 1 128 | }; 129 | 130 | const spy = { 131 | runner: () => {} 132 | }; 133 | spyOn(spy, 'runner'); 134 | spyOn(console, 'warn'); 135 | const runner = () => {}; 136 | 137 | // invoke the runner with the matched json, which is the 138 | // same as teh json above - hence unbounded recursion 139 | const rule = pathRule('.{.foo}', d => d.runner(d.match)); 140 | const result = rule(json, spy.runner); 141 | 142 | expect(spy.runner).not.toHaveBeenCalled(); 143 | expect(console.warn).toHaveBeenCalledWith('Warning: un-bounded recursion detected'); 144 | }); 145 | 146 | it('warns if recursion detected, and halt the runner, when runner invoked without arguments', () => { 147 | const json = { 148 | foo: 1 149 | }; 150 | 151 | const spy = { 152 | runner: () => {} 153 | }; 154 | spyOn(spy, 'runner'); 155 | spyOn(console, 'warn'); 156 | const runner = () => {}; 157 | 158 | // invoke the runner with the matched json, which is the 159 | // same as teh json above - hence unbounded recursion 160 | const rule = pathRule('.{.foo}', d => d.runner()); 161 | const result = rule(json, spy.runner); 162 | 163 | expect(spy.runner).not.toHaveBeenCalled(); 164 | expect(console.warn).toHaveBeenCalledWith('Warning: un-bounded recursion detected'); 165 | }); 166 | 167 | }); 168 | -------------------------------------------------------------------------------- /test/scenarioTestSpec.js: -------------------------------------------------------------------------------- 1 | const transformer = require('./../build/json-transforms'); 2 | const identity = transformer.identity; 3 | const pathRule = transformer.pathRule; 4 | const transform = transformer.transform; 5 | 6 | describe('pathRule', () => { 7 | 8 | it('should allow the json to be transformed in non-tree order', () => { 9 | // this scenario demonstrates how a path rule can cause the json to 10 | // be parsed in an order other than the original tree-like structure 11 | const json = { 12 | 'automobiles': [ 13 | { 'maker': 'Nissan', 'model': 'Teana', 'year': 2011 }, 14 | { 'maker': 'Honda', 'model': 'Jazz', 'year': 2010 }, 15 | { 'maker': 'Honda', 'model': 'Civic', 'year': 2007 }, 16 | { 'maker': 'Toyota', 'model': 'Yaris', 'year': 2008 }, 17 | { 'maker': 'Honda', 'model': 'Accord', 'year': 2011 } 18 | ] 19 | }; 20 | 21 | const groupBy = (arr, fn) => 22 | arr.reduce((p, c) => { 23 | p[fn(c)] ? p[c.maker].push(c) : p[fn(c)] = [c]; 24 | return p; 25 | }, {}); 26 | 27 | const rules = [ 28 | pathRule( 29 | '.automobiles', d => d.runner(groupBy(d.match, c => c.maker)) 30 | ), 31 | pathRule( 32 | '.{.maker}', d => ({ 33 | model: d.match.model, 34 | year: d.match.year 35 | }) 36 | ), 37 | identity 38 | ]; 39 | 40 | const transformed = transform(json, rules); 41 | 42 | expect(transformed).toEqual({ 43 | 'Nissan': [ 44 | { 'model': 'Teana', 'year': 2011 } 45 | ], 46 | 'Honda': [ 47 | { 'model': 'Jazz', 'year': 2010 }, 48 | { 'model': 'Civic', 'year': 2007 }, 49 | { 'model': 'Accord', 'year': 2011 } 50 | ], 51 | 'Toyota': [ 52 | { 'model': 'Yaris', 'year': 2008 } 53 | ] 54 | }); 55 | }); 56 | 57 | it('should work for the simple case given in the readme', () => { 58 | const json = { 59 | 'automobiles': [ 60 | { 'maker': 'Nissan', 'model': 'Teana', 'year': 2011 }, 61 | { 'maker': 'Honda', 'model': 'Jazz', 'year': 2010 }, 62 | { 'maker': 'Honda', 'model': 'Civic', 'year': 2007 }, 63 | { 'maker': 'Toyota', 'model': 'Yaris', 'year': 2008 }, 64 | { 'maker': 'Honda', 'model': 'Accord', 'year': 2011 } 65 | ] 66 | }; 67 | 68 | const rules = [ 69 | pathRule( 70 | '.automobiles{.maker === "Honda"}', 71 | d => ({ 72 | honda: d.runner() 73 | }) 74 | ), 75 | pathRule( 76 | '.{.maker}', d => ({ 77 | model: d.match.model, 78 | year: d.match.year 79 | }) 80 | ), 81 | identity 82 | ]; 83 | 84 | const transformed = transform(json, rules); 85 | 86 | expect(transformed).toEqual({ 87 | 'honda': [ 88 | { 89 | 'model': 'Jazz', 90 | 'year': 2010 91 | }, 92 | { 93 | 'model': 'Civic', 94 | 'year': 2007 95 | }, 96 | { 97 | 'model': 'Accord', 98 | 'year': 2011 99 | } 100 | ] 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "test", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "stopSpecOnExpectationFailure": false, 7 | "random": false 8 | } 9 | -------------------------------------------------------------------------------- /test/transformSpec.js: -------------------------------------------------------------------------------- 1 | const transformer = require('./../build/json-transforms'); 2 | const identity = transformer.identity; 3 | const transform = transformer.transform; 4 | 5 | describe('Transform', () => { 6 | 7 | describe('Identity transformation', () => { 8 | 9 | it('should return the same json as the input', () => { 10 | const json = { 11 | foo: 'bar', 12 | bar: { 13 | cat: 23, 14 | array: [1, 2, 3, 4] 15 | } 16 | }; 17 | 18 | const output = transform(json, [identity]); 19 | expect(output).toEqual(json); 20 | }); 21 | 22 | it('should return the same array-wrapped json as the input', () => { 23 | const json = [ 24 | 1, 2, 3, 25 | { foo: 'bar' } 26 | ]; 27 | 28 | const output = transform(json, [identity]); 29 | expect(output).toEqual(json); 30 | }); 31 | 32 | }); 33 | 34 | describe('Rule precidence', () => { 35 | 36 | it('should stop rule evaluation on the first match', () => { 37 | const json = { 38 | foo: 'bar', 39 | bar: { 40 | cat: 23 41 | } 42 | }; 43 | 44 | // a rule that replaces any object with a key 'cat' with 'fish' 45 | const rule = (json) => json.cat ? 'fish' : null; 46 | 47 | const output = transform(json, [rule, identity]); 48 | expect(output).toEqual({ 49 | foo: 'bar', 50 | bar: 'fish' 51 | }); 52 | }); 53 | }); 54 | 55 | }); 56 | --------------------------------------------------------------------------------