From 154c9fbc0aa1fb56307eba9fd1d5b0302ac55673 Mon Sep 17 00:00:00 2001 From: Akim McMath Date: Wed, 15 Jun 2016 03:56:47 -0700 Subject: [PATCH] Refactor with TypeScript --- .eslintrc.yml | 5 -- .gitignore | 2 + .npmignore | 7 ++- .travis.yml | 8 ++- README.md | 107 ++++++++++++++++++++++++++-------------- lib/index.js | 64 ++---------------------- package.json | 29 +++++++---- src/deep-map-keys.ts | 51 +++++++++++++++++++ src/index.test.ts | 92 +++++++++++++++++++++++++++++++++++ src/index.ts | 3 ++ src/lang.ts | 13 +++++ test/index.coffee | 113 ------------------------------------------- test/mocha.opts | 3 -- tsconfig.json | 16 ++++++ tslint.json | 92 +++++++++++++++++++++++++++++++++++ typings.json | 10 ++++ 16 files changed, 385 insertions(+), 230 deletions(-) delete mode 100644 .eslintrc.yml create mode 100644 src/deep-map-keys.ts create mode 100644 src/index.test.ts create mode 100644 src/index.ts create mode 100644 src/lang.ts delete mode 100644 test/index.coffee delete mode 100644 test/mocha.opts create mode 100644 tsconfig.json create mode 100644 tslint.json create mode 100644 typings.json diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 31e8802..0000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,5 +0,0 @@ -extends: eslint:recommended -parserOptions: - ecmaVersion: 5 -env: - node: true diff --git a/.gitignore b/.gitignore index 3f1b497..e9ba244 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /coverage/ +/lib/ /node_modules/ +/typings/ /npm-debug.log diff --git a/.npmignore b/.npmignore index 75dc28a..d88b493 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,9 @@ /coverage/ -/test/ +/typings/ +/src/ +/lib/*.test.* /.editorconfig -/.eslintrc.yml /.gitignore /.travis.yml +/tsconfig.json +/tslint.json diff --git a/.travis.yml b/.travis.yml index 82d92f2..03ef91c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,12 @@ node_js: - "4" - "0.12" - "0.10" +before_script: + - npm run ci:typings after_success: - - npm run coveralls + - npm run ci:coveralls +before_deploy: + - npm run build deploy: provider: npm email: @@ -17,3 +21,5 @@ deploy: repo: akim-mcmath/deep-map-keys node: "4" tags: true +after_deploy: + - npm run build:remove diff --git a/README.md b/README.md index 58865e5..464780e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# deep-map-keys +# Deep Map Keys [![Version][version-badge]][npm] [![License][license-badge]][license] @@ -6,11 +6,12 @@ [![Coverage][coverage-badge]][coveralls] [![Dependencies][dependencies-badge]][gemnasium] -Recurses through a JSON-like object and transforms its keys, returning a new object. +**Deep Map Keys** recurses through an object and transforms its keys – and +the keys of any nested objects – according to some function. ## Install -Install via [npm][npm]. +Install Deep Map Keys via [npm][npm]. ```sh npm install --save deep-map-keys @@ -38,8 +39,11 @@ let comment = { let result = deepMapKeys(comment, key => { return key.replace(/_(\w)/g, (match, char) => char.toUpperCase()); }); +``` + +And the result will look like this: -console.log(result); /* +```js { commentId: 42, userId: 1024, @@ -49,46 +53,75 @@ console.log(result); /* { 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`. +#### `deepMapKeys(object, mapFn, [options])` + +#### Parameters + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParamTypeDescription
objectany + The object whose keys are to be transformed. Typically, this will be + a complex object containing other nested objects. This object may be an + + Array, in which case the keys of any objects it + contains will be transformed. +
mapFnfunction + The function used to transform each key. The function is + called with two arguments: +
    +
  • + key <string> + The key being transformed +
  • +
  • + value <any> + The value of the node whose key is being transformed +
  • +
+ The return value determines the new name of the key, and must therefore + be a string. +
[options]object + An optional options object. The following option is accepted: +
    +
  • + thisArg <any = undefined> + Sets the value of + this + within mapFn() +
  • +
+
#### Returns -`object` - Returns a new object. ## License diff --git a/lib/index.js b/lib/index.js index 9a4e014..5bdb3e0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,60 +1,4 @@ -'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; +"use strict"; +var deep_map_keys_1 = require('./deep-map-keys'); +module.exports = deep_map_keys_1.deepMapKeys; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/package.json b/package.json index 61f7408..bb1de27 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,19 @@ { "name": "deep-map-keys", "version": "1.0.0", - "description": "Transforms keys of a JSON-like object", + "description": "Transforms nested keys of complex objects", "main": "lib/index.js", + "typings": "lib/index.d.ts", "scripts": { - "test:lint": "eslint lib", - "test:unit": "istanbul cover _mocha", + "build:compile": "tsc", + "build:remove": "rimraf lib", + "build": "npm run build:remove && npm run build:compile", + "test:lint": "tslint 'src/**/*.ts'", + "test:unit": "istanbul cover -e .ts -x '*.test.ts' _mocha -- 'src/**/*.test.ts' --compilers ts:ts-node/register", + "test:report": "npm test && open coverage/lcov-report/index.html", "test": "npm run test:lint && npm run test:unit", - "coveralls": "cat coverage/lcov.info | coveralls" + "ci:typings": "typings install", + "ci:coveralls": "cat coverage/lcov.info | coveralls" }, "engines": { "node": ">=0.10" @@ -20,19 +26,24 @@ "nested", "object", "array", - "json" + "json", + "typescript", + "typings" ], "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", + "istanbul": "1.0.0-alpha.2", "mocha": "^2.5.3", + "rimraf": "^2.5.2", "sinon": "^1.17.4", - "sinon-chai": "^2.8.0" + "sinon-chai": "^2.8.0", + "ts-node": "^0.9.1", + "tslint": "^3.11.0", + "typescript": "^1.8.10", + "typings": "^1.1.0" }, "dependencies": {}, "repository": { diff --git a/src/deep-map-keys.ts b/src/deep-map-keys.ts new file mode 100644 index 0000000..b0f6d93 --- /dev/null +++ b/src/deep-map-keys.ts @@ -0,0 +1,51 @@ +import {isArray, isFunction, isObject, isVoid} from './lang'; + +export interface MapFn { + (key: string, value: any): string; +} + +export interface Options { + thisArg?: any; +} + +export function deepMapKeys(object: any, mapFn: MapFn, options?: Options): T { + options = isVoid(options) ? {} : options; + + if (!mapFn) { + throw new Error('mapFn is required'); + } else if (!isFunction(mapFn)) { + throw new TypeError('mapFn must be a function'); + } else if (!isObject(options)) { + throw new TypeError('options must be an object'); + } + + return map(object, mapFn, options); +} + +function map(value: any, fn: MapFn, opts: Options): any { + return isArray(value) ? mapArray(value, fn, opts) : + isObject(value) ? mapObject(value, fn, opts) : + value; +} + +function mapArray(arr: any[], fn: MapFn, opts: Options): any[] { + let result: any[] = []; + let len = arr.length; + + for (let i = 0; i < len; i++) { + result.push(map(arr[i], fn, opts)); + } + + return result; +} + +function mapObject(obj: {[key: string]: any}, fn: MapFn, opts: Options): {[key: string]: any} { + let result: {[key: string]: any} = {}; + + for (let key in obj) { + let value = obj[key]; + result[fn.call(opts.thisArg, key, value)] = map(value, fn, opts); + } + + return result; +} diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..f1d45fd --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,92 @@ +import chai = require('chai'); +import sinonChai = require('sinon-chai'); +import sinon = require('sinon'); +import deepMapKeys = require('./'); + +before(() => { + chai.use(sinonChai); + chai.should(); +}); + +describe('deepMapKeys(object, mapFn, [options])', () => { + let caps: (key: string) => string; + + beforeEach(() => { + caps = sinon.spy((key: string) => key.toUpperCase()); + }); + + it('exports a function', () => { + deepMapKeys.should.be.a('function'); + }); + + describe('@object: any', () => { + + it('transforms keys of simple object', () => { + deepMapKeys({one: 1, two: 2}, caps).should.deep.equal({ONE: 1, TWO: 2}); + }); + + it('transforms keys of object with nested objects/arrays', () => { + deepMapKeys({one: 1, obj: {two: 2, three: 3}, arr: [4, 5]}, caps) + .should.deep.equal({ONE: 1, OBJ: {TWO: 2, THREE: 3}, ARR: [4, 5]}); + }); + + it('transforms keys of array with nested object/array', () => { + deepMapKeys([1, {two: 2, three: 3, arr: [4, {five: 5}]}], caps) + .should.deep.equal([1, {TWO: 2, THREE: 3, ARR: [4, {FIVE: 5}]}]); + }); + + }); + + describe('@mapFn(key: string, value: any): string', () => { + + it('throws Error if undefined', () => { + deepMapKeys.bind(null, {one: 1}).should.throw(Error); + }); + + it('throws TypeError if not a function', () => { + deepMapKeys.bind(null, {one: 1}, 42).should.throw(TypeError); + }); + + it('is called once per object property', () => { + deepMapKeys({one: 1, obj: {two: 2, three: 3}, arr: [4, 5]}, caps); + caps.should.have.callCount(5); + }); + + it('is called with @key as first argument', () => { + deepMapKeys({one: 1, arr: [2, 3]}, caps); + caps.should.have.been.calledWith('one'); + caps.should.have.been.calledWith('arr'); + }); + + it('is called with @value as second argument', () => { + let {any} = sinon.match; + deepMapKeys({one: 1, arr: [2, 3]}, caps); + caps.should.have.been.calledWith(any, 1); + caps.should.have.been.calledWithMatch(any, [2, 3]); + }); + + }); + + describe('@options?', () => { + + it('throws TypeError if defined but not an object', () => { + deepMapKeys.bind(null, {one: 1}, caps, 42).should.throw(TypeError); + }); + + describe('option: thisArg', () => { + + it('sets context within @mapFn', () => { + deepMapKeys({one: 1, arr: [2, 3]}, caps, {thisArg: 42}); + caps.should.have.been.calledOn(42); + }); + + it('defaults to undefined', () => { + deepMapKeys({one: 1, arr: [2, 3]}, caps); + caps.should.have.been.calledOn(undefined); + }); + + }); + + }); + +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ef772ce --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +import {deepMapKeys} from './deep-map-keys'; + +export = deepMapKeys; diff --git a/src/lang.ts b/src/lang.ts new file mode 100644 index 0000000..07cca64 --- /dev/null +++ b/src/lang.ts @@ -0,0 +1,13 @@ +export const isArray = Array.isArray; + +export function isFunction(value: any): boolean { + return typeof value === 'function'; +} + +export function isObject(value: any): boolean { + return !isVoid(value) && (typeof value === 'object' || isFunction(value)); +} + +export function isVoid(value: any): boolean { + return value == null; +} diff --git a/test/index.coffee b/test/index.coffee deleted file mode 100644 index 457d2eb..0000000 --- a/test/index.coffee +++ /dev/null @@ -1,113 +0,0 @@ -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 deleted file mode 100644 index 4779dbc..0000000 --- a/test/mocha.opts +++ /dev/null @@ -1,3 +0,0 @@ ---recursive ---reporter dot ---compilers coffee:coffee-script/register diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b099673 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "declaration": true, + "sourceMap": true, + "noImplicitAny": true, + "rootDir": "src", + "outDir": "lib" + }, + "compileOnSave": false, + "exclude": [ + "node_modules", + "lib" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..a01a39f --- /dev/null +++ b/tslint.json @@ -0,0 +1,92 @@ +{ + "rules": { + "member-access": false, + "member-ordering": [true, {"order": "statics-first"}], + "no-any": false, + "no-inferrable-types": [true, "ignore-params"], + "no-namespace": [true, "allow-declarations"], + "no-reference": true, + "no-var-requires": true, + "typedef": [true, "call-signature"], + "typedef-whitespace": [true, { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + },{ + "call-signature": "space", + "index-signature": "space", + "parameter": "space", + "property-declaration": "space", + "variable-declaration": "space" + }], + "ban": false, + "curly": false, + "forin": false, + "label-position": true, + "no-arg": true, + "no-bitwise": false, + "no-conditional-assignment": false, + "no-console": false, + "no-debugger": false, + "no-duplicate-variable": true, + "no-empty": false, + "no-eval": true, + "no-invalid-this": false, + "no-null-keyword": false, + "no-shadowed-variable": false, + "no-string-literal": false, + "no-switch-case-fall-through": false, + "no-unreachable": true, + "no-unused-expression": false, + "no-unused-variable": [true, "react"], + "no-use-before-declare": false, + "no-var-keyword": true, + "radix": true, + "switch-default": false, + "triple-equals": false, + "use-isnan": true, + "use-strict": false, + "eofline": true, + "indent": [true, "spaces"], + "max-line-length": [true, 120], + "no-default-export": true, + "no-require-imports": false, + "no-trailing-whitespace": true, + "object-literal-sort-keys": false, + "trailing-comma": [true, { + "multiline": "never", + "singleline": "never" + }], + "align": [false], + "class-name": true, + "comment-format": [true, "check-space"], + "interface-name": [true, "never-prefix"], + "jsdoc-format": true, + "new parens": true, + "no-angle-bracket-type-assertion": true, + "no-consecutive-blank-lines": true, + "no-constructor-vars": false, + "one-line": [true, + "check-catch", + "check-finally", + "check-else", + "check-open-brace", + "check-whitespace" + ], + "one-variable-per-declaration": [true, "ignore-for-loop"], + "quotemark": [true, "single", "avoidEscape"], + "semicolon": [true, "always"], + "variable-name": [true, "check-format", "allow-leading-underscore"], + "whitespace": [true, + "check-branch", + "check-decl", + "check-operator", + "check-module", + "check-separator", + "check-type", + "check-typecast" + ] + } +} diff --git a/typings.json b/typings.json new file mode 100644 index 0000000..a93bd6f --- /dev/null +++ b/typings.json @@ -0,0 +1,10 @@ +{ + "globalDependencies": { + "mocha": "registry:env/mocha#2.2.5+20160321223601" + }, + "dependencies": { + "chai": "registry:npm/chai#3.5.0+20160415060238", + "sinon": "registry:npm/sinon#1.16.0+20160427193336", + "sinon-chai": "registry:npm/sinon-chai#2.8.0+20160310030142" + } +}