Skip to content

Commit

Permalink
refactor: rename Replacer to Serializer and Reviver to Deserializer
Browse files Browse the repository at this point in the history
BREAKING CHANGE most apis are renamed
  • Loading branch information
Dmitry Steblyuk committed Sep 8, 2021
1 parent 467ad11 commit f42021d
Show file tree
Hide file tree
Showing 59 changed files with 504 additions and 462 deletions.
66 changes: 33 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

Serialization made finally easy. This library enables you to:

1. Write custom serialization plugins for any javascript classes, objects or functions.
2. Combine plugins together so that objects containing values of different types can be serialized.
3. Split serialization/deserialization to separate files (reduces bundle size in some cases).
4. Serialize most common javascript types with built-in plugins.
5. Preserve javascript references (including circular) with a built-in plugin.
- Write custom serializers for any javascript classes, objects or functions.
- Combine serializers to serialize objects with properties of different types.
- Split serialization and deserialization into separate files (reduces bundle size in some cases).
- Serialize most common javascript types with built-in serializers.
- Preserve javascript references (including circular) with a built-in serializer.

This library takes advantage of replacer/reviver callbacks provived by [JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#parameters) and [JSON.parse()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#parameters) respectively.
This library takes advantage of replacer and reviver callbacks provived by [JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#parameters) and [JSON.parse()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#parameters).

## Installation

Expand All @@ -24,69 +24,69 @@ Playground - https://codesandbox.io/s/custom-types-serializer-z6onp

### Serialize Javascript

Serialize most common javascript types with built-in `jsReplacer`/`jsReviver`.
Serialize most common javascript types with built-in `jsSerializer`/`jsDeserializer`.

```javascript
import { jsReplacer, jsReviver } from "custom-types-serializer";
import { jsSerializer, jsDeserializer } from "custom-types-serializer";

const data = {
error: new Error("Something went wrong."),
symbol: Symbol("test"),
set: new Set([new Date(1234567890), undefined, NaN, -0, 123n, /^(?=abc).*$/g]),
set: new Set([new Date(1234567890), undefined, NaN, -0, 123n, /^(?=abc).*$/i]),
};
const serialized = JSON.stringify(data, jsReplacer.getCallback());
const deserialized = JSON.parse(serialized, jsReviver.getCallback());
const serialized = JSON.stringify(data, jsSerializer.getReplacer());
const deserialized = JSON.parse(serialized, jsDeserializer.getReviver());

[...deserialized.set.values()][0].getTime(); // 1234567890
console.log([...deserialized.set.values()][0].getTime()); // 1234567890
```

### Serialize Custom Types

Write your own serialization plugins with `customType()`.
Create custom serializers with `customType()`.

```javascript
import moment from "moment";
import { customType } from "custom-types-serializer";

const momentType = customType("Moment");
const momentReplacer = momentType.createReplacer(
const momentSerializer = momentType.createSerializer(
// Use `original` value because moment implements `.toJSON()`.
(_value, { original }) => moment.isMoment(original),
String
);
const momentReviver = momentType.createReviver((isoString) => moment(isoString));
const momentDeserializer = momentType.createDeserializer((isoString) => moment(isoString));

const data = {
date: moment("2018-06-26 17:30"),
};
const serialized = JSON.stringify(data, momentReplacer.getCallback());
const deserialized = JSON.parse(serialized, momentReviver.getCallback());
const serialized = JSON.stringify(data, momentSerializer.getReplacer());
const deserialized = JSON.parse(serialized, momentDeserializer.getReviver());

deserialized.date.format("MMMM Do YYYY, h:mm:ss a"); // "June 26th 2018, 5:30:00 pm"
console.log(deserialized.date.format("MMMM Do YYYY, h:mm:ss a")); // "June 26th 2018, 5:30:00 pm"
```

### Combine Plugins
### Combine Serializers/Deserializers

Use `Replacer.combine(...replacers)` and `Reviver.combine(...revivers)` to combine plugins.
Use `Serializer.combine(...serializers)` and `Deserializer.combine(...deserializers)` to apply multiple serializers to the same object.

Use `referenceReplacer`, `referenceReviver` to preserve references.
Use `referenceSerializer`, `referenceDeserializer` to preserve references.

```javascript
import { Replacer, Reviver, mapReplacer, mapReviver, referenceReplacer, referenceReviver } from "custom-types-serializer";
import { Serializer, Deserializer, mapSerializer, mapDeserializer, referenceSerializer, referenceDeserializer } from "custom-types-serializer";

const myReplacer = Replacer.combine(referenceReplacer, mapReplacer);
const myReviver = Reviver.combine(referenceReviver, mapReviver);
const mySerializer = Serializer.combine(referenceSerializer, mapSerializer);
const myDeserializer = Deserializer.combine(referenceDeserializer, mapDeserializer);

const data = new Map();
const circular = { data };
data.set("a", circular);
data.set("b", circular);

const serialized = JSON.stringify(data, myReplacer.getCallback());
const deserialized = JSON.parse(serialized, myReviver.getCallback());
const serialized = JSON.stringify(data, mySerializer.getReplacer());
const deserialized = JSON.parse(serialized, myDeserializer.getReviver());

deserialized.get("a").data === deserialized; // true
deserialized.get("a") === deserialized.get("b"); // true
console.log(deserialized.get("a").data === deserialized); // true
console.log(deserialized.get("a") === deserialized.get("b")); // true
```

### Serialize Functions
Expand All @@ -97,21 +97,21 @@ import { customType } from "custom-types-serializer";
const registeredFunctions = [];

const functionType = customType("Function");
const functionReplacer = functionType.createReplacer(
const functionSerializer = functionType.createSerializer(
(x) => typeof x === "function",
(fn) => registeredFunctions.push(fn) - 1
);
const functionReviver = functionType.createReviver((id) => registeredFunctions[id]);
const functionDeserializer = functionType.createDeserializer((id) => registeredFunctions[id]);

const serialized = JSON.stringify(
{
doSmth() {
return "okay";
},
},
functionReplacer.getCallback()
functionSerializer.getReplacer()
);
const deserialized = JSON.parse(serialized, functionReviver.getCallback());
const deserialized = JSON.parse(serialized, functionDeserializer.getReviver());

deserialized.doSmth(); // "okay"
console.log(deserialized.doSmth()); // "okay"
```
10 changes: 5 additions & 5 deletions src/core-default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('customType()', () => {
});
});

describe('createReplacerCallback()/createReviverCallback()', () => {
describe('.createReplacer()/.createReviver()', () => {
it('should serialize any custom type', () => {
const value = {
date: new Date(1234567890),
Expand All @@ -39,7 +39,7 @@ describe('createReplacerCallback()/createReviverCallback()', () => {

const serialized = JSON.stringify(
value,
defaultCoreModule.createReplacerCallback((value, typed, {original}) => {
defaultCoreModule.createReplacer((value, typed, {original}) => {
if (original instanceof Date) {
return typed('Date', value as string);
}
Expand All @@ -60,7 +60,7 @@ describe('createReplacerCallback()/createReviverCallback()', () => {
);
const deserialzed = JSON.parse(
serialized,
defaultCoreModule.createReviverCallback((value: any, type) => {
defaultCoreModule.createReviver((value: any, type) => {
if (type === 'Date') {
return new Date(value);
}
Expand All @@ -86,7 +86,7 @@ describe('createReplacerCallback()/createReviverCallback()', () => {
const callCount = [0, 0];
const serialized = JSON.stringify(
Symbol('my-symbol'),
defaultCoreModule.createReplacerCallback((value, typed) => {
defaultCoreModule.createReplacer((value, typed) => {
callCount[0]++;
if (typeof value === 'symbol') {
const {description = null} = value;
Expand All @@ -97,7 +97,7 @@ describe('createReplacerCallback()/createReviverCallback()', () => {
);
const deserialzed: symbol = JSON.parse(
serialized,
defaultCoreModule.createReviverCallback((value: any, type) => {
defaultCoreModule.createReviver((value: any, type) => {
callCount[1]++;
if (type === 'Symbol') {
return Symbol(value ?? undefined);
Expand Down
10 changes: 7 additions & 3 deletions src/core-module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type {ReplacerCallback, ReviverCallback, WrapWithType} from './types';
import type {
SerializerCallback,
DeserializerCallback,
WrapWithType,
} from './types';
import {assertIsNonEmptyString} from './utils';

export abstract class AbstractCoreModule<T> {
Expand All @@ -9,7 +13,7 @@ export abstract class AbstractCoreModule<T> {
abstract isCustomType(value: unknown): value is T;
abstract getCustomTypeAndValue(value: T): {type: string; value: unknown};

createReplacerCallback(callback: ReplacerCallback) {
createReplacer(callback: SerializerCallback) {
const typed = this.#wrapWithType;
const self = this;

Expand All @@ -23,7 +27,7 @@ export abstract class AbstractCoreModule<T> {
};
}

createReviverCallback(callback: ReviverCallback) {
createReviver(callback: DeserializerCallback) {
const self = this;
return function (this: object, key: string, json: unknown) {
if (self.isCustomType(this)) {
Expand Down
19 changes: 11 additions & 8 deletions src/custom-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@ import {customType} from './custom-type';
describe('customType()', () => {
it('should work for the example with Moment from Readme', () => {
const momentType = customType<string>('Moment');
const momentReplacer = momentType.createReplacer(
const momentSerializer = momentType.createSerializer(
// Use `original` value because moment implements `.toJSON()`.
(_value, {original}) => moment.isMoment(original),
String,
);
const momentReviver = momentType.createReviver((isoString) =>
const momentDeserializer = momentType.createDeserializer((isoString) =>
moment(isoString),
);

const data = {
date: moment('2018-06-26 17:30'),
};
const serialized = JSON.stringify(data, momentReplacer.getCallback());
const serialized = JSON.stringify(data, momentSerializer.getReplacer());
const deserialized: typeof data = JSON.parse(
serialized,
momentReviver.getCallback(),
momentDeserializer.getReviver(),
);

expect(deserialized.date.format('MMMM Do YYYY, h:mm:ss a')).toBe(
Expand All @@ -31,11 +31,11 @@ describe('customType()', () => {
it('should work for the example with functions from Readme', () => {
const registeredFunctions: Function[] = [];
const functionType = customType<number>('Function');
const functionReplacer = functionType.createReplacer(
const functionSerializer = functionType.createSerializer(
(x): x is Function => typeof x === 'function',
(fn) => registeredFunctions.push(fn) - 1,
);
const functionReviver = functionType.createReviver(
const functionDeserializer = functionType.createDeserializer(
(id) => registeredFunctions[id],
);

Expand All @@ -45,9 +45,12 @@ describe('customType()', () => {
return 'okay';
},
},
functionReplacer.getCallback(),
functionSerializer.getReplacer(),
);
const deserialized = JSON.parse(
serialized,
functionDeserializer.getReviver(),
);
const deserialized = JSON.parse(serialized, functionReviver.getCallback());

expect(deserialized.doSmth()).toBe('okay');
});
Expand Down
34 changes: 20 additions & 14 deletions src/custom-type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {TypedReplacer} from './replacer';
import {TypedReviver} from './reviver';
import type {ReplacerContextWithState, ReviverContextWithState} from './types';
import {TypedSerializer} from './serializer';
import {TypedDeserializer} from './deserializer';
import type {
DeserializerContextWithState,
SerializerContextWithState,
} from './types';
import {assertIsNonEmptyString} from './utils';

export function customType<T>(id: string) {
Expand All @@ -21,31 +24,34 @@ class CustomType<T> {
assertIsNonEmptyString(id, 'Custom type new id');
this.#id = id;
}
createReplacer<V, S = undefined>(
createSerializer<V, S = undefined>(
check:
| ((value: unknown, context: ReplacerContextWithState<S>) => value is V)
| ((value: unknown, context: ReplacerContextWithState<S>) => boolean),
replace: (value: V, context: ReplacerContextWithState<S>) => T,
| ((
value: unknown,
context: DeserializerContextWithState<S>,
) => value is V)
| ((value: unknown, context: DeserializerContextWithState<S>) => boolean),
replace: (value: V, context: DeserializerContextWithState<S>) => T,
createState?: () => S,
) {
return new TypedReplacer<V, T>(this, (next) => {
return new TypedSerializer<V, T>(this, (next) => {
const state = createState?.()!;

return (value, typed, context) => {
const replacerContext = {...context, state};
const contextWithState = {...context, state};

if (check(value, replacerContext)) {
return typed(this.#id, replace(value, replacerContext));
if (check(value, contextWithState)) {
return typed(this.#id, replace(value, contextWithState));
}
return next(value, typed, context);
};
});
}
createReviver<V, S = undefined>(
revive: (value: T, context: ReviverContextWithState<S>) => V,
createDeserializer<V, S = undefined>(
revive: (value: T, context: SerializerContextWithState<S>) => V,
createState?: () => S,
) {
return new TypedReviver<T, V>(this, (next) => {
return new TypedDeserializer<T, V>(this, (next) => {
const state = createState?.()!;

return (value, typeId, context) => {
Expand Down
29 changes: 29 additions & 0 deletions src/deserializer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {describe, it, expect} from '@jest/globals';
import {customType} from './custom-type';
import {Deserializer} from './deserializer';

describe('Deserializer', () => {
it('should not allow duplicate types', () => {
const combined = Deserializer.combine(
Deserializer.combine(
customType('type1').createDeserializer(() => null),
customType('type2').createDeserializer(() => null),
),
Deserializer.combine(
Deserializer.combine(
customType('type3').createDeserializer(() => null),
),
customType('type4').createDeserializer(() => null),
customType('type5').createDeserializer(() => null),
),
);
expect(combined.getDeserializers().length).toBe(5);

expect(() => {
Deserializer.combine(
combined,
customType('type3').createDeserializer(() => null),
);
}).toThrowError(new Error('Duplicate custom type "type3".'));
});
});
Loading

0 comments on commit f42021d

Please sign in to comment.