From 5d285ce2dfad60f648354969fb4f79968a74e801 Mon Sep 17 00:00:00 2001 From: Akim McMath Date: Sun, 19 Jun 2016 16:34:41 -0700 Subject: [PATCH] Add support for circular references - Add support for objects containing circular references - Update tests accordingly - Add two new dependencies - lodash - es6-weak-map - Update README accordingly --- .npmignore | 4 ++- README.md | 6 ++-- custom.d.ts | 10 ++++++ mocha.opts | 3 ++ package.json | 8 +++-- src/deep-map-keys.ts | 76 +++++++++++++++++++++++++------------------- src/index.test.ts | 12 +++++++ src/index.ts | 17 +++++++++- src/lang.ts | 13 -------- typings.json | 3 +- 10 files changed, 100 insertions(+), 52 deletions(-) create mode 100644 custom.d.ts create mode 100644 mocha.opts delete mode 100644 src/lang.ts diff --git a/.npmignore b/.npmignore index d88b493..e0e064c 100644 --- a/.npmignore +++ b/.npmignore @@ -1,9 +1,11 @@ /coverage/ /typings/ /src/ -/lib/*.test.* +/lib/**/*.test.* /.editorconfig /.gitignore /.travis.yml +/custom.d.ts +/mocha.opts /tsconfig.json /tslint.json diff --git a/README.md b/README.md index 2172301..e5448cc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ [Install](#install) | [Usage](#usage) | [API](#api) | [TypeScript](#typescript) | [License](#license) **Deep Map Keys** recurses through an object and transforms its keys – and -the keys of any nested objects – according to some function. +the keys of any nested objects – according to some function. Circular +references are supported. To transform the *values* of an object rather than its keys, use [Deep Map][deep-map]. @@ -86,7 +87,8 @@ And the result will look like this: 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. + contains will be transformed. The object may contain circular + references. diff --git a/custom.d.ts b/custom.d.ts new file mode 100644 index 0000000..d495e4c --- /dev/null +++ b/custom.d.ts @@ -0,0 +1,10 @@ +declare module 'es6-weak-map' { + + export = class WeakMap { + delete(key: K): boolean; + get(key: K): V; + has(key: K): boolean; + set(key: K, value?: V): this; + }; + +} diff --git a/mocha.opts b/mocha.opts new file mode 100644 index 0000000..66342aa --- /dev/null +++ b/mocha.opts @@ -0,0 +1,3 @@ +--compilers ts:ts-node/register +--recursive +--reporter dot diff --git a/package.json b/package.json index d2b9576..6c71a6b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "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:unit": "istanbul cover -e .ts -x '*.test.ts' _mocha -- src --opts mocha.opts", "test:report": "npm test && open coverage/lcov-report/index.html", "test": "npm run test:lint && npm run test:unit", "ci:typings": "typings install", @@ -26,6 +26,7 @@ "nested", "object", "array", + "circular", "json", "typescript", "typings" @@ -45,7 +46,10 @@ "typescript": "^1.8.10", "typings": "^1.1.0" }, - "dependencies": {}, + "dependencies": { + "es6-weak-map": "^2.0.1", + "lodash": "^4.13.1" + }, "repository": { "type": "git", "url": "git+https://github.com/akim-mcmath/deep-map-keys.git" diff --git a/src/deep-map-keys.ts b/src/deep-map-keys.ts index 73a2b6b..eec0a70 100644 --- a/src/deep-map-keys.ts +++ b/src/deep-map-keys.ts @@ -1,53 +1,65 @@ -import {isArray, isFunction, isObject, isVoid} from './lang'; +import WeakMap = require('es6-weak-map'); +import {isArray, isObject} from 'lodash'; + +interface NonPrimitive extends Object { + [key: string]: any; + [index: number]: any; +} export interface MapFn { (key: string, value: any): string; } -export interface Options { +export interface Opts { thisArg?: any; } -export function deepMapKeys(object: any, mapFn: MapFn, options?: Options): T { - options = isVoid(options) ? {} : options; +export class DeepMapKeys { + + private cache = new WeakMap(); - 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'); + constructor( + private mapFn: MapFn, + private opts: Opts + ) { } + + public map(value: any): any { + return isArray(value) ? this.mapArray(value) : + isObject(value) ? this.mapObject(value) : + value; } - return map(object, mapFn, options); -} + private mapArray(arr: any[]): any[] { + if (this.cache.has(arr)) { + return this.cache.get(arr); + } -function map(value: any, fn: MapFn, opts: Options): any { - return isArray(value) ? mapArray(value, fn, opts) : - isObject(value) ? mapObject(value, fn, opts) : - value; -} + let length = arr.length; + let result: any[] = []; + this.cache.set(arr, result); -function mapArray(arr: any[], fn: MapFn, opts: Options): any[] { - let result: any[] = []; - let len = arr.length; + for (let i = 0; i < length; i++) { + result.push(this.map(arr[i])); + } - for (let i = 0; i < len; i++) { - result.push(map(arr[i], fn, opts)); + return result; } - return result; -} + private mapObject(obj: NonPrimitive): NonPrimitive { + if (this.cache.has(obj)) { + return this.cache.get(obj); + } -function mapObject(obj: {[key: string]: any}, fn: MapFn, opts: Options): {[key: string]: any} { - let result: {[key: string]: any} = {}; + let {mapFn, opts: {thisArg}} = this; + let result: NonPrimitive = {}; + this.cache.set(obj, result); - for (let key in obj) { - if (obj.hasOwnProperty(key)) { - let value = obj[key]; - result[fn.call(opts.thisArg, key, value)] = map(value, fn, opts); + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + result[mapFn.call(thisArg, key, obj[key])] = this.map(obj[key]); + } } - } - return result; + return result; + } } diff --git a/src/index.test.ts b/src/index.test.ts index f1d45fd..d6ba49b 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -35,6 +35,18 @@ describe('deepMapKeys(object, mapFn, [options])', () => { .should.deep.equal([1, {TWO: 2, THREE: 3, ARR: [4, {FIVE: 5}]}]); }); + it('transforms an object with circular references', () => { + let obj = {one: 1, arr: [2, 3], self: null as any, arr2: null as any[]}; + obj.self = obj; + obj.arr2 = obj.arr; + + let exp = {ONE: 1, ARR: [2, 3], SELF: null as any, ARR2: null as any[]}; + exp.SELF = exp; + exp.ARR2 = exp.ARR; + + deepMapKeys(obj, caps).should.deep.equal(exp); + }); + }); describe('@mapFn(key: string, value: any): string', () => { diff --git a/src/index.ts b/src/index.ts index ef772ce..7faab76 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,18 @@ -import {deepMapKeys} from './deep-map-keys'; +import {isFunction, isNil, isObject} from 'lodash'; +import {DeepMapKeys, MapFn, Opts} from './deep-map-keys'; + +function deepMapKeys(object: any, mapFn: MapFn, options?: Opts): T { + options = isNil(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 new DeepMapKeys(mapFn, options).map(object); +} export = deepMapKeys; diff --git a/src/lang.ts b/src/lang.ts deleted file mode 100644 index 07cca64..0000000 --- a/src/lang.ts +++ /dev/null @@ -1,13 +0,0 @@ -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/typings.json b/typings.json index daf3791..c02e0dc 100644 --- a/typings.json +++ b/typings.json @@ -6,6 +6,7 @@ "devDependencies": { "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" + "sinon-chai": "registry:npm/sinon-chai#2.8.0+20160310030142", + "lodash": "registry:npm/lodash#4.0.0+20160416211519" } }