Skip to content

Commit

Permalink
feat: json reviver and replacer utils (#396)
Browse files Browse the repository at this point in the history
  • Loading branch information
peterpeterparker authored Aug 14, 2023
1 parent 223d9da commit e5679f7
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
- utils `v0.0.20`
- nns-proto `v0.0.6`

## Features

- Reviver and replacer to interpret `BigInt`, `Principal`, and `Uint8Array` in `JSON.stringify|parse`

# 0.18.1 (2023-08-07)

## Release
Expand Down
22 changes: 22 additions & 0 deletions packages/utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ npm i @dfinity/agent @dfinity/candid @dfinity/principal
- [toNullable](#gear-tonullable)
- [fromNullable](#gear-fromnullable)
- [fromDefinedNullable](#gear-fromdefinednullable)
- [jsonReplacer](#gear-jsonreplacer)
- [jsonReviver](#gear-jsonreviver)
- [principalToSubAccount](#gear-principaltosubaccount)
- [smallerVersion](#gear-smallerversion)

Expand Down Expand Up @@ -294,6 +296,26 @@ Not null and not undefined and not empty

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/utils/src/utils/did.utils.ts#L12)

#### :gear: jsonReplacer

A parser that interprets revived BigInt, Principal, and Uint8Array when constructing JavaScript values or objects.

| Function | Type |
| -------------- | ------------------------------------------- |
| `jsonReplacer` | `(_key: string, value: unknown) => unknown` |

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/utils/src/utils/json.utils.ts#L11)

#### :gear: jsonReviver

A function that alters the behavior of the stringification process for BigInt, Principal and Uint8Array.

| Function | Type |
| ------------- | ------------------------------------------- |
| `jsonReviver` | `(_key: string, value: unknown) => unknown` |

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/utils/src/utils/json.utils.ts#L30)

#### :gear: principalToSubAccount

Convert a principal to a Uint8Array 32 length.
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from "./utils/base32.utils";
export * from "./utils/crc.utils";
export * from "./utils/debounce.utils";
export * from "./utils/did.utils";
export * from "./utils/json.utils";
export * from "./utils/nullish.utils";
export * from "./utils/principal.utils";
export * from "./utils/version.utils";
107 changes: 107 additions & 0 deletions packages/utils/src/utils/json.utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { AnonymousIdentity } from "@dfinity/agent";
import { Principal } from "@dfinity/principal";
import { arrayOfNumberToUint8Array } from "@dfinity/utils";
import { jsonReplacer, jsonReviver } from "./json.utils";

describe("json-utils", () => {
describe("stringify", () => {
it("should stringify bigint with a custom representation", () => {
expect(JSON.stringify(123n, jsonReplacer)).toEqual(
'{"__bigint__":"123"}',
);
expect(JSON.stringify({ value: 123n }, jsonReplacer)).toEqual(
'{"value":{"__bigint__":"123"}}',
);
});

it("should stringify Principal with a custom representation", () => {
const principal = new AnonymousIdentity().getPrincipal();

expect(JSON.stringify(principal, jsonReplacer)).toEqual(
'{"__principal__":"2vxsx-fae"}',
);
expect(JSON.stringify({ principal }, jsonReplacer)).toEqual(
'{"principal":{"__principal__":"2vxsx-fae"}}',
);

const rootCanisterId = Principal.fromText("tmxop-wyaaa-aaaaa-aaapa-cai");

expect(JSON.stringify(rootCanisterId, jsonReplacer)).toEqual(
'{"__principal__":"tmxop-wyaaa-aaaaa-aaapa-cai"}',
);
expect(
JSON.stringify({ principal: rootCanisterId }, jsonReplacer),
).toEqual(
'{"principal":{"__principal__":"tmxop-wyaaa-aaaaa-aaapa-cai"}}',
);
});

it("should stringify Uint8Array with a custom representation", () => {
const arr = arrayOfNumberToUint8Array([1, 2, 3]);

expect(JSON.stringify(arr, jsonReplacer)).toEqual(
'{"__uint8array__":[1,2,3]}',
);
expect(JSON.stringify({ arr }, jsonReplacer)).toEqual(
'{"arr":{"__uint8array__":[1,2,3]}}',
);
});
});

describe("parse", () => {
it("should parse bigint from a custom representation", () => {
expect(JSON.parse('{"__bigint__":"123"}', jsonReviver)).toEqual(123n);
expect(JSON.parse('{"value":{"__bigint__":"123"}}', jsonReviver)).toEqual(
{ value: 123n },
);
});

it("should parse principal from a custom representation", () => {
const principal = new AnonymousIdentity().getPrincipal();

expect(JSON.parse('{"__principal__":"2vxsx-fae"}', jsonReviver)).toEqual(
principal,
);
expect(
JSON.parse('{"principal":{"__principal__":"2vxsx-fae"}}', jsonReviver),
).toEqual({ principal });

const rootCanisterId = Principal.fromText("tmxop-wyaaa-aaaaa-aaapa-cai");

expect(
JSON.parse(
'{"__principal__":"tmxop-wyaaa-aaaaa-aaapa-cai"}',
jsonReviver,
),
).toEqual(rootCanisterId);
expect(
JSON.parse(
'{"principal":{"__principal__":"tmxop-wyaaa-aaaaa-aaapa-cai"}}',
jsonReviver,
),
).toEqual({ principal: rootCanisterId });
});

it("should parse principal to object", () => {
const obj = JSON.parse(
'{"__principal__":"tmxop-wyaaa-aaaaa-aaapa-cai"}',
jsonReviver,
);
expect(obj instanceof Principal).toBeTruthy();
expect((obj as Principal).toText()).toEqual(
"tmxop-wyaaa-aaaaa-aaapa-cai",
);
});

it("should parse Uint8Array from a custom representation", () => {
const arr = arrayOfNumberToUint8Array([1, 2, 3]);

expect(JSON.parse('{"__uint8array__":[1,2,3]}', jsonReviver)).toEqual(
arr,
);
expect(
JSON.parse('{"arr":{"__uint8array__":[1,2,3]}}', jsonReviver),
).toEqual({ arr });
});
});
});
58 changes: 58 additions & 0 deletions packages/utils/src/utils/json.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Principal } from "@dfinity/principal";
import { nonNullish } from "./nullish.utils";

const JSON_KEY_BIGINT = "__bigint__";
const JSON_KEY_PRINCIPAL = "__principal__";
const JSON_KEY_UINT8ARRAY = "__uint8array__";

/**
* A parser that interprets revived BigInt, Principal, and Uint8Array when constructing JavaScript values or objects.
*/
export const jsonReplacer = (_key: string, value: unknown): unknown => {
if (typeof value === "bigint") {
return { [JSON_KEY_BIGINT]: `${value}` };
}

if (nonNullish(value) && value instanceof Principal) {
return { [JSON_KEY_PRINCIPAL]: value.toText() };
}

if (nonNullish(value) && value instanceof Uint8Array) {
return { [JSON_KEY_UINT8ARRAY]: Array.from(value) };
}

return value;
};

/**
* A function that alters the behavior of the stringification process for BigInt, Principal and Uint8Array.
*/
export const jsonReviver = (_key: string, value: unknown): unknown => {
const mapValue = <T>(key: string): T => (value as Record<string, T>)[key];

if (
nonNullish(value) &&
typeof value === "object" &&
JSON_KEY_BIGINT in value
) {
return BigInt(mapValue(JSON_KEY_BIGINT));
}

if (
nonNullish(value) &&
typeof value === "object" &&
JSON_KEY_PRINCIPAL in value
) {
return Principal.fromText(mapValue(JSON_KEY_PRINCIPAL));
}

if (
nonNullish(value) &&
typeof value === "object" &&
JSON_KEY_UINT8ARRAY in value
) {
return Uint8Array.from(mapValue(JSON_KEY_UINT8ARRAY));
}

return value;
};

0 comments on commit e5679f7

Please sign in to comment.