diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ad2642e..31db7f83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/utils/README.md b/packages/utils/README.md index a2cd3481..c8f9e4c3 100644 --- a/packages/utils/README.md +++ b/packages/utils/README.md @@ -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) @@ -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. diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index e8095559..d411e20a 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -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"; diff --git a/packages/utils/src/utils/json.utils.spec.ts b/packages/utils/src/utils/json.utils.spec.ts new file mode 100644 index 00000000..c282e9ef --- /dev/null +++ b/packages/utils/src/utils/json.utils.spec.ts @@ -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 }); + }); + }); +}); diff --git a/packages/utils/src/utils/json.utils.ts b/packages/utils/src/utils/json.utils.ts new file mode 100644 index 00000000..8dce3b2b --- /dev/null +++ b/packages/utils/src/utils/json.utils.ts @@ -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 = (key: string): T => (value as Record)[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; +};