Skip to content
Open
13 changes: 11 additions & 2 deletions src/accessDeep.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { isMap, isArray, isPlainObject, isSet } from './is.js';
import { includes } from './util.js';

const OUT_OF_BOUNDS_ERROR = 'index out of bounds';

const getNthKey = (value: Map<any, any> | Set<any>, n: number): any => {
if (n > value.size) throw new Error('index out of bounds');
if (n > value.size) throw new Error(OUT_OF_BOUNDS_ERROR);
const keys = value.keys();
while (n > 0) {
keys.next();
Expand Down Expand Up @@ -70,8 +72,11 @@ export const setDeep = (

if (isArray(parent)) {
const index = +key;
if (index > parent.length - 1) throw new Error(OUT_OF_BOUNDS_ERROR);
parent = parent[index];
} else if (isPlainObject(parent)) {
// Should use (key in parent) here and throw 'OUT_OF_BOUNDS_ERROR' if not present
// but it may affect performance if not really needed
parent = parent[key];
} else if (isSet(parent)) {
const row = +key;
Expand Down Expand Up @@ -100,8 +105,12 @@ export const setDeep = (
const lastKey = path[path.length - 1];

if (isArray(parent)) {
parent[+lastKey] = mapper(parent[+lastKey]);
const index = +lastKey;
if (index > parent.length - 1) throw new Error(OUT_OF_BOUNDS_ERROR);
parent[index] = mapper(parent[index]);
} else if (isPlainObject(parent)) {
// Should use (key in parent) here and throw 'OUT_OF_BOUNDS_ERROR' if not present
// but it may affect performance if not really needed
parent[lastKey] = mapper(parent[lastKey]);
}

Expand Down
35 changes: 27 additions & 8 deletions src/custom-transformer-registry.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
import { JSONValue } from './types.js';
import { SuperJSONValue, JSONValue } from './types.js';
import { find } from './util.js';

export interface CustomTransfomer<I, O extends JSONValue> {
export interface NonRecursiveCustomTransfomer<I, O extends JSONValue> {
name: string;
isApplicable: (v: any) => v is I;
serialize: (v: I) => O;
deserialize: (v: O) => I;
recursive?: false;
}

export interface RecursiveCustomTransfomer<I, O extends SuperJSONValue> {
name: string;
isApplicable: (v: any) => v is I;
serialize: (v: I) => O;
deserialize: (v: O) => I;
recursive: true;
}

export type AnyCustomTransformer =
| NonRecursiveCustomTransfomer<any, JSONValue>
| RecursiveCustomTransfomer<any, SuperJSONValue>;

export class CustomTransformerRegistry {
private transfomers: Record<string, CustomTransfomer<any, any>> = {};
private transformers: Record<string, AnyCustomTransformer> = {};

register<I, O extends JSONValue>(transformer: CustomTransfomer<I, O>) {
this.transfomers[transformer.name] = transformer;
register<I, O extends JSONValue>(
transformer: NonRecursiveCustomTransfomer<I, O>
): void;
register<I, O extends SuperJSONValue>(
transformer: RecursiveCustomTransfomer<I, O>
): void;
register(transformer: AnyCustomTransformer) {
this.transformers[transformer.name] = transformer;
}

findApplicable<T>(v: T) {
return find(this.transfomers, transformer =>
return find(this.transformers, transformer =>
transformer.isApplicable(v)
) as CustomTransfomer<T, JSONValue> | undefined;
) as AnyCustomTransformer | undefined;
}

findByName(name: string) {
return this.transfomers[name];
return this.transformers[name];
}
}
84 changes: 70 additions & 14 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,27 @@ describe('stringify & parse', () => {
},
},
},
'works for recrusive custom registry': {
input: () => {
class Custom {
constructor(public date: Date) {}
}
SuperJSON.registerCustom(
{
isApplicable: v => v instanceof Custom,
serialize: (v: Custom) => v.date,
deserialize: (v: any) => new Custom(v),
recursive: true,
},
'OurCustom'
);
return new Custom(new Date(2020, 1, 1));
},
output: new Date(2020, 1, 1).toISOString(),
outputAnnotations: {
values: [['custom', 'OurCustom'], ['Date']],
},
},
};

function deepFreeze(object: any, alreadySeenObjects = new Set()) {
Expand Down Expand Up @@ -832,7 +853,7 @@ describe('stringify & parse', () => {
}
if (meta) {
const { v, ...rest } = meta;
expect(v).toBe(1);
expect(v).toBe(2);
expect(rest).toEqual(expectedOutputAnnotations);
} else {
expect(meta).toEqual(expectedOutputAnnotations);
Expand Down Expand Up @@ -860,7 +881,7 @@ describe('stringify & parse', () => {
private topSpeed: number,
private color: 'red' | 'blue' | 'yellow',
private brand: string,
public carriages: Set<Carriage>,
public carriages: Set<Carriage>
) {}

public brag() {
Expand All @@ -871,25 +892,35 @@ describe('stringify & parse', () => {
SuperJSON.registerClass(Train);

const { json, meta } = SuperJSON.serialize({
s7: new Train(100, 'yellow', 'Bombardier', new Set([new Carriage('front'), new Carriage('back')])) as any,
s7: new Train(
100,
'yellow',
'Bombardier',
new Set([new Carriage('front'), new Carriage('back')])
) as any,
});

expect(json).toEqual({
s7: {
topSpeed: 100,
color: 'yellow',
brand: 'Bombardier',
carriages: [
{ name: 'front' },
{ name: 'back' },
],
carriages: [{ name: 'front' }, { name: 'back' }],
},
});

expect(meta).toEqual({
v: 1,
v: 2,
values: {
s7: [['class', 'Train'], { carriages: ["set", { 0: [['class', 'Carriage']], 1: [['class', 'Carriage']] }] }],
s7: [
['class', 'Train'],
{
carriages: [
'set',
{ 0: [['class', 'Carriage']], 1: [['class', 'Carriage']] },
],
},
],
},
});

Expand Down Expand Up @@ -954,7 +985,7 @@ describe('stringify & parse', () => {
a: '1000',
},
meta: {
v: 1,
v: 2,
values: {
a: ['bigint'],
},
Expand Down Expand Up @@ -1038,7 +1069,7 @@ test('regression #83: negative zero', () => {

const stringified = SuperJSON.stringify(input);
expect(stringified).toMatchInlineSnapshot(
`"{\\"json\\":\\"-0\\",\\"meta\\":{\\"values\\":[\\"number\\"],\\"v\\":1}}"`
`"{\\"json\\":\\"-0\\",\\"meta\\":{\\"values\\":[\\"number\\"],\\"v\\":2}}"`
);

const parsed: number = SuperJSON.parse(stringified);
Expand Down Expand Up @@ -1258,7 +1289,7 @@ test('regression #245: superjson referential equalities only use the top-most pa
"b",
],
},
"v": 1,
"v": 2,
}
`);

Expand Down Expand Up @@ -1339,7 +1370,9 @@ test('doesnt iterate to keys that dont exist', () => {
test('deserialize in place', () => {
const serialized = SuperJSON.serialize({ a: new Date() });
const deserializedCopy = SuperJSON.deserialize(serialized);
const deserializedInPlace = SuperJSON.deserialize(serialized, { inPlace: true });
const deserializedInPlace = SuperJSON.deserialize(serialized, {
inPlace: true,
});
expect(deserializedInPlace).toBe(serialized.json);
expect(deserializedCopy).not.toBe(serialized.json);
expect(deserializedCopy).toEqual(deserializedInPlace);
Expand Down Expand Up @@ -1384,3 +1417,26 @@ test('#310 fixes backwards compat', () => {
},
});
});

test('Handle Doller at start of string correctly', () => {
class Van {
constructor(public value: any) {}
}

SuperJSON.registerCustom(
{
isApplicable: v => v instanceof Van,
serialize: inst => inst.value,
deserialize: v => new Van(v),
recursive: true,
},
'Van'
);

const input = {
$key: 'value',
nested: { $nestedKey: new Van({ $van: 'van' }) },
};

expect(SuperJSON.deserialize(SuperJSON.serialize(input))).toEqual(input);
});
43 changes: 24 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { Class, JSONValue, SuperJSONResult, SuperJSONValue } from './types.js';
import { ClassRegistry, RegisterOptions } from './class-registry.js';
import { Registry } from './registry.js';
import {
CustomTransfomer,
NonRecursiveCustomTransfomer,
RecursiveCustomTransfomer,
AnyCustomTransformer,
CustomTransformerRegistry,
} from './custom-transformer-registry.js';
import {
applyReferentialEqualityAnnotations,
applyValueAnnotations,
applyMeta,
generateReferentialEqualityAnnotations,
walker,
} from './plainer.js';
Expand All @@ -31,7 +32,7 @@ export default class SuperJSON {
}

serialize(object: SuperJSONValue): SuperJSONResult {
const identities = new Map<any, any[][]>();
const identities = new Map<any, [any[], number][]>();
const output = walker(object, identities, this, this.dedupe);
const res: SuperJSONResult = {
json: output.transformedValue,
Expand All @@ -48,33 +49,29 @@ export default class SuperJSON {
identities,
this.dedupe
);

if (equalityAnnotations) {
res.meta = {
...res.meta,
referentialEqualities: equalityAnnotations,
};
}

if (res.meta) res.meta.v = 1;
if (res.meta) res.meta.v = 2;

return res;
}

deserialize<T = unknown>(payload: SuperJSONResult, options?: { inPlace?: boolean }): T {
deserialize<T = unknown>(
payload: SuperJSONResult,
options?: { inPlace?: boolean }
): T {
const { json, meta } = payload;

let result: T = options?.inPlace ? json : copy(json) as any;

if (meta?.values) {
result = applyValueAnnotations(result, meta.values, meta.v ?? 0, this);
}
let result: T = options?.inPlace ? json : (copy(json) as any);

if (meta?.referentialEqualities) {
result = applyReferentialEqualityAnnotations(
result,
meta.referentialEqualities,
meta.v ?? 0
);
if (meta) {
result = applyMeta(result, meta, this);
}

return result;
Expand All @@ -100,13 +97,21 @@ export default class SuperJSON {

readonly customTransformerRegistry = new CustomTransformerRegistry();
registerCustom<I, O extends JSONValue>(
transformer: Omit<CustomTransfomer<I, O>, 'name'>,
transformer: Omit<NonRecursiveCustomTransfomer<I, O>, 'name'>,
name: string
): void;
registerCustom<I, O extends SuperJSONValue>(
transformer: Omit<RecursiveCustomTransfomer<I, O>, 'name'>,
name: string
): void;
registerCustom(
transformer: Omit<AnyCustomTransformer, 'name'>,
name: string
) {
this.customTransformerRegistry.register({
name,
...transformer,
});
} as any);
}

readonly allowedErrorProps: string[] = [];
Expand Down
21 changes: 15 additions & 6 deletions src/pathstringifier.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
export type StringifiedPath = string;
type Path = string[];

export const escapeKey = (key: string) =>
key.replace(/\\/g, '\\\\').replace(/\./g, '\\.');
export const escapeKey = (key: string) => {
key = key.replace(/\\/g, '\\\\');
if (key[0] === '$') key = '\\' + key;
return key.replace(/\./g, '\\.');
};

export const stringifyPath = (path: Path): StringifiedPath =>
path
.map(String)
.map(escapeKey)
.join('.');

export const parsePath = (string: StringifiedPath, legacyPaths: boolean) => {
export const parsePath = (
string: StringifiedPath,
legacyPaths: boolean,
depthSegment: boolean
) => {
const result: string[] = [];

let segment = '';
Expand All @@ -23,7 +30,7 @@ export const parsePath = (string: StringifiedPath, legacyPaths: boolean) => {
segment += '\\';
i++;
continue;
} else if (escaped !== '.') {
} else if (escaped !== '.' && escaped !== '$') {
throw Error('invalid path');
}
}
Expand All @@ -45,8 +52,10 @@ export const parsePath = (string: StringifiedPath, legacyPaths: boolean) => {
segment += char;
}

const lastSegment = segment;
result.push(lastSegment);
let lastSegment = segment;
if (!depthSegment || lastSegment[0] !== '$') {
result.push(segment.slice(0, 2) === '\\$' ? segment.slice(1) : segment);
}

return result;
};
Loading