├── .gitignore ├── .travis.yml ├── .jscsrc ├── .jshintrc ├── gulpfile.js ├── package.json ├── LICENSE.md ├── example ├── example.js └── source.json ├── CHANGELOG.md ├── lib └── fanci.js ├── test ├── test.extract.js ├── test.transform.js └── test.rename.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | - "4.0.0" 6 | script: gulp travis 7 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "node-style-guide", 3 | "requireCapitalizedComments": null, 4 | "disallowQuotedKeysInObjects": null, 5 | "requireTrailingComma": null, 6 | "validateQuoteMarks": null, 7 | "validateIndentation": 4, 8 | "maximumLineLength": 120, 9 | "disallowMultipleVarDecl": null 10 | } 11 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": { 3 | }, 4 | "jquery": true, 5 | "curly": true, 6 | "strict": false, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "latedef": "nofunc", 10 | "newcap": false, 11 | "noarg": true, 12 | "sub": true, 13 | "undef": true, 14 | "unused": false, 15 | "onevar": true, 16 | "eqnull": true, 17 | "browser": false, 18 | "mocha": true, 19 | "node": true, 20 | "devel": true, 21 | "maxcomplexity": 5 22 | } 23 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var jshint = require('gulp-jshint'); 3 | var jscs = require('gulp-jscs'); 4 | var exec = require('child_process').exec; 5 | 6 | var scripts = [ 7 | './**/*.js', 8 | '!./node_modules/**/*.js' 9 | ]; 10 | 11 | gulp.task('lint', function() { 12 | return gulp.src(scripts) 13 | .pipe(jshint('./.jshintrc')) 14 | .pipe(jshint.reporter('jshint-stylish')) 15 | .pipe(jshint.reporter('fail')) 16 | .pipe(jscs()); 17 | }); 18 | 19 | gulp.task('test', function(cb) { 20 | exec('npm test', function(err, stdout, stderr) { 21 | console.log(stdout); 22 | console.log(stderr); 23 | cb(err); 24 | }); 25 | }); 26 | 27 | gulp.task('default', [ 'lint' ]); 28 | gulp.task('travis', [ 'lint', 'test' ]); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fanci", 3 | "version": "0.7.0", 4 | "description": "Extract, rename or transform a JSON based on a template structure", 5 | "main": "lib/fanci.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:liip/fanci.git" 9 | }, 10 | "author": "Stefan Oderbolz & Philipp Küng, Liip AG", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/liip/fanci/issues" 14 | }, 15 | "homepage": "https://github.com/liip/fanci", 16 | "dependencies": { 17 | "underscore": "~1.8.3" 18 | }, 19 | "devDependencies": { 20 | "chai": "^4.0.2", 21 | "gulp": "^3.9.1", 22 | "gulp-jscs": "^4.0.0", 23 | "gulp-jshint": "^2.0.4", 24 | "jshint": "^2.9.5", 25 | "jshint-stylish": "^2.2.1", 26 | "mocha": "~3.4.2" 27 | }, 28 | "scripts": { 29 | "test": "mocha -R spec -t 10000" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Liip AG 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 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | var fanci = require('../lib/fanci'); 2 | var source = require('./source'); 3 | 4 | // Take all products ('*') but only the id, name and status keys 5 | var productTemplate = { 6 | 'products': { 7 | '*': { 8 | 'id': true, 9 | 'name': true, 10 | 'status': true 11 | } 12 | } 13 | }; 14 | console.log("All products with id, name and status", fanci.extract(source, productTemplate)); 15 | 16 | // If the JSON contains an array, use '*' so all the rules of are applied to all elements of the array 17 | var docTemplate = { 18 | 'docs': { 19 | '*': { 20 | 'description': true 21 | } 22 | } 23 | }; 24 | console.log("Extract only one key from an array", fanci.extract(source, docTemplate)); 25 | 26 | // Alternatively you can specify certain array indices to extract only a subset 27 | var docTemplate = { 28 | 'docs': { 29 | '1': { 30 | 'description': true 31 | }, 32 | '3': { 33 | 'author': true 34 | } 35 | } 36 | }; 37 | console.log("Specify which array elements should be extracted", fanci.extract(source, docTemplate)); 38 | -------------------------------------------------------------------------------- /example/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": { 3 | "1234": { 4 | "id": 1234, 5 | "internal_id": "X04BEEF", 6 | "name": "The Beef", 7 | "status": { 8 | "available": true 9 | }, 10 | "delivery": { 11 | "company": "My Transport", 12 | "rate": "business_hour", 13 | "time": "daily" 14 | } 15 | }, 16 | "4567": { 17 | "id": 4567, 18 | "internal_id": "X08CAFE", 19 | "name": "El Coffee", 20 | "status": { 21 | "available": true 22 | }, 23 | "delivery": { 24 | "company": "Ayayay", 25 | "rate": "weekend", 26 | "time": "weekend" 27 | } 28 | }, 29 | "6789": { 30 | "id": 6789, 31 | "internal_id": "X07DEAD", 32 | "name": "Life Product", 33 | "status": { 34 | "available": false 35 | }, 36 | "delivery": { 37 | "company": "My Transport", 38 | "rate": "business_hour", 39 | "time": "weekend" 40 | } 41 | }, 42 | "char:1": { 43 | "internal_id": "char:1" 44 | }, 45 | "char:2": { 46 | "internal_id": "char:2" 47 | }, 48 | "bar:1": { 49 | "internal_id": "bar:1" 50 | }, 51 | "foo:1": { 52 | "internal_id": "foo:1" 53 | } 54 | }, 55 | "docs": [ 56 | { 57 | "author": "Gandalf", 58 | "date": "2014-02-03", 59 | "description": "Put some magic in here" 60 | }, 61 | { 62 | "author": "Harry", 63 | "date": "2014-02-04", 64 | "description": "Rainbow Unicorns!" 65 | }, 66 | { 67 | "author": "Phil", 68 | "date": "2014-05-19", 69 | "description": "Valuable information" 70 | }, 71 | { 72 | "author": "Odi", 73 | "date": "2014-05-22", 74 | "description": "Fanci stuff!" 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project follows [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased][unreleased] 6 | 7 | ## 0.7.0 - 2017-07-06 8 | ### Changed 9 | - Updated all npm dev dependencies 10 | 11 | ## 0.6.0 - 2016-01-09 12 | ### Changed 13 | - Updated all npm dependencies 14 | - Run tests with node 4.0 as well 15 | 16 | ## 0.5.0 - 2015-05-18 17 | ### Added 18 | - Functionality to match a property name using '*' as wildcard (thanks Elin Ahmedow for this contribution!) 19 | 20 | ### Changed 21 | - New rules for JSCS based on node preset 22 | 23 | ## 0.4.0 - 2014-12-30 24 | ### Added 25 | - Added transform functionality to create arbitrary objects based on a template 26 | - Added format function to transform, so that transformation are more flexible 27 | 28 | ### Changed 29 | - Refactoring to use the Template Method pattern 30 | - BC-break: the rename template has a new structure, i.e. the new renamed keys are the keys of the template 31 | 32 | ## 0.3.0 - 2014-12-13 33 | ### Added 34 | - New functionality to rename object keys based on a template 35 | - Introduced a change log 36 | 37 | ## 0.2.1 - 2014-05-26 38 | ### Added 39 | - License information in the LICENSE.md file 40 | 41 | ### Changed 42 | - Transfer repository from 'metaodi' to 'liip' 43 | 44 | ## 0.2.0 - 2014-05-26 45 | ### Added 46 | - Tests for `extract` 47 | - Added support for arrays 48 | - Added wildcard '*' to match all keys of a level 49 | 50 | ## 0.1.0 - 2014-05-24 51 | ### Changed 52 | - Renamed `transform` to `extract` (BC break) as it better describes its function 53 | 54 | ## 0.0.2 - 2014-05-23 55 | ### Changed 56 | - Change description in package.json 57 | - Improved README with examples and better description of fanci 58 | 59 | ## 0.0.1 - 2014-05-23 60 | ### Added 61 | - Initial version of fanci with basic implementation of `transform` 62 | - Added README file 63 | - Added examples (template and source JSON) 64 | 65 | 66 | # Categories 67 | - `Added` for new features. 68 | - `Changed` for changes in existing functionality. 69 | - `Deprecated` for once-stable features removed in upcoming releases. 70 | - `Removed` for deprecated features removed in this release. 71 | - `Fixed` for any bug fixes. 72 | - `Security` to invite users to upgrade in case of vulnerabilities. 73 | -------------------------------------------------------------------------------- /lib/fanci.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | /* Processor */ 4 | function Processor() {} 5 | Processor.prototype.process = function(source, template) { 6 | var me = this; 7 | try { 8 | if ((typeof (source) === 'object') && (typeof (template) === 'object')) { 9 | Object.keys(source); 10 | Object.keys(template); 11 | } else { 12 | throw new Error('TypeError: Object.keys called on non-object'); 13 | } 14 | } catch (e) { 15 | // if either source or template has no keys, 16 | // this means we reached the end of the tree 17 | return source; 18 | } 19 | if (_.isArray(source)) { 20 | return me.array(source, template); 21 | } 22 | 23 | // This function creates the result of one level of the tree 24 | return me.level(source, template); 25 | }; 26 | Processor.prototype.array = function(source, template) { 27 | var me = this, 28 | templateKeys = _.keys(template); 29 | 30 | if (templateKeys.indexOf('*') >= 0) { 31 | return me.allElements(source, template); 32 | } 33 | return me.someElements(source, template); 34 | }; 35 | Processor.prototype.allElements = function(source, template) { 36 | var me = this; 37 | return _.map(source, function(subsource) { 38 | return me.process(subsource, template['*']); 39 | }); 40 | }; 41 | Processor.prototype.someElements = function(source, template) { 42 | var me = this; 43 | var templateKeys = _.keys(template); 44 | return _.map(source, function(subsource, index) { 45 | var subtemplate; 46 | if (templateKeys.indexOf(index.toString()) >= 0) { 47 | subtemplate = template[index]; 48 | } else { 49 | subtemplate = template; 50 | } 51 | return me.process(subsource, subtemplate); 52 | }); 53 | }; 54 | 55 | /* Extractor */ 56 | function Extractor() { 57 | Processor.call(this); 58 | } 59 | Extractor.prototype = Object.create(Processor.prototype); 60 | Extractor.prototype.constructor = Extractor; 61 | Extractor.prototype.level = function(source, template) { 62 | var me = this; 63 | var sourceKeys = _.keys(source); 64 | var templateKeys = _.keys(template); 65 | var intersectKeys = []; 66 | if (templateKeys.indexOf('*') >= 0) { 67 | intersectKeys = sourceKeys; 68 | } else { 69 | intersectKeys = _.intersection(sourceKeys, templateKeys); 70 | } 71 | 72 | return me.extractLevel(intersectKeys, source, template); 73 | }; 74 | Extractor.prototype.extractLevel = function(keys, obj, template) { 75 | var me = this; 76 | var result = {}; 77 | _.each(keys, function(key) { 78 | if (template['*']) { 79 | result[key] = me.process(obj[key], template['*']); 80 | } else { 81 | result[key] = me.process(obj[key], template[key]); 82 | } 83 | }); 84 | return result; 85 | }; 86 | Extractor.prototype.someElements = function(source, template) { 87 | var me = this; 88 | var templateKeys = _.keys(template); 89 | var selectedElements = []; 90 | _.each(source, function(subsource, index) { 91 | if (templateKeys.indexOf(index.toString()) >= 0) { 92 | selectedElements.push(me.process(subsource, template[index])); 93 | } 94 | }); 95 | return selectedElements; 96 | }; 97 | 98 | /* Renamer */ 99 | function Renamer() { 100 | Processor.call(this); 101 | } 102 | Renamer.prototype = Object.create(Processor.prototype); 103 | Renamer.prototype.constructor = Renamer; 104 | Renamer.prototype.level = function(source, template) { 105 | var me = this; 106 | var result = {}; 107 | var templateKeys = _.keys(template); 108 | var sourceKeys = _.keys(source); 109 | 110 | result = _.clone(source); 111 | 112 | _.each(templateKeys, function(key, value) { 113 | if (template['*']) { 114 | _.each(sourceKeys, function(sourceKey) { 115 | result[sourceKey] = me.process(source[sourceKey], template['*']); 116 | }); 117 | } else { 118 | var propertyTemplate = template[key], 119 | oldProp, 120 | subTemplate; 121 | if (_.isArray(propertyTemplate)) { 122 | oldProp = propertyTemplate[0]; 123 | subTemplate = propertyTemplate[1]; 124 | } else if (_.isObject(propertyTemplate)) { 125 | oldProp = key; 126 | subTemplate = propertyTemplate; 127 | } else { 128 | oldProp = propertyTemplate; 129 | subTemplate = {}; 130 | } 131 | delete result[oldProp]; 132 | if (source[oldProp] !== undefined) { 133 | result[key] = me.process(source[oldProp], subTemplate); 134 | } 135 | } 136 | }); 137 | return result; 138 | }; 139 | 140 | /* Transformer */ 141 | function Transformer() { 142 | Processor.call(this); 143 | this.cachedTagMatcher = {}; 144 | } 145 | Transformer.prototype = Object.create(Processor.prototype); 146 | Transformer.prototype.constructor = Transformer; 147 | Transformer.prototype.someElements = function(source, template) { 148 | var me = this; 149 | return _.map(source, function(subsource, index) { 150 | return me.process(subsource, template); 151 | }); 152 | }; 153 | Transformer.prototype.level = function(source, template) { 154 | var me = this; 155 | var result = {}; 156 | var obj; 157 | _.each(template, function(value, key) { 158 | if (_.isArray(value)) { 159 | var formatFn = value[1]; 160 | if (_.isFunction(formatFn)) { 161 | obj = me.findKey(source, value[0]); 162 | result[key] = formatFn(obj); 163 | } else { 164 | result[key] = _.toArray(me.process(source, value)); 165 | } 166 | } else if (_.isObject(value)) { 167 | result[key] = me.process(source, value); 168 | } else { 169 | obj = me.findKey(source, value); 170 | if (obj !== undefined) { 171 | result[key] = me.findKey(source, value); 172 | } 173 | } 174 | }); 175 | return result; 176 | }; 177 | Transformer.prototype.tagMatches = function tagMatches(mask, tag) { 178 | if (!this.cachedTagMatcher[mask]) { 179 | var rxStr = '^' + mask.split('*').join('.*') + '$'; 180 | this.cachedTagMatcher[mask] = new RegExp(rxStr, 'i'); 181 | } 182 | return tag.match(this.cachedTagMatcher[mask]); 183 | }; 184 | 185 | Transformer.prototype.findKey = function(source, propString) { 186 | if (!propString) { 187 | return source; 188 | } 189 | 190 | var me = this; 191 | var props = propString.split('.'); 192 | var prop = props.shift(); 193 | 194 | if (prop.indexOf('*') >= 0) { 195 | var obj; 196 | var elems = []; 197 | var isSourceArr = source instanceof Array; 198 | _.each(_.keys(source), function(sourceKey) { 199 | if (me.tagMatches(prop, sourceKey) || isSourceArr) { 200 | obj = me.findKey(source[sourceKey], props.join('.')); 201 | if (obj !== undefined) { 202 | elems.push(obj); 203 | } 204 | } 205 | }); 206 | if (elems.length === 1) { 207 | return _.first(elems); 208 | } 209 | return _.flatten(elems, true); 210 | } 211 | 212 | var candidate = source[prop]; 213 | if (props.length > 0 && candidate !== undefined) { 214 | return me.findKey(candidate, props.join('.')); 215 | } 216 | return candidate; 217 | }; 218 | 219 | /* Exports */ 220 | var extract = new Extractor(); 221 | exports.extract = function(source, template) { 222 | return extract.process(source, template); 223 | }; 224 | var rename = new Renamer(); 225 | exports.rename = function(source, template) { 226 | return rename.process(source, template); 227 | }; 228 | var transform = new Transformer(); 229 | exports.transform = function(source, template) { 230 | return transform.process(source, template); 231 | }; 232 | -------------------------------------------------------------------------------- /test/test.extract.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | var expect = require('chai').expect; 3 | 4 | var fanci = require('../lib/fanci'); 5 | var source = require('../example/source'); 6 | 7 | describe('Extract full object', function() { 8 | it('should return the whole objects as it was before', function() { 9 | var template = { 10 | '*': true 11 | }; 12 | expect(fanci.extract(source, template)).to.be.deep.equal(source); 13 | }); 14 | }); 15 | 16 | describe('Extract empty template', function() { 17 | it('should return an empty object', function() { 18 | var template = {}; 19 | expect(fanci.extract(source, template)).to.be.empty; 20 | }); 21 | }); 22 | 23 | describe('Extract empty source', function() { 24 | it('should return an empty object', function() { 25 | var template = { 26 | 'products': { 27 | '*': true 28 | } 29 | }; 30 | expect(fanci.extract({}, template)).to.be.empty; 31 | }); 32 | }); 33 | 34 | describe('Extract arbitrary keys with *', function() { 35 | it('should return all objects', function() { 36 | var template = { 37 | 'products': { 38 | '*': true 39 | } 40 | }; 41 | expect(fanci.extract(source, template)).to.be.deep.equal({ 42 | "products": { 43 | "1234": { 44 | "id": 1234, 45 | "internal_id": "X04BEEF", 46 | "name": "The Beef", 47 | "status": { 48 | "available": true 49 | }, 50 | "delivery": { 51 | "company": "My Transport", 52 | "rate": "business_hour", 53 | "time": "daily" 54 | } 55 | }, 56 | "4567": { 57 | "id": 4567, 58 | "internal_id": "X08CAFE", 59 | "name": "El Coffee", 60 | "status": { 61 | "available": true 62 | }, 63 | "delivery": { 64 | "company": "Ayayay", 65 | "rate": "weekend", 66 | "time": "weekend" 67 | } 68 | }, 69 | "6789": { 70 | "id": 6789, 71 | "internal_id": "X07DEAD", 72 | "name": "Life Product", 73 | "status": { 74 | "available": false 75 | }, 76 | "delivery": { 77 | "company": "My Transport", 78 | "rate": "business_hour", 79 | "time": "weekend" 80 | } 81 | }, 82 | "char:1": { 83 | "internal_id": "char:1" 84 | }, 85 | "char:2": { 86 | "internal_id": "char:2" 87 | }, 88 | "bar:1": { 89 | "internal_id": "bar:1" 90 | }, 91 | "foo:1": { 92 | "internal_id": "foo:1" 93 | } 94 | } 95 | }); 96 | }); 97 | }); 98 | 99 | describe('Extract only a subset of keys', function() { 100 | it('should return objects with only those keys', function() { 101 | var template = { 102 | 'products': { 103 | '*': { 104 | 'id': true, 105 | 'name': true, 106 | 'status': true 107 | } 108 | } 109 | }; 110 | expect(fanci.extract(source, template)).to.be.deep.equal({ 111 | "products": { 112 | "1234": { 113 | "id": 1234, 114 | "name": "The Beef", 115 | "status": { 116 | "available": true 117 | } 118 | }, 119 | "4567": { 120 | "id": 4567, 121 | "name": "El Coffee", 122 | "status": { 123 | "available": true 124 | } 125 | }, 126 | "6789": { 127 | "id": 6789, 128 | "name": "Life Product", 129 | "status": { 130 | "available": false 131 | } 132 | }, 133 | "char:1": {}, 134 | "char:2": {}, 135 | "bar:1": {}, 136 | "foo:1": {} 137 | } 138 | }); 139 | }); 140 | }); 141 | 142 | describe('Extract an array of objects', function() { 143 | it('should return the array of objects', function() { 144 | var template = { 145 | 'docs': true 146 | }; 147 | expect(fanci.extract(source, template)).to.be.deep.equal({ 148 | "docs": [ 149 | { 150 | "author": "Gandalf", 151 | "date": "2014-02-03", 152 | "description": "Put some magic in here" 153 | }, 154 | { 155 | "author": "Harry", 156 | "date": "2014-02-04", 157 | "description": "Rainbow Unicorns!" 158 | }, 159 | { 160 | "author": "Phil", 161 | "date": "2014-05-19", 162 | "description": "Valuable information" 163 | }, 164 | { 165 | "author": "Odi", 166 | "date": "2014-05-22", 167 | "description": "Fanci stuff!" 168 | } 169 | ] 170 | }); 171 | }); 172 | }); 173 | 174 | describe('Extract object subset from array', function() { 175 | it('should return objects with only those keys', function() { 176 | var template = { 177 | 'docs': { 178 | '*': { 179 | 'author': true 180 | } 181 | } 182 | }; 183 | expect(fanci.extract(source, template)).to.be.deep.equal({ 184 | "docs": [ 185 | { 186 | "author": "Gandalf" 187 | }, 188 | { 189 | "author": "Harry" 190 | }, 191 | { 192 | "author": "Phil" 193 | }, 194 | { 195 | "author": "Odi" 196 | } 197 | ] 198 | }); 199 | }); 200 | }); 201 | 202 | describe('Extract from array', function() { 203 | it('should return an array with the extacted objects', function() { 204 | var template = { 205 | '*': { 206 | 'author': true 207 | } 208 | }; 209 | expect(fanci.extract(source.docs, template)).to.be.deep.equal([ 210 | { 211 | "author": "Gandalf" 212 | }, 213 | { 214 | "author": "Harry" 215 | }, 216 | { 217 | "author": "Phil" 218 | }, 219 | { 220 | "author": "Odi" 221 | } 222 | ]); 223 | }); 224 | }); 225 | 226 | describe('Extract subset from array', function() { 227 | it('should return only the specified subset of the array', function() { 228 | var template = { 229 | 'docs': { 230 | '0': { 231 | 'author': true 232 | }, 233 | '3': { 234 | 'description': true 235 | } 236 | } 237 | }; 238 | expect(fanci.extract(source, template)).to.be.deep.equal({ 239 | "docs": [ 240 | { 241 | "author": "Gandalf" 242 | }, 243 | { 244 | "description": "Fanci stuff!" 245 | } 246 | ] 247 | }); 248 | }); 249 | }); 250 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fanci 2 | ===== 3 | 4 | [![Build Status](https://travis-ci.org/liip/fanci.svg?branch=master)](https://travis-ci.org/liip/fanci) 5 | [![Dependency Status](https://david-dm.org/liip/fanci.svg)](https://david-dm.org/liip/fanci) 6 | [![devDependency Status](https://david-dm.org/liip/fanci/dev-status.svg)](https://david-dm.org/liip/fanci#info=devDependencies) 7 | 8 | [![NPM](https://nodei.co/npm/fanci.png)](https://nodei.co/npm/fanci/) 9 | 10 | Fanci is a lightweight node module to extract a subsets (using `extract()`), rename keys (using `rename()`) or tranform the structure (using `transform()`) of a JSON based on a template. 11 | 12 | The initial goal was to consume a large JSON from an external API, and extract a smaller JSON with only the relevant fields. 13 | Unfortunately the available solutions did not really solve this problem (e.g. [json-path][json-path], [jsont][jsont], [json2json][json2json], [JSONStream][jsonstream], ...), at least not up to this level that we needed. 14 | 15 | 16 | * `extract()` does not change the original structure of the object, it extracts a subset of its keys 17 | * `rename()` does not change the original structure of the object, it can rename keys. All not renamed keys remain the same. 18 | * `transform()` changes the structure of the object, only the defined keys will be in the resulting object 19 | 20 | All these methods take a source object as their first parameter and a template as their second. The template defines how the resulting JSON looks like. 21 | 22 | 23 | ## Usage 24 | 25 | Using `fanci` is very easy. All you need is your original JSON and a template which defines whats to do. 26 | You can find more examples in the example and test directory. 27 | 28 | ### `extract` keys from JSON 29 | 30 | ```javascript 31 | var fanci = require('fanci'); 32 | 33 | var original = { 34 | "products": { 35 | "1234": { 36 | "id": 1234, 37 | "internal_id": "X04BEEF", 38 | "name": "The Beef", 39 | "status": { 40 | "available": true 41 | }, 42 | "delivery": { 43 | "company": "My Transport", 44 | "rate": "business_hour", 45 | "time": "daily" 46 | } 47 | }, 48 | "4567": { 49 | "id": 4567, 50 | "internal_id": "X08CAFE", 51 | "name": "El Coffee", 52 | "status": { 53 | "available": true 54 | }, 55 | "delivery": { 56 | "company": "Ayayay", 57 | "rate": "weekend", 58 | "time": "weekend" 59 | } 60 | } 61 | } 62 | }; 63 | 64 | var template = { 65 | 'products': { 66 | '*': { 67 | 'id': true, 68 | 'name': true 69 | } 70 | } 71 | } 72 | var target = fanci.extract(original, template); 73 | ``` 74 | 75 | #### Result 76 | 77 | `target` now contains the JSON with the fields from the template: 78 | 79 | ```javascript 80 | { 81 | "products": { 82 | "1234": { 83 | "id": 1234, 84 | "name": "The Beef" 85 | }, 86 | "4567": { 87 | "id": 4567, 88 | "name": "El Coffee" 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | 95 | #### Template 96 | 97 | The given JSON is compared to the template JSON. The structure can not be changed, i.e. each level in the original has its equivalent in the template. 98 | If the template does not specify deeper levels, the original JSON is transfered. 99 | 100 | ```javascript 101 | { 102 | 'pic': { 103 | 'id': true, 104 | 'date': true, 105 | 'author': { // from the 'author' object only 'name' is extracted 106 | 'name': true 107 | }, 108 | 'urls': true // if 'urls' is an object, the whole object is extracted 109 | } 110 | } 111 | ``` 112 | 113 | When dealing with arrays you can specify single array positions as object keys. 114 | 115 | ```javascript 116 | { 117 | 'posts': { // here only the 3rd and 8th item from the posts array are extracted 118 | '2': true // the whole object is extracted 119 | '7': { // only the comments field containing the first comment is extracted 120 | 'comments': { 121 | '0': true 122 | } 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | ### `rename` keys from JSON 129 | 130 | ```javascript 131 | var fanci = require('fanci'); 132 | 133 | var original = { 134 | "products": { 135 | "1234": { 136 | "name": "The Beef", 137 | "status": { 138 | "available": true 139 | }, 140 | "delivery": { 141 | "company": "My Transport", 142 | "time": "daily" 143 | } 144 | }, 145 | "4567": { 146 | "name": "El Coffee", 147 | "status": { 148 | "available": true 149 | }, 150 | "delivery": { 151 | "company": "Ayayay", 152 | "time": "weekend" 153 | } 154 | } 155 | } 156 | }; 157 | 158 | var template = { 159 | 'stock': ['products', { 160 | '*': { 161 | 'transport': 'delivery', 162 | 'status': { 163 | 'in_stock': 'available' 164 | } 165 | } 166 | }] 167 | } 168 | var target = fanci.rename(origial, template); 169 | ``` 170 | 171 | #### Result 172 | 173 | `target` now contains the JSON with the renamed keys from the template: 174 | 175 | ```javascript 176 | { 177 | "stock": { 178 | "1234": { 179 | "name": "The Beef", 180 | "status": { 181 | "in_stock": true 182 | }, 183 | "transport": { 184 | "company": "My Transport", 185 | "time": "daily" 186 | } 187 | }, 188 | "4567": { 189 | "name": "El Coffee", 190 | "status": { 191 | "in_stock": true 192 | }, 193 | "transport": { 194 | "company": "Ayayay", 195 | "time": "weekend" 196 | } 197 | } 198 | } 199 | } 200 | ``` 201 | 202 | #### Template 203 | 204 | In the template the new names are defined. For each new name, the old key has to be given as its value. To be able to change parent keys for objects and array, the template supports arrays to define the new names of the keys. That way arbitrary structures can processed. 205 | 206 | ```javascript 207 | { 208 | 'books': { 209 | 'id': 'identifier', //rename key 'identifier' to 'id' 210 | 'writer': ['author', { //rename 'author' to 'writer' AND specify further rules for the next level 211 | 'name': 'title' // rename the authors 'title' property to 'name' 212 | }] 213 | } 214 | } 215 | ``` 216 | 217 | When dealing with arrays you can specify single array positions as object keys. 218 | 219 | ```javascript 220 | { 221 | 'posts': { // here only the 3rd and 8th item from the posts array are renamed 222 | '2': { 223 | 'name': 'fullname' 224 | }, 225 | '7': { 226 | 'name': 'firstname' 227 | } 228 | } 229 | } 230 | ``` 231 | 232 | ### `transform` the structure of a JSON 233 | 234 | ```javascript 235 | var fanci = require('fanci'); 236 | 237 | var original = { 238 | "products": { 239 | "1234": { 240 | "id": 1234, 241 | "internal_id": "X04BEEF", 242 | "name": "The Beef", 243 | "status": { 244 | "available": true 245 | }, 246 | "delivery": { 247 | "company": "My Transport", 248 | "rate": "business_hour", 249 | "time": "daily" 250 | } 251 | }, 252 | "4567": { 253 | "id": 4567, 254 | "internal_id": "X08CAFE", 255 | "name": "El Coffee", 256 | "status": { 257 | "available": true 258 | }, 259 | "delivery": { 260 | "company": "Ayayay", 261 | "rate": "weekend", 262 | "time": "weekend" 263 | } 264 | } 265 | } 266 | }; 267 | 268 | var template = { 269 | 'id': 'products.1234.internal_id', 270 | 'company': 'products.4567.delivery.company', 271 | 'name': [ 272 | 'products.6789.name', 273 | function(value) { 274 | return value.toUpperCase(); 275 | } 276 | ], 277 | 'available': 'products.*.status.available' 278 | } 279 | var target = fanci.transform(origial, template); 280 | ``` 281 | 282 | #### Result 283 | 284 | `target` now contains the JSON with the fields from the template: 285 | 286 | ```javascript 287 | { 288 | "id": "X04BEEF", 289 | "company": "Ayayay", 290 | "name": "LIFE PRODUCT", 291 | "available": [ 292 | true, 293 | true 294 | ] 295 | } 296 | ``` 297 | 298 | #### Template 299 | 300 | The template defines the new structure of the resulting object. The values are _paths_ in the original JSON. Like that, it is possible to select nested elements and to put them in a new strucutre. By using the asteriks all elements of a level are considered. The resulting array in flattend or even removed completly if it only contains one item. 301 | It is possible to specify a format function to be applied to the object extracted from the path. This opens new possibilities to generate more complex structures. To do this, you have to specify an array instead of the path string, the first element is the path string, the second one is a function that takes the extracted object as an argument. 302 | If the second element is not a function, it is assumed that you wanted to construct an array in the resulting object. 303 | 304 | ```javascript 305 | { 306 | 'pics': { 307 | 'id': 'pics.id', 308 | 'dates': [ 309 | 'pics.1.date', 310 | 'pics.3.date', 311 | 'pics.5.date', 312 | ], 313 | 'authors': 'pics.*.author.name' 314 | }, 315 | 'date': [ 316 | 'Date', 317 | function(value) { 318 | return new Date(value); 319 | } 320 | ] 321 | } 322 | ``` 323 | 324 | ### Special meaning of the `*` character 325 | 326 | The asterisk (`*`) has a special meaning in the template: 327 | 328 | 1. It means to use all keys from one level, this is useful when your JSON contains arbitrary keys 329 | 330 | ```javascript 331 | { 332 | 'products': { 333 | '*': { // all keys below products are taken, but only with the 'name' key 334 | 'name': true 335 | } 336 | } 337 | } 338 | ``` 339 | 2. For arrays the asterisk represents all array elements 340 | 341 | ```javascript 342 | { 343 | 'docs': { 344 | '*': { // if docs is an array, '*' ensures that all elements are extracted 345 | 'author': true 346 | } 347 | } 348 | } 349 | ``` 350 | 351 | 3. The same applies in the "path" that is used for `transform()` 352 | 353 | ```javascript 354 | { 355 | 'authors': 'docs.*.author' 356 | } 357 | ``` 358 | 359 | 4. In fact you can use * as a wildcard in the "path" 360 | 361 | ```javascript 362 | { 363 | // returns properties like 'name', 'my_name' or 'name_of_product' of products with ids starting with 4 and ending with 7 (e.g. 4567) 364 | 'ids': 'products.4*7.*name*' 365 | } 366 | ``` 367 | 368 | 369 | ## Tests 370 | 371 | To run the tests simply use the following command: 372 | 373 | ```bash 374 | npm test 375 | ``` 376 | 377 | ## Release 378 | 379 | To create a new release follow these steps: 380 | 381 | 1. Update the version number in `package.json` 382 | 1. Update the `CHANGELOG.md` 383 | 1. Create a [new release/tag on GitHub](https://github.com/liip/fanci/releases) 384 | 1. Publish the release with npm: `npm publish` 385 | 386 | [json-path]: https://github.com/flitbit/json-path 387 | [jsont]: https://github.com/CamShaft/jsont 388 | [json2json]: https://github.com/joelvh/json2json 389 | [jsonstream]: https://github.com/dominictarr/JSONStream 390 | -------------------------------------------------------------------------------- /test/test.transform.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | 3 | var expect = require('chai').expect; 4 | var _ = require('underscore'); 5 | 6 | var fanci = require('../lib/fanci'); 7 | var source = require('../example/source'); 8 | 9 | describe('Return only one of two root keys', function() { 10 | it('should return the docs object with new key', function() { 11 | var template = { 12 | 'documents': 'docs' 13 | }; 14 | var result = fanci.transform(source, template); 15 | 16 | expect(Object.keys(result)).to.be.deep.equal([ 'documents' ]); 17 | }); 18 | }); 19 | 20 | describe('Return only the sub-object', function() { 21 | it('should return the docs object with new key', function() { 22 | var template = { 23 | 'status1234': 'products.1234.status' 24 | }; 25 | var result = fanci.transform(source, template); 26 | 27 | expect(result).to.be.deep.equal({ 28 | 'status1234': { 29 | 'available': true 30 | } 31 | }); 32 | }); 33 | }); 34 | 35 | describe('Using an empty template', function() { 36 | it('should return an empty object', function() { 37 | var template = {}; 38 | var result = fanci.transform(source, template); 39 | expect(result).to.be.empty; 40 | }); 41 | }); 42 | 43 | describe('Using a * template', function() { 44 | it('should return its values as an array', function() { 45 | var obj = { 46 | 'foo': 'bar', 47 | 'list': [ 48 | { 'test': 1 }, 49 | { 'test': 2 } 50 | ] 51 | }; 52 | var template = { 'test': '*' }; 53 | 54 | var result = fanci.transform(obj, template); 55 | 56 | expect(result['test']).to.be.deep.equal([ 57 | 'bar', 58 | { 'test': 1 }, 59 | { 'test': 2 } 60 | ]); 61 | }); 62 | }); 63 | 64 | describe('Using the wrong template path', function() { 65 | it('should return an undefined object', function() { 66 | var template = { 67 | 'status1234': 'products.xyz' 68 | }; 69 | var result = fanci.transform(source, template); 70 | 71 | expect(result).to.be.deep.equal({}); 72 | }); 73 | }); 74 | 75 | describe('Use several templates at once', function() { 76 | it('should return the correct object for each given template', function() { 77 | var template = { 78 | 'status1234': 'products.1234.status', 79 | 'status4567': 'products.4567.status' 80 | }; 81 | var result = fanci.transform(source, template); 82 | 83 | expect(result).to.be.deep.equal( 84 | { 85 | 'status1234': { 86 | 'available': true 87 | }, 88 | 'status4567': { 89 | 'available': true 90 | } 91 | } 92 | ); 93 | }); 94 | }); 95 | 96 | describe('Transform objects from an array', function() { 97 | it('should return the correct object for each array template', function() { 98 | var template = { 99 | 'author': 'docs.1.author', 100 | 'desc': 'docs.0.description', 101 | 'delivery': 'products.6789.delivery' 102 | }; 103 | var result = fanci.transform(source, template); 104 | 105 | expect(result).to.be.deep.equal( 106 | { 107 | 'author': 'Harry', 108 | 'desc': 'Put some magic in here', 109 | 'delivery': { 110 | 'company': 'My Transport', 111 | 'rate': 'business_hour', 112 | 'time': 'weekend' 113 | } 114 | } 115 | ); 116 | }); 117 | }); 118 | 119 | describe('Use the * to jump over one level', function() { 120 | it('should return the correct object for the wildcard', function() { 121 | var template = { 122 | 'authors': 'docs.*.author' 123 | }; 124 | var result = fanci.transform(source, template); 125 | 126 | expect(result).to.be.deep.equal({ 127 | 'authors': [ 128 | 'Gandalf', 129 | 'Harry', 130 | 'Phil', 131 | 'Odi' 132 | ] 133 | }); 134 | }); 135 | }); 136 | 137 | describe('Use the * to jump over several levels', function() { 138 | it('should return the correct object for the wildcards', function() { 139 | var template = { 140 | 'available': 'products.*.*.available' 141 | }; 142 | var result = fanci.transform(source, template); 143 | 144 | expect(result).to.be.deep.equal({ 145 | 'available': [ 146 | true, 147 | true, 148 | false 149 | ] 150 | }); 151 | }); 152 | }); 153 | 154 | describe('Use the * to jump over one non-array level', function() { 155 | it('should return the correct object without array', function() { 156 | var template = { 157 | 'company': '*.company' 158 | }; 159 | var result = fanci.transform(source.products['1234'], template); 160 | 161 | expect(result).to.be.deep.equal({ 162 | 'company': 'My Transport' 163 | }); 164 | }); 165 | }); 166 | 167 | describe('Use the * if there are no value', function() { 168 | it('should return the an empty array', function() { 169 | var template = { 170 | 'company': '*.companies' 171 | }; 172 | var result = fanci.transform(source.products['1234'], template); 173 | 174 | expect(result).to.be.deep.equal({ 175 | 'company': [] 176 | }); 177 | }); 178 | }); 179 | 180 | describe('Use a nested template for more complex objects', function() { 181 | it('should return an object structured like the nested template', function() { 182 | var template = { 183 | 'transport': { 184 | 'company': 'products.1234.delivery.company' 185 | }, 186 | 'ids': 'products.*.id', 187 | 'state': { 188 | 'level': 'products.*.status' 189 | } 190 | }; 191 | var result = fanci.transform(source, template); 192 | 193 | expect(result).to.be.deep.equal({ 194 | 'transport': { 195 | 'company': 'My Transport' 196 | }, 197 | 'ids': [ 1234, 4567, 6789 ], 198 | 'state': { 199 | 'level': [ 200 | { 'available': true }, 201 | { 'available': true }, 202 | { 'available': false } 203 | ] 204 | } 205 | }); 206 | }); 207 | }); 208 | 209 | describe('Construct an array', function() { 210 | it('should return the array with the corresponding values', function() { 211 | var template = { 212 | 'names': [ 213 | 'products.1234.name', 214 | 'products.4567.name', 215 | 'products.6789.name' 216 | ] 217 | }; 218 | var result = fanci.transform(source, template); 219 | 220 | expect(result['names']).to.be.deep.equal( 221 | [ 222 | 'The Beef', 223 | 'El Coffee', 224 | 'Life Product' 225 | ] 226 | ); 227 | }); 228 | }); 229 | 230 | describe('Use a format function', function() { 231 | it('should return the formatted object', function() { 232 | var template = { 233 | 'upper': [ 234 | 'products.1234.name', 235 | function(value) { 236 | return value.toUpperCase(); 237 | } 238 | ] 239 | }; 240 | var result = fanci.transform(source, template); 241 | 242 | expect(result).to.be.deep.equal( 243 | { 244 | 'upper': 'THE BEEF' 245 | } 246 | ); 247 | }); 248 | }); 249 | 250 | describe('Use a format function on an array', function() { 251 | it('should return the formatted object', function() { 252 | var template = { 253 | 'lower_names': [ 254 | 'products.*.name', 255 | function(values) { 256 | return _.map(values, function(value) { 257 | return value.toLowerCase(); 258 | }); 259 | } 260 | ] 261 | }; 262 | var result = fanci.transform(source, template); 263 | 264 | expect(result).to.be.deep.equal( 265 | { 266 | 'lower_names': [ 267 | 'the beef', 268 | 'el coffee', 269 | 'life product' 270 | ] 271 | } 272 | ); 273 | }); 274 | }); 275 | 276 | describe('Use a more complex transformation with two format functions', function() { 277 | it('should return the new transformed and formatted object', function() { 278 | var obj = { 279 | 'Datum': '2014-03-15', 280 | 'Oel': 'X', 281 | 'Glas': '', 282 | 'Metall': 'X' 283 | }; 284 | var formatFn = function(value) { 285 | return (value === 'X'); 286 | }; 287 | var template = { 288 | 'date': [ 289 | 'Datum', 290 | function(value) { 291 | var dateObj = new Date(value); 292 | return dateObj.toISOString(); 293 | } 294 | ], 295 | 'kind': { 296 | 'oil': [ 'Oel', formatFn ], 297 | 'glass': [ 'Glas', formatFn ], 298 | 'metal': [ 'Metall', formatFn ] 299 | } 300 | }; 301 | var result = fanci.transform(obj, template); 302 | 303 | expect(result).to.be.deep.equal( 304 | { 305 | 'date': '2014-03-15T00:00:00.000Z', 306 | 'kind': { 307 | 'oil': true, 308 | 'glass': false, 309 | 'metal': true 310 | } 311 | } 312 | ); 313 | }); 314 | }); 315 | 316 | describe('Return only the internal_id value', function() { 317 | it('should return internal_id value match based on wildcard *ar:*', function() { 318 | var template = { 319 | 'match_only_*ar:*': 'products.*ar:*.internal_id' 320 | }; 321 | var result = fanci.transform(source, template); 322 | 323 | expect(result).to.be.deep.equal({ 324 | 'match_only_*ar:*': [ 325 | 'char:1', 326 | 'char:2', 327 | 'bar:1' 328 | ] 329 | }); 330 | }); 331 | }); 332 | 333 | describe('Return products containing 4 in id', function() { 334 | it('should return products 1234 and 4567', function() { 335 | var template = { 336 | 'four_products': 'products.*4*.name' 337 | }; 338 | var result = fanci.transform(source, template); 339 | 340 | expect(result).to.be.deep.equal({ 341 | 'four_products': [ 342 | 'The Beef', 343 | 'El Coffee' 344 | ] 345 | }); 346 | }); 347 | }); 348 | 349 | describe('Return products ending with 4', function() { 350 | it('should return product 1234', function() { 351 | var template = { 352 | 'four_products': 'products.*4.name' 353 | }; 354 | var result = fanci.transform(source, template); 355 | 356 | expect(result).to.be.deep.equal({ 357 | 'four_products': 'The Beef' 358 | }); 359 | }); 360 | }); 361 | 362 | describe('Return products starting with 4', function() { 363 | it('should return product 4567', function() { 364 | var template = { 365 | 'four_products': 'products.4*.name' 366 | }; 367 | var result = fanci.transform(source, template); 368 | 369 | expect(result).to.be.deep.equal({ 370 | 'four_products': 'El Coffee' 371 | }); 372 | }); 373 | }); 374 | 375 | describe('Return ids of products starting with 4 and ending with 7', function() { 376 | it('should return ids of product 4567', function() { 377 | var template = { 378 | 'ids': 'products.4*7.*id*' 379 | }; 380 | var result = fanci.transform(source, template); 381 | 382 | expect(result).to.be.deep.equal({ 383 | 'ids': [ 384 | 4567, 385 | 'X08CAFE' 386 | ] 387 | }); 388 | }); 389 | }); 390 | -------------------------------------------------------------------------------- /test/test.rename.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | 3 | var expect = require('chai').expect; 4 | var _ = require('underscore'); 5 | 6 | var fanci = require('../lib/fanci'); 7 | var source = require('../example/source'); 8 | 9 | describe('Rename root keys of object', function() { 10 | it('should return the whole object with new root keys', function() { 11 | var template = { 12 | 'stock': 'products', 13 | 'library': 'docs' 14 | }; 15 | var result = fanci.rename(source, template); 16 | 17 | expect(Object.keys(result)).to.be.deep.equal([ 'stock', 'library' ]); 18 | }); 19 | }); 20 | 21 | describe('Rename with empty template', function() { 22 | it('should return the same object as before', function() { 23 | var obj = { 24 | 'test': { 25 | 'hallo': 'velo' 26 | }, 27 | 'langs': [ 'PHP', 'JavaScript', 'Sass' ] 28 | }; 29 | var template = {}; 30 | var result = fanci.rename(obj, template); 31 | 32 | expect(result).to.be.deep.equal(obj); 33 | }); 34 | }); 35 | 36 | describe('Rename with non-matching template', function() { 37 | it('should return the same object as before', function() { 38 | var obj = { 39 | 'test': { 40 | 'hallo': 'velo' 41 | }, 42 | 'langs': [ 'PHP', 'JavaScript', 'Sass' ] 43 | }; 44 | var template = { 45 | 'language': 'lang' 46 | }; 47 | var result = fanci.rename(obj, template); 48 | 49 | expect(result).to.be.deep.equal(obj); 50 | }); 51 | }); 52 | 53 | describe('Rename with empty source', function() { 54 | it('should return an empty object', function() { 55 | var obj = {}; 56 | var template = { 57 | 'language': 'lang' 58 | }; 59 | var result = fanci.rename(obj, template); 60 | 61 | expect(result).to.be.empty; 62 | }); 63 | }); 64 | 65 | describe('Rename parent-only', function() { 66 | it('should return the object with renamed parent keys', function() { 67 | var obj = { 68 | 'test': { 69 | 'hallo': { 70 | 'velo': 'foobar' 71 | } 72 | }, 73 | 'langs': [ 'PHP', 'JavaScript', 'Sass' ] 74 | }; 75 | var template1 = { 76 | 'velo': 'test' 77 | }; 78 | var template2 = { 79 | 'velo': [ 'test' ] 80 | }; 81 | 82 | var result1 = fanci.rename(obj, template1); 83 | expect(result1).to.be.deep.equal({ 84 | 'velo': { 85 | 'hallo': { 86 | 'velo': 'foobar' 87 | } 88 | }, 89 | 'langs': [ 'PHP', 'JavaScript', 'Sass' ] 90 | }); 91 | 92 | var result2 = fanci.rename(obj, template2); 93 | expect(result2).to.be.deep.equal({ 94 | 'velo': { 95 | 'hallo': { 96 | 'velo': 'foobar' 97 | } 98 | }, 99 | 'langs': [ 'PHP', 'JavaScript', 'Sass' ] 100 | }); 101 | }); 102 | }); 103 | 104 | describe('Rename parent and child', function() { 105 | it('should return the object with renamed parent and child keys', function() { 106 | var obj = { 107 | 'test': { 108 | 'hallo': { 109 | 'velo': 'foobar' 110 | } 111 | }, 112 | 'langs': [ 'PHP', 'JavaScript', 'Sass' ] 113 | }; 114 | var template = { 115 | 'velo': [ 116 | 'test', 117 | { 118 | 'hello': [ 119 | 'hallo', 120 | { 121 | 'blubb': 'velo' 122 | } 123 | ] 124 | } 125 | ] 126 | }; 127 | var result = fanci.rename(obj, template); 128 | 129 | expect(result).to.be.deep.equal({ 130 | 'velo': { 131 | 'hello': { 132 | 'blubb': 'foobar' 133 | } 134 | }, 135 | 'langs': [ 'PHP', 'JavaScript', 'Sass' ] 136 | }); 137 | }); 138 | }); 139 | 140 | describe('Rename child-only', function() { 141 | it('should return the object with renamed child keys', function() { 142 | var obj = { 143 | "products": { 144 | "1234": { 145 | "name": "The Beef", 146 | "status": { 147 | "available": true 148 | }, 149 | "delivery": { 150 | "company": "My Transport", 151 | "time": "daily" 152 | } 153 | }, 154 | "4567": { 155 | "name": "El Coffee", 156 | "status": { 157 | "available": true 158 | }, 159 | "delivery": { 160 | "company": "Ayayay", 161 | "time": "weekend" 162 | } 163 | } 164 | } 165 | }; 166 | 167 | var template = { 168 | 'stock': [ 169 | 'products', 170 | { 171 | '*': { 172 | 'transport': 'delivery', 173 | 'status': { 174 | 'in_stock': 'available' 175 | } 176 | } 177 | } 178 | ] 179 | }; 180 | var result = fanci.rename(obj, template); 181 | 182 | expect(result).to.be.deep.equal({ 183 | "stock": { 184 | "1234": { 185 | "name": "The Beef", 186 | "status": { 187 | "in_stock": true 188 | }, 189 | "transport": { 190 | "company": "My Transport", 191 | "time": "daily" 192 | } 193 | }, 194 | "4567": { 195 | "name": "El Coffee", 196 | "status": { 197 | "in_stock": true 198 | }, 199 | "transport": { 200 | "company": "Ayayay", 201 | "time": "weekend" 202 | } 203 | } 204 | } 205 | }); 206 | }); 207 | }); 208 | 209 | describe('Rename child key of object', function() { 210 | it('should return the whole object with the renamed key', function() { 211 | var template = { 212 | '*': { 213 | 'transport': 'delivery' 214 | } 215 | }; 216 | var result = fanci.rename(source.products, template); 217 | expect(result).to.be.deep.equal({ 218 | "1234": { 219 | "id": 1234, 220 | "internal_id": "X04BEEF", 221 | "name": "The Beef", 222 | "status": { 223 | "available": true 224 | }, 225 | "transport": { 226 | "company": "My Transport", 227 | "rate": "business_hour", 228 | "time": "daily" 229 | } 230 | }, 231 | "4567": { 232 | "id": 4567, 233 | "internal_id": "X08CAFE", 234 | "name": "El Coffee", 235 | "status": { 236 | "available": true 237 | }, 238 | "transport": { 239 | "company": "Ayayay", 240 | "rate": "weekend", 241 | "time": "weekend" 242 | } 243 | }, 244 | "6789": { 245 | "id": 6789, 246 | "internal_id": "X07DEAD", 247 | "name": "Life Product", 248 | "status": { 249 | "available": false 250 | }, 251 | "transport": { 252 | "company": "My Transport", 253 | "rate": "business_hour", 254 | "time": "weekend" 255 | } 256 | }, 257 | "char:1": { 258 | "internal_id": "char:1" 259 | }, 260 | "char:2": { 261 | "internal_id": "char:2" 262 | }, 263 | "bar:1": { 264 | "internal_id": "bar:1" 265 | }, 266 | "foo:1": { 267 | "internal_id": "foo:1" 268 | } 269 | }); 270 | }); 271 | }); 272 | 273 | describe('Rename multiple child keys of object', function() { 274 | it('should return the whole object with the renamed keys', function() { 275 | var template = { 276 | '*': { 277 | 'identifier': 'id', 278 | 'transport': 'delivery', 279 | 'quo': [ 'status', { 'vadis': 'available' } ] 280 | } 281 | }; 282 | var result = fanci.rename(source.products, template); 283 | expect(result).to.be.deep.equal({ 284 | "1234": { 285 | "identifier": 1234, 286 | "internal_id": "X04BEEF", 287 | "name": "The Beef", 288 | "quo": { 289 | "vadis": true 290 | }, 291 | "transport": { 292 | "company": "My Transport", 293 | "rate": "business_hour", 294 | "time": "daily" 295 | } 296 | }, 297 | "4567": { 298 | "identifier": 4567, 299 | "internal_id": "X08CAFE", 300 | "name": "El Coffee", 301 | "quo": { 302 | "vadis": true 303 | }, 304 | "transport": { 305 | "company": "Ayayay", 306 | "rate": "weekend", 307 | "time": "weekend" 308 | } 309 | }, 310 | "6789": { 311 | "identifier": 6789, 312 | "internal_id": "X07DEAD", 313 | "name": "Life Product", 314 | "quo": { 315 | "vadis": false 316 | }, 317 | "transport": { 318 | "company": "My Transport", 319 | "rate": "business_hour", 320 | "time": "weekend" 321 | } 322 | }, 323 | "char:1": { 324 | "internal_id": "char:1" 325 | }, 326 | "char:2": { 327 | "internal_id": "char:2" 328 | }, 329 | "bar:1": { 330 | "internal_id": "bar:1" 331 | }, 332 | "foo:1": { 333 | "internal_id": "foo:1" 334 | } 335 | }); 336 | }); 337 | }); 338 | 339 | describe('Rename multiple child keys of object in array', function() { 340 | it('should return the whole array with objects with renamed keys', function() { 341 | var template = { 342 | '*': { 343 | 'writer': 'author' 344 | } 345 | }; 346 | var result = fanci.rename(source.docs, template); 347 | expect(result).to.be.deep.equal([ 348 | { 349 | "writer": "Gandalf", 350 | "date": "2014-02-03", 351 | "description": "Put some magic in here" 352 | }, 353 | { 354 | "writer": "Harry", 355 | "date": "2014-02-04", 356 | "description": "Rainbow Unicorns!" 357 | }, 358 | { 359 | "writer": "Phil", 360 | "date": "2014-05-19", 361 | "description": "Valuable information" 362 | }, 363 | { 364 | "writer": "Odi", 365 | "date": "2014-05-22", 366 | "description": "Fanci stuff!" 367 | } 368 | ]); 369 | }); 370 | }); 371 | 372 | describe('Rename multiple child keys of object in array without asterisk', function() { 373 | it('should return the whole array with objects with renamed keys', function() { 374 | var template = { 375 | 'writer': 'author' 376 | }; 377 | var result = fanci.rename(source.docs, template); 378 | expect(result).to.be.deep.equal([ 379 | { 380 | "writer": "Gandalf", 381 | "date": "2014-02-03", 382 | "description": "Put some magic in here" 383 | }, 384 | { 385 | "writer": "Harry", 386 | "date": "2014-02-04", 387 | "description": "Rainbow Unicorns!" 388 | }, 389 | { 390 | "writer": "Phil", 391 | "date": "2014-05-19", 392 | "description": "Valuable information" 393 | }, 394 | { 395 | "writer": "Odi", 396 | "date": "2014-05-22", 397 | "description": "Fanci stuff!" 398 | } 399 | ]); 400 | }); 401 | }); 402 | 403 | describe('Rename only subset from array', function() { 404 | it('should return the object with only the subset of the array with replaced keys', function() { 405 | var template = { 406 | '0': { 407 | 'writer': 'author' 408 | }, 409 | '3': { 410 | 'desc': 'description' 411 | } 412 | }; 413 | expect(fanci.rename(source.docs, template)).to.be.deep.equal([ 414 | { 415 | "writer": "Gandalf", 416 | "date": "2014-02-03", 417 | "description": "Put some magic in here" 418 | }, 419 | { 420 | "author": "Harry", 421 | "date": "2014-02-04", 422 | "description": "Rainbow Unicorns!" 423 | }, 424 | { 425 | "author": "Phil", 426 | "date": "2014-05-19", 427 | "description": "Valuable information" 428 | }, 429 | { 430 | "author": "Odi", 431 | "date": "2014-05-22", 432 | "desc": "Fanci stuff!" 433 | } 434 | ]); 435 | }); 436 | }); 437 | --------------------------------------------------------------------------------