Skip to content

Commit 306a4fc

Browse files
committed
Merge branch 'feature/circular-refs'
Merges PR #4
2 parents 828a080 + 5d285ce commit 306a4fc

10 files changed

+100
-52
lines changed

.npmignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/coverage/
22
/typings/
33
/src/
4-
/lib/*.test.*
4+
/lib/**/*.test.*
55
/.editorconfig
66
/.gitignore
77
/.travis.yml
8+
/custom.d.ts
9+
/mocha.opts
810
/tsconfig.json
911
/tslint.json

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
[Install](#install) | [Usage](#usage) | [API](#api) | [TypeScript](#typescript) | [License](#license)
1010

1111
**Deep Map Keys** recurses through an object and transforms its keys – and
12-
the keys of any nested objects – according to some function.
12+
the keys of any nested objects – according to some function. Circular
13+
references are supported.
1314

1415
To transform the *values* of an object rather than its keys, use
1516
[Deep Map][deep-map].
@@ -86,7 +87,8 @@ And the result will look like this:
8687
a complex object containing other nested objects. This object may be an
8788
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array">
8889
<code>Array</code></a>, in which case the keys of any objects it
89-
contains will be transformed.
90+
contains will be transformed. The object may contain circular
91+
references.
9092
</td>
9193
</tr>
9294
<tr>

custom.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
declare module 'es6-weak-map' {
2+
3+
export = class WeakMap<K, V> {
4+
delete(key: K): boolean;
5+
get(key: K): V;
6+
has(key: K): boolean;
7+
set(key: K, value?: V): this;
8+
};
9+
10+
}

mocha.opts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
--compilers ts:ts-node/register
2+
--recursive
3+
--reporter dot

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"build:remove": "rimraf lib",
1010
"build": "npm run build:remove && npm run build:compile",
1111
"test:lint": "tslint 'src/**/*.ts'",
12-
"test:unit": "istanbul cover -e .ts -x '*.test.ts' _mocha -- 'src/**/*.test.ts' --compilers ts:ts-node/register",
12+
"test:unit": "istanbul cover -e .ts -x '*.test.ts' _mocha -- src --opts mocha.opts",
1313
"test:report": "npm test && open coverage/lcov-report/index.html",
1414
"test": "npm run test:lint && npm run test:unit",
1515
"ci:typings": "typings install",
@@ -26,6 +26,7 @@
2626
"nested",
2727
"object",
2828
"array",
29+
"circular",
2930
"json",
3031
"typescript",
3132
"typings"
@@ -45,7 +46,10 @@
4546
"typescript": "^1.8.10",
4647
"typings": "^1.1.0"
4748
},
48-
"dependencies": {},
49+
"dependencies": {
50+
"es6-weak-map": "^2.0.1",
51+
"lodash": "^4.13.1"
52+
},
4953
"repository": {
5054
"type": "git",
5155
"url": "git+https://github.com/akim-mcmath/deep-map-keys.git"

src/deep-map-keys.ts

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,65 @@
1-
import {isArray, isFunction, isObject, isVoid} from './lang';
1+
import WeakMap = require('es6-weak-map');
2+
import {isArray, isObject} from 'lodash';
3+
4+
interface NonPrimitive extends Object {
5+
[key: string]: any;
6+
[index: number]: any;
7+
}
28

39
export interface MapFn {
410
(key: string, value: any): string;
511
}
612

7-
export interface Options {
13+
export interface Opts {
814
thisArg?: any;
915
}
1016

11-
export function deepMapKeys<T>(object: any, mapFn: MapFn, options?: Options): T {
12-
options = isVoid(options) ? {} : options;
17+
export class DeepMapKeys {
18+
19+
private cache = new WeakMap<NonPrimitive, any>();
1320

14-
if (!mapFn) {
15-
throw new Error('mapFn is required');
16-
} else if (!isFunction(mapFn)) {
17-
throw new TypeError('mapFn must be a function');
18-
} else if (!isObject(options)) {
19-
throw new TypeError('options must be an object');
21+
constructor(
22+
private mapFn: MapFn,
23+
private opts: Opts
24+
) { }
25+
26+
public map(value: any): any {
27+
return isArray(value) ? this.mapArray(value) :
28+
isObject(value) ? this.mapObject(value) :
29+
value;
2030
}
2131

22-
return map(object, mapFn, options);
23-
}
32+
private mapArray(arr: any[]): any[] {
33+
if (this.cache.has(arr)) {
34+
return this.cache.get(arr);
35+
}
2436

25-
function map(value: any, fn: MapFn, opts: Options): any {
26-
return isArray(value) ? mapArray(value, fn, opts) :
27-
isObject(value) ? mapObject(value, fn, opts) :
28-
value;
29-
}
37+
let length = arr.length;
38+
let result: any[] = [];
39+
this.cache.set(arr, result);
3040

31-
function mapArray(arr: any[], fn: MapFn, opts: Options): any[] {
32-
let result: any[] = [];
33-
let len = arr.length;
41+
for (let i = 0; i < length; i++) {
42+
result.push(this.map(arr[i]));
43+
}
3444

35-
for (let i = 0; i < len; i++) {
36-
result.push(map(arr[i], fn, opts));
45+
return result;
3746
}
3847

39-
return result;
40-
}
48+
private mapObject(obj: NonPrimitive): NonPrimitive {
49+
if (this.cache.has(obj)) {
50+
return this.cache.get(obj);
51+
}
4152

42-
function mapObject(obj: {[key: string]: any}, fn: MapFn, opts: Options): {[key: string]: any} {
43-
let result: {[key: string]: any} = {};
53+
let {mapFn, opts: {thisArg}} = this;
54+
let result: NonPrimitive = {};
55+
this.cache.set(obj, result);
4456

45-
for (let key in obj) {
46-
if (obj.hasOwnProperty(key)) {
47-
let value = obj[key];
48-
result[fn.call(opts.thisArg, key, value)] = map(value, fn, opts);
57+
for (let key in obj) {
58+
if (obj.hasOwnProperty(key)) {
59+
result[mapFn.call(thisArg, key, obj[key])] = this.map(obj[key]);
60+
}
4961
}
50-
}
5162

52-
return result;
63+
return result;
64+
}
5365
}

src/index.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ describe('deepMapKeys(object, mapFn, [options])', () => {
3535
.should.deep.equal([1, {TWO: 2, THREE: 3, ARR: [4, {FIVE: 5}]}]);
3636
});
3737

38+
it('transforms an object with circular references', () => {
39+
let obj = {one: 1, arr: [2, 3], self: null as any, arr2: null as any[]};
40+
obj.self = obj;
41+
obj.arr2 = obj.arr;
42+
43+
let exp = {ONE: 1, ARR: [2, 3], SELF: null as any, ARR2: null as any[]};
44+
exp.SELF = exp;
45+
exp.ARR2 = exp.ARR;
46+
47+
deepMapKeys(obj, caps).should.deep.equal(exp);
48+
});
49+
3850
});
3951

4052
describe('@mapFn(key: string, value: any): string', () => {

src/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1-
import {deepMapKeys} from './deep-map-keys';
1+
import {isFunction, isNil, isObject} from 'lodash';
2+
import {DeepMapKeys, MapFn, Opts} from './deep-map-keys';
3+
4+
function deepMapKeys<T>(object: any, mapFn: MapFn, options?: Opts): T {
5+
options = isNil(options) ? {} : options;
6+
7+
if (!mapFn) {
8+
throw new Error('mapFn is required');
9+
} else if (!isFunction(mapFn)) {
10+
throw new TypeError('mapFn must be a function');
11+
} else if (!isObject(options)) {
12+
throw new TypeError('options must be an object');
13+
}
14+
15+
return new DeepMapKeys(mapFn, options).map(object);
16+
}
217

318
export = deepMapKeys;

src/lang.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

typings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"devDependencies": {
77
"chai": "registry:npm/chai#3.5.0+20160415060238",
88
"sinon": "registry:npm/sinon#1.16.0+20160427193336",
9-
"sinon-chai": "registry:npm/sinon-chai#2.8.0+20160310030142"
9+
"sinon-chai": "registry:npm/sinon-chai#2.8.0+20160310030142",
10+
"lodash": "registry:npm/lodash#4.0.0+20160416211519"
1011
}
1112
}

0 commit comments

Comments
 (0)