Skip to content

Commit e1a4c5d

Browse files
committed
support custom types
1 parent 8b5d363 commit e1a4c5d

File tree

4 files changed

+94
-8
lines changed

4 files changed

+94
-8
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,37 @@ const json = `{
7878
const data = devalue.unflatten(JSON.parse(json).data);
7979
```
8080

81+
## Custom types
82+
83+
You can serialize and serialize custom types by passing a second argument to `stringify` containing an object of types and their _reducers_, and a second argument to `parse` or `unflatten` containing an object of types and their _revivers_:
84+
85+
```js
86+
class Vector {
87+
constructor(x, y) {
88+
this.x = x;
89+
this.y = y;
90+
}
91+
92+
magnitude() {
93+
return Math.sqrt(this.x * this.x + this.y * this.y);
94+
}
95+
}
96+
97+
const stringified = devalue.stringify(new Vector(30, 40), {
98+
Vector: (value) => value instanceof Vector && [value.x, value.y]
99+
});
100+
101+
console.log(stringified); // [["Vector",1],[2,3],30,40]
102+
103+
const vector = devalue.parse(stringified, {
104+
Vector: ([x, y]) => new Vector(x, y)
105+
});
106+
107+
console.log(vector.magnitude()); // 50
108+
```
109+
110+
If a function passed to `stringify` returns a truthy value, it's treated as a match.
111+
81112
## Error handling
82113

83114
If `uneval` or `stringify` encounters a function or a non-POJO, it will throw an error. You can find where in the input data the offending value lives by inspecting `error.path`:

src/parse.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ import {
1010
/**
1111
* Revive a value serialized with `devalue.stringify`
1212
* @param {string} serialized
13+
* @param {Record<string, (value: any) => any>} revivers
1314
*/
14-
export function parse(serialized) {
15-
return unflatten(JSON.parse(serialized));
15+
export function parse(serialized, revivers) {
16+
return unflatten(JSON.parse(serialized), revivers);
1617
}
1718

1819
/**
19-
* Revive a value flattened with `devalue.flatten`
20+
* Revive a value flattened with `devalue.stringify`
2021
* @param {number | any[]} parsed
22+
* @param {Record<string, (value: any) => any>} revivers
2123
*/
22-
export function unflatten(parsed) {
24+
export function unflatten(parsed, revivers) {
2325
if (typeof parsed === 'number') return hydrate(parsed, true);
2426

2527
if (!Array.isArray(parsed) || parsed.length === 0) {
@@ -30,7 +32,10 @@ export function unflatten(parsed) {
3032

3133
const hydrated = Array(values.length);
3234

33-
/** @param {number} index */
35+
/**
36+
* @param {number} index
37+
* @returns {any}
38+
*/
3439
function hydrate(index, standalone = false) {
3540
if (index === UNDEFINED) return undefined;
3641
if (index === NAN) return NaN;
@@ -50,6 +55,11 @@ export function unflatten(parsed) {
5055
if (typeof value[0] === 'string') {
5156
const type = value[0];
5257

58+
const reviver = revivers?.[type];
59+
if (reviver) {
60+
return (hydrated[index] = reviver(hydrate(value[1])));
61+
}
62+
5363
switch (type) {
5464
case 'Date':
5565
hydrated[index] = new Date(value[1]);
@@ -90,6 +100,9 @@ export function unflatten(parsed) {
90100
obj[value[i]] = hydrate(value[i + 1]);
91101
}
92102
break;
103+
104+
default:
105+
throw new Error(`Unknown type ${type}`);
93106
}
94107
} else {
95108
const array = new Array(value.length);

src/stringify.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,21 @@ import {
1717
/**
1818
* Turn a value into a JSON string that can be parsed with `devalue.parse`
1919
* @param {any} value
20+
* @param {Record<string, (value: any) => any>} reducers
2021
*/
21-
export function stringify(value) {
22+
export function stringify(value, reducers) {
2223
/** @type {any[]} */
2324
const stringified = [];
2425

2526
/** @type {Map<any, number>} */
2627
const indexes = new Map();
2728

29+
/** @type {Array<{ key: string, fn: (value: any) => any }>} */
30+
const custom = [];
31+
for (const key in reducers) {
32+
custom.push({ key, fn: reducers[key] });
33+
}
34+
2835
/** @type {string[]} */
2936
const keys = [];
3037

@@ -47,6 +54,14 @@ export function stringify(value) {
4754
const index = p++;
4855
indexes.set(thing, index);
4956

57+
for (const { key, fn } of custom) {
58+
const value = fn(thing);
59+
if (value) {
60+
stringified[index] = `["${key}",${flatten(value)}]`;
61+
return index;
62+
}
63+
}
64+
5065
let str = '';
5166

5267
if (is_primitive(thing)) {

test/test.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import * as assert from 'uvu/assert';
33
import * as uvu from 'uvu';
44
import { uneval, unflatten, parse, stringify } from '../index.js';
55

6+
class Custom {
7+
constructor(value) {
8+
this.value = value;
9+
}
10+
}
11+
612
const fixtures = {
713
basics: [
814
{
@@ -374,12 +380,32 @@ const fixtures = {
374380
assert.equal(Object.keys(value).length, 0);
375381
}
376382
}
383+
],
384+
385+
custom: [
386+
{
387+
name: 'Custom type',
388+
value: new Custom({ answer: 42 }),
389+
js: null,
390+
json: '[["Custom",1],{"answer":2},42]',
391+
reducers: {
392+
Custom: (value) => value instanceof Custom && value.value
393+
},
394+
revivers: {
395+
Custom: (value) => new Custom(value)
396+
},
397+
validate: (obj) => {
398+
assert.ok(obj instanceof Custom);
399+
assert.equal(obj.value.answer, 42);
400+
}
401+
}
377402
]
378403
};
379404

380405
for (const [name, tests] of Object.entries(fixtures)) {
381406
const test = uvu.suite(`uneval: ${name}`);
382407
for (const t of tests) {
408+
if (t.reducers) continue;
383409
test(t.name, () => {
384410
const actual = uneval(t.value);
385411
const expected = t.js;
@@ -393,7 +419,7 @@ for (const [name, tests] of Object.entries(fixtures)) {
393419
const test = uvu.suite(`stringify: ${name}`);
394420
for (const t of tests) {
395421
test(t.name, () => {
396-
const actual = stringify(t.value);
422+
const actual = stringify(t.value, t.reducers);
397423
const expected = t.json;
398424
assert.equal(actual, expected);
399425
});
@@ -405,7 +431,7 @@ for (const [name, tests] of Object.entries(fixtures)) {
405431
const test = uvu.suite(`parse: ${name}`);
406432
for (const t of tests) {
407433
test(t.name, () => {
408-
const actual = parse(t.json);
434+
const actual = parse(t.json, t.revivers);
409435
const expected = t.value;
410436

411437
if (t.validate) {
@@ -421,6 +447,7 @@ for (const [name, tests] of Object.entries(fixtures)) {
421447
for (const [name, tests] of Object.entries(fixtures)) {
422448
const test = uvu.suite(`unflatten: ${name}`);
423449
for (const t of tests) {
450+
if (t.reducers) continue;
424451
test(t.name, () => {
425452
const actual = unflatten(JSON.parse(t.json));
426453
const expected = t.value;

0 commit comments

Comments
 (0)