diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..31e8802 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,5 @@ +extends: eslint:recommended +parserOptions: + ecmaVersion: 5 +env: + node: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f1b497 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/coverage/ +/node_modules/ +/npm-debug.log diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..75dc28a --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +/coverage/ +/test/ +/.editorconfig +/.eslintrc.yml +/.gitignore +/.travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..82d92f2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: node_js +node_js: + - "6" + - "4" + - "0.12" + - "0.10" +after_success: + - npm run coveralls +deploy: + provider: npm + email: + secure: RgE+z8gZdN6PAaEnzPK3EGiv9crsbsVzZuVVM+TsrVwQU1R0/NzbL4nNP85vqS8ZBAIrA9adZIDGNtOpZ8ZGmbgd5CKVtizTF3HFtH2ctAmLerPY+HeniXkzt1+NoVZIzDgE78ubGgT6dRfpwmfyXzE83obdcQUVVBKQIR94295o3pu73oUj8a03IPD4Njbdsd8std+mzRPXXcgV3oVkcsuEVK5HifvzS7LC1owimpTrWvJ28qcUJXp7KZBpdCL3zFApsbhykOfToDHqf1RpXcnD74hkOC8Uc0S4a2ILeCl55OkjiJMzNyM1uqwHaBIUG5JK5+aFUMnkAsHRlrmdJoFeJpLxigUF8QZQYSRBNgzf1V7F4mt6FFd9keksOPJI25KqQdpOSC0He+4IJtpu97iXw2GOhPZ31KUg63wHsPHbqzBDk8xqLqaA+V8Z/4ccMpsQFd+/txzbVhQDZDD/jzLLM4gXsV7fhN5fxX1oZcIiFb+OnG2UYQsjdVPkVjJrDUrSC3HQQUK/XXK5/hwm28emQjBZkRFjE127HiVQi7FCHwJzSoAsypPezL3FJ3kNuSJMOkci8dc0R18kOvig0ofFtogYA/qVsmHl7BB+h0MLkzlV6Cl3DgzaArU4PUp827btZ2bFFF1I1h7FC1yfx3+wi6O1TaMRzt2sxs1hnAA= + api_key: + secure: LM2UzLwKTAuxxXgIZXUb2YV/eWMJm2L5oWbL8OW8z4q5CGmAOgw+iAzqW2zI8daozY3u49oY4aRM1emBjmS3rl12CFHoS1WAO49ypHfwEcWYmHIvEoBanIrVDrapJhv6CYMQKGlUfdTDEpw+cUVY+Vi/ioVBCCgeT0dljtW5JnVFhYeZ2e2P9yazXYMnMbB1Fdb42TY5WyBxAtXqPd0kdgiNuYTETWibKJc1h5ttV0LVGnV78ta+K/+M5DO1/AHE28raIfaH+HMaEcXbA7ygfWedQlcoXZNUe/twQNF3ihfBxBbtVwjCWXwwgkzJG6Exsy6rjmyVTg6p+FDFy1mHgLEJmXHBcBID4kwH+Qg1gnwob+bONUVzVsx946ppxItzshxyUBBayUEovH9Jgr5vf2Jm0ZYHAazZToOvr4NMRdK1MdrsSKKLNluwT/iXbW581SkBtHjTmpCBWS944UDBOYbxXgIT21p98veK99Hiksaqr4VErMMD9UBefyEl5jhC7QmyqH88IF0LlAHOdGGdWHvCbS9MhwsJM6zVLBmLDe237lOoq6Ib2TZGKAT69B1Qd1wuhc5PQtc6S5hRMRaClV3JFHXvdNMZ6B+ISNHzV76xCH3hxfv1/TJjZrsOHgpcqgQ4IWhkPFrgG3Dc5PG/WBwAE2jKmevvK4T5b2G5Cyg= + skip_cleanup: true + on: + repo: akim-mcmath/deep-map-keys + node: "4" + tags: true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6c1f028 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016 Akim McMath + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..23f3065 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# deep-map-keys + +[![Version][version-badge]][npm] +[![License][license-badge]][license] +[![Build][build-badge]][travis] +[![Coverage][coverage-badge]][coveralls] +[![Dependencies][dependencies-badge]][gemnasium] + +Recurses through a JSON-like object and transforms its keys, returning a new object. + +## Install + +Install via [npm][npm]. + +```sh +npm install --save deep-map-keys +``` + +## Usage + +Suppose we want to change the keys of an object from [snake case][snake-case] to +[camel case][camel-case]. We can do something like this: + +```js +const deepMapKeys = require('deep-map-keys'); + +let comment = { + comment_id: 42, + user_id: 1024, + user_name: 'Mufasa', + content: 'You deliberately disobeyed me.', + viewed_by: [ + { user_id: 3820, user_name: 'Zazu' }, + { user_id: 8391, user_name: 'Rafiki' } + ] +}; + +let result = deepMapKeys(comment, key => { + return key.replace(/_(\w)/g, (match, char) => char.toUpperCase()); +}); + +console.log(result); /* +{ + commentId: 42, + userId: 1024, + userName: 'Mufasa', + content: 'You deliberately disobeyed me.', + viewedBy: [ + { userId: 3820, userName: 'Rafiki' }, + { userId: 8391, userName: 'Zazu' } + ] +}; +*/ +``` + +## API + +### `deepMapKeys(object, transformFn, [options])` + +Applies `transformFn` to each key in an object. Keys are visited recursively, +so nested keys will be transformed. A new object is always returned; the +original object is unmodified. + +##### object + +`object` + +The object whose keys are to be transformed. This object may be an `Array`. + +##### transformFn + +`function` + +The function to call for each key. The return value of the function +determines the transformed value. The function is called with a single +argument: + +* **key**: The key being transformed. + +##### options + +`object` (optional) + +An options object. The following options are accepted: + +* **thisArg**: Sets the value of `this` within `transformFn`. + +#### Returns + +`object` + +Returns a new object. + +## License + +Copyright © 2016 Akim McMath. Licensed under the [MIT License][license]. + +[version-badge]: https://img.shields.io/npm/v/deep-map-keys.svg?style=flat-square +[license-badge]: https://img.shields.io/npm/l/deep-map-keys.svg?style=flat-square +[build-badge]: https://img.shields.io/travis/akim-mcmath/deep-map-keys/master.svg?style=flat-square +[coverage-badge]: https://img.shields.io/coveralls/akim-mcmath/deep-map-keys/master.svg?style=flat-square&service=github +[dependencies-badge]: https://img.shields.io/gemnasium/akim-mcmath/deep-map-keys.svg?style=flat-square +[npm]: https://www.npmjs.com/package/deep-map-keys +[license]: LICENSE +[travis]: https://travis-ci.org/akim-mcmath/deep-map-keys +[coveralls]: https://coveralls.io/github/akim-mcmath/deep-map-keys?branch=master +[gemnasium]: https://gemnasium.com/akim-mcmath/deep-map-keys +[snake-case]: https://en.wikipedia.org/wiki/Snake_case +[camel-case]: https://en.wikipedia.org/wiki/CamelCase diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..9a4e014 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,60 @@ +'use strict'; + +var isArray = Array.isArray; + +function deepMapKeys(obj, fn, opts) { + opts = opts || {}; + + if (!fn) { + err(Error, 'transformFn must be defined'); + } else if (!isFunction(fn)) { + err(TypeError, 'transformFn must be a function'); + } else if (!isObject(opts)) { + err(TypeError, 'options must be an object or undefined'); + } + + return map(obj, fn, opts); +} + +function map(value, fn, opts) { + return isArray(value) ? mapArray(value, fn, opts) : + isObject(value) ? mapObject(value, fn, opts) : + value; +} + +function mapArray(arr, fn, opts) { + var result = []; + var len = arr.length; + + for (var i = 0; i < len; i++) { + result.push(map(arr[i], fn, opts)); + } + + return result; +} + +function mapObject(obj, fn, opts) { + var result = {}; + + for (var key in obj) { + result[fn.call(opts.thisArg, key)] = map(obj[key], fn, opts); + } + + return result; +} + +function isFunction(value) { + return typeof value === 'function'; +} + +function isObject(value) { + return typeof value === 'object' || isFunction(value); +} + +function err(ctor, msg) { + var e = new ctor(msg); + Error.captureStackTrace(e, deepMapKeys); + throw e; +} + +module.exports = deepMapKeys; diff --git a/package.json b/package.json new file mode 100644 index 0000000..61f7408 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "deep-map-keys", + "version": "1.0.0", + "description": "Transforms keys of a JSON-like object", + "main": "lib/index.js", + "scripts": { + "test:lint": "eslint lib", + "test:unit": "istanbul cover _mocha", + "test": "npm run test:lint && npm run test:unit", + "coveralls": "cat coverage/lcov.info | coveralls" + }, + "engines": { + "node": ">=0.10" + }, + "keywords": [ + "map", + "keys", + "deep", + "recursive", + "nested", + "object", + "array", + "json" + ], + "author": "Akim McMath ", + "license": "MIT", + "devDependencies": { + "chai": "^3.5.0", + "coffee-script": "^1.10.0", + "coveralls": "^2.11.9", + "eslint": "^2.12.0", + "istanbul": "^0.4.3", + "mocha": "^2.5.3", + "sinon": "^1.17.4", + "sinon-chai": "^2.8.0" + }, + "dependencies": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/akim-mcmath/deep-map-keys.git" + }, + "bugs": { + "url": "https://github.com/akim-mcmath/deep-map-keys/issues" + }, + "homepage": "https://github.com/akim-mcmath/deep-map-keys#readme" +} diff --git a/test/index.coffee b/test/index.coffee new file mode 100644 index 0000000..457d2eb --- /dev/null +++ b/test/index.coffee @@ -0,0 +1,113 @@ +chai = require 'chai' +sinonChai = require 'sinon-chai' +sinon = require 'sinon' +deepMapKeys = require '../lib' + +before -> + chai.use sinonChai + chai.should() + +err = (fn, ctor) -> + fn.should.throw ctor + +describe 'deepMapKeys(object, transformFn, [options])', -> + snakeToCamel = null + camelMap = null + + beforeEach -> + snakeToCamel = sinon.spy (str) -> + str.replace /_(\w)/g, (match, char) -> + char.toUpperCase() + + camelMap = (object, options) -> + deepMapKeys object, snakeToCamel, options + + it 'is the main module', -> + deepMapKeys.should.equal require('..') + + it 'is a function', -> + deepMapKeys.should.be.a 'function' + + context 'simple object is passed', -> + + it 'transforms keys', -> + camelMap + user_id: 42 + user_name: 'Mufasa' + .should.deep.equal + userId: 42 + userName: 'Mufasa' + + context 'nested object is passed', -> + + it 'transforms keys', -> + camelMap + user_info: + user_id: 42 + user_name: 'Mufasa' + .should.deep.equal + userInfo: + userId: 42 + userName: 'Mufasa' + + context 'complex object is passed', -> + + it 'transforms keys', -> + camelMap + user_id: 42 + user_name: 'Mufasa' + viewed_by: [ + user_id: 100, user_name: 'Rafiki' + , + user_id: 101, user_name: 'Zazu' + ] + .should.deep.equal + userId: 42 + userName: 'Mufasa' + viewedBy: [ + userId: 100, userName: 'Rafiki' + , + userId: 101, userName: 'Zazu' + ] + + context 'complex array is passed', -> + + it 'transforms keys', -> + camelMap [ + user_id: 100, user_name: 'Rafiki' + , + user_id: 101, user_name: 'Zazu' + ] + .should.deep.equal [ + userId: 100, userName: 'Rafiki' + , + userId: 101, userName: 'Zazu' + ] + + context '@options.thisArg is set', -> + + it 'sets context in @transformFn', -> + camelMap({user_id: 41}, {thisArg: 42}) + snakeToCamel.should.have.been.calledOn 42 + + context 'non-object is passed as @options', -> + + it 'throws TypeError', -> + err((-> camelMap({user_id: 42}, 42)), TypeError) + + context 'non-function is passed as @transformFn', -> + + it 'throws TypeError', -> + err((-> deepMapKeys({user_id: 42}, 42)), TypeError) + + context 'undefined @transformFn', -> + + it 'throws Error', -> + err((-> deepMapKeys({user_id: 42})), Error) + + describe 'return value', -> + + it 'is a new object', -> + obj = user_id: 42 + camelMap(obj).should.not.equal obj + obj.should.deep.equal user_id: 42 diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..4779dbc --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,3 @@ +--recursive +--reporter dot +--compilers coffee:coffee-script/register