Skip to content

Commit 5882a58

Browse files
authored
feat!: Add transformer/mapValueTransformer options (#6)
1 parent d5b6468 commit 5882a58

File tree

13 files changed

+337
-113
lines changed

13 files changed

+337
-113
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ module.exports = {
99
'jest/prefer-expect-assertions': 'off',
1010
'jest/prefer-lowercase-title': 'off',
1111
'jest/max-expects': ['error', { max: 8 }],
12+
'jest/unbound-method': ['error', { ignoreStatic: true }],
13+
'@typescript-eslint/unbound-method': 'off', // disable in favor of jest/unbound-method
1214
},
1315
},
1416
],

README.md

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ npm install deep-equality-data-structures
1515
ES `Map` and `Set` only support referential equality:
1616

1717
```typescript
18-
interface MyObject {
18+
interface MyType {
1919
a: number;
2020
}
21-
const set = new Set<MyObject>();
21+
const set = new Set<MyType>();
2222
set.add({ a: 1 });
2323
set.add({ a: 1 });
2424
set.size; // 2
@@ -29,18 +29,18 @@ Now, using deep equality:
2929
```typescript
3030
import { DeepSet } from 'deep-equality-data-structures';
3131

32-
interface MyObject {
32+
interface MyType {
3333
a: number;
3434
}
35-
const set = new DeepSet<MyObject>();
35+
const set = new DeepSet<MyType>();
3636
set.add({ a: 1 });
3737
set.add({ a: 1 });
3838
set.size; // 1
3939
```
4040

4141
## How?
4242

43-
This project relies on the [object-hash](https://github.com/puleos/object-hash) library to map object types to strings.
43+
This project relies on the [object-hash](https://github.com/puleos/object-hash) library to normalize object types to unique strings.
4444

4545
## Comparable Interface
4646

@@ -58,16 +58,55 @@ set1.contains(set3); // true
5858

5959
## Configuration Options
6060

61+
The default settings should be suitable for most use cases, but behavior can be configured.
62+
6163
```typescript
62-
new DeepSet(values?, options?)
63-
new DeepMap(entries?, options?)
64+
new DeepSet<K>(values?, options?)
65+
new DeepMap<K,V>(entries?, options?)
6466
```
6567
66-
The `options` argument is a superset of the options defined for [object-hash](https://github.com/puleos/object-hash#hashvalue-options), with the same defaults (exception: the default algoritm is `md5`).
68+
The `options` argument is a superset of the options defined for [object-hash](https://github.com/puleos/object-hash#hashvalue-options), with the same defaults (exception: the default algoritm is `md5`). There are also library-specific options.
69+
70+
### Library-specific options:
71+
72+
- `transformer` - a custom function that transforms Map keys/Set values prior to hashing. It does not affect the values that are stored.
73+
74+
```typescript
75+
type MyType = { val: number; other: number };
76+
const a: MyType = { val: 1, other: 1 };
77+
const b: MyType = { val: 1, other: 2 };
78+
const transformer = (obj: MyType) => ({ val });
79+
80+
const set = new DeepSet([a, b]);
81+
set.size; // 2
82+
const set = new DeepSet([a, b], { transformer });
83+
set.size; // 1
84+
85+
[...set.values()]; // [{ val: 1, other: 2 }]
86+
```
87+
88+
- `mapValueTransformer` - a custom function that transforms Map values prior to hashing. This is only relevant to `Comparable` interface operations. It does not affect the values that are stored.
89+
90+
```typescript
91+
type MyType = { val: number; other: number };
92+
const a: MyType = { val: 1, other: 1 };
93+
const b: MyType = { val: 1, other: 2 };
94+
const mapValueTransformer = (obj: MyType) => ({ val });
95+
96+
const map1 = new DeepMap([[1, a]]);
97+
const map2 = new DeepMap([[1, b]]);
98+
map1.equals(map2); // false
99+
100+
const map1 = new DeepMap([[1, a]], { mapValueTransformer });
101+
const map2 = new DeepMap([[1, b]], { mapValueTransformer });
102+
map1.equals(map2); // true
103+
104+
[...map1.entries()]; // [[1, { val: 1, other: 2 }]]
105+
```
67106
68-
Additional project-specific options:
107+
- `useToJsonTransform` - if true, only use JSON-serializable properties when computing hashes, equality, etc. (default: false)
69108
70-
- `jsonSerializableOnly` - if true, only use JSON-serializable properties when computing hashes, equality, etc. (default: false)
109+
> _NOTE: This setting overrides both `transformer` and `mapValueTransformer`_
71110
72111
```typescript
73112
class A {
@@ -81,13 +120,15 @@ Additional project-specific options:
81120

82121
const set = new DeepSet([a, b]);
83122
set.size; // 2
84-
const set = new DeepSet([a, b], { jsonSerializableOnly: true });
123+
const set = new DeepSet([a, b], { useToJsonTransform: true });
85124
set.size; // 1
86125
```
87126
88127
## Notes/Caveats
89128
90-
- Don't mutate a map key (or set value) while still using the data structure. The internal representation is not affected by this mutation, so behavior may be unexpected.
129+
- This still supports primitive keys/values like traditional `Map`/`Set`.
130+
- Don't mutate objects stored in the data structure. The internal representation is not affected by this mutation, so behavior may be unexpected.
131+
- Don't mutate objects in the user-supplied `transformer` or `mapValueTransformer` functions. It will affect the stored version.
91132
- This implementation does not explicitly "handle" key collisions. However, with the default algorithm (MD5), even if a map contained one TRILLION entries, the probability of a collision on the next insert is only 0.000000000000001. If you need better odds, use SHA1, SHA256, etc.
92133
93134
## CI/CD

src/__tests__/hash.test.ts

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

src/__tests__/normalizer.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import hash from 'object-hash';
2+
3+
import { Normalizer } from '../normalizer';
4+
5+
jest.mock('object-hash', () => {
6+
const origModule = jest.requireActual('object-hash');
7+
return {
8+
__esModule: true,
9+
...origModule,
10+
default: jest.fn((...args: unknown[]) => origModule(...args)),
11+
};
12+
});
13+
14+
describe('../normalizer.ts', () => {
15+
const n = new Normalizer();
16+
17+
describe('normalize', () => {
18+
it('object-hash sanity checks', async () => {
19+
// Positive
20+
expect(n.normalizeKey({})).toBe(n.normalizeKey({}));
21+
expect(n.normalizeKey({ a: 'hi', b: 'bye' })).toBe(n.normalizeKey({ b: 'bye', a: 'hi' }));
22+
expect(n.normalizeKey(['blah'])).toBe(n.normalizeKey(['blah']));
23+
// Negative
24+
expect(n.normalizeKey({ a: 'hi', b: 'bye' })).not.toBe(n.normalizeKey({ a: 'hi' }));
25+
expect(n.normalizeKey({ a: 'hi', b: 'bye' })).not.toBe(n.normalizeKey({ a: 'hi', b: 'bye bye' }));
26+
expect(n.normalizeKey(['bleep', 'bloop'])).not.toBe(n.normalizeKey(['bloop', 'bleep']));
27+
});
28+
29+
it('primitive inputs are normalized to themselves', async () => {
30+
expect(n.normalizeKey(5)).toBe(5);
31+
expect(n.normalizeKey('hi')).toBe('hi');
32+
expect(n.normalizeKey(true)).toBe(true);
33+
expect(n.normalizeKey(null)).toBeNull();
34+
expect(n.normalizeKey(undefined)).toBeUndefined();
35+
});
36+
37+
describe('Configurable options', () => {
38+
describe('options.algorithm', () => {
39+
it('Uses MD5 as default algorithm', async () => {
40+
n.normalizeKey({});
41+
expect(hash).toHaveBeenCalledWith({}, { algorithm: 'md5' });
42+
});
43+
44+
it('Uses specified algorithm', async () => {
45+
const n = new Normalizer({ algorithm: 'sha1' });
46+
n.normalizeKey({});
47+
expect(hash).toHaveBeenCalledWith({}, { algorithm: 'sha1' });
48+
});
49+
});
50+
51+
describe('options.transformer', () => {
52+
it('Can define a transformer function', () => {
53+
type Blah = { val: number };
54+
const a: Blah = { val: 1 };
55+
const b: Blah = { val: 3 };
56+
expect(n.normalizeKey(a)).not.toBe(n.normalizeKey(b));
57+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
58+
const transformer = (obj: Blah) => {
59+
return { val: obj.val % 2 };
60+
};
61+
const withTransformer = new Normalizer({ transformer });
62+
expect(withTransformer.normalizeKey(a)).toBe(withTransformer.normalizeKey(b));
63+
});
64+
});
65+
66+
describe('options.mapValueTransformer', () => {
67+
it('Can define a mapValueTransformer function', () => {
68+
type Blah = { val: number };
69+
const a: Blah = { val: 1 };
70+
const b: Blah = { val: 3 };
71+
expect(n.normalizeValue(a)).not.toBe(n.normalizeValue(b));
72+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
73+
const mapValueTransformer = (obj: Blah) => {
74+
return { val: obj.val % 2 };
75+
};
76+
const withTransformer = new Normalizer({ mapValueTransformer });
77+
expect(withTransformer.normalizeValue(a)).toBe(withTransformer.normalizeValue(b));
78+
});
79+
});
80+
81+
describe('options.useToJsonTransform', () => {
82+
it('Can specify useToJsonTransform setting', () => {
83+
class A {
84+
constructor(public x: number) {}
85+
}
86+
class B {
87+
constructor(public x: number) {}
88+
}
89+
const a = new A(45);
90+
const b = new B(45);
91+
expect(n.normalizeKey(a)).not.toBe(n.normalizeKey(b));
92+
const withToJson = new Normalizer({ useToJsonTransform: true });
93+
expect(withToJson.normalizeKey(a)).toBe(withToJson.normalizeKey(b));
94+
expect(withToJson.normalizeValue(a)).toBe(withToJson.normalizeValue(b));
95+
});
96+
});
97+
});
98+
});
99+
});

src/__tests__/options.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { getOptionsWithDefaults } from '../options';
2+
import { Transformers } from '../transformers';
3+
4+
describe('../options.ts', () => {
5+
describe('getOptionsWithDefaults', () => {
6+
it('baseline/default values', async () => {
7+
expect(getOptionsWithDefaults({})).toStrictEqual({
8+
algorithm: 'md5',
9+
transformer: Transformers.identity,
10+
mapValueTransformer: Transformers.identity,
11+
useToJsonTransform: false,
12+
});
13+
});
14+
15+
it('specified values override default values', async () => {
16+
expect(
17+
getOptionsWithDefaults({
18+
algorithm: 'sha1',
19+
useToJsonTransform: true,
20+
})
21+
).toStrictEqual({
22+
algorithm: 'sha1',
23+
transformer: Transformers.identity,
24+
mapValueTransformer: Transformers.identity,
25+
useToJsonTransform: true,
26+
});
27+
});
28+
});
29+
});

src/__tests__/set.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ describe('DeepSet', () => {
193193
});
194194
});
195195

196-
describe('Hash Options', () => {
197-
describe('jsonSerializableOnly', () => {
196+
describe('Normalizer Options', () => {
197+
describe('useToJsonTransform', () => {
198198
class A {
199199
constructor(public a: number) {}
200200
}
@@ -204,14 +204,16 @@ describe('DeepSet', () => {
204204
const b = new B(45);
205205
const c = new C(45);
206206

207-
it('jsonSerializableOnly=false', async () => {
207+
it('useToJsonTransform=false', async () => {
208208
const set = new DeepSet([b, c]);
209209
expect(set.size).toBe(2);
210210
});
211211

212-
it('jsonSerializableOnly=true', async () => {
213-
const set = new DeepSet([b, c], { jsonSerializableOnly: true });
212+
it('useToJsonTransform=true', async () => {
213+
const set = new DeepSet([b, c], { useToJsonTransform: true });
214214
expect(set.size).toBe(1);
215+
// Last one in wins
216+
expect([...set.values()]).toStrictEqual([c]);
215217
});
216218
});
217219
});

src/hash.ts

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

0 commit comments

Comments
 (0)