From 685fc54066f704a157877dd29e3748b042dd09b3 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Jun 2020 16:02:38 +0200 Subject: [PATCH] json-rpc: Fork JSON compatibility code from @iov/encoding --- packages/json-rpc/src/compatibility.spec.ts | 125 ++++++++++++++++++++ packages/json-rpc/src/compatibility.ts | 73 ++++++++++++ packages/json-rpc/types/compatibility.d.ts | 23 ++++ 3 files changed, 221 insertions(+) create mode 100644 packages/json-rpc/src/compatibility.spec.ts create mode 100644 packages/json-rpc/src/compatibility.ts create mode 100644 packages/json-rpc/types/compatibility.d.ts diff --git a/packages/json-rpc/src/compatibility.spec.ts b/packages/json-rpc/src/compatibility.spec.ts new file mode 100644 index 00000000..485fc784 --- /dev/null +++ b/packages/json-rpc/src/compatibility.spec.ts @@ -0,0 +1,125 @@ +import { isJsonCompatibleArray, isJsonCompatibleDictionary, isJsonCompatibleValue } from "./compatibility"; + +describe("json", () => { + function sum(a: number, b: number): number { + return a + b; + } + + describe("isJsonCompatibleValue", () => { + it("returns true for primitive types", () => { + expect(isJsonCompatibleValue(null)).toEqual(true); + expect(isJsonCompatibleValue(0)).toEqual(true); + expect(isJsonCompatibleValue(1)).toEqual(true); + expect(isJsonCompatibleValue("abc")).toEqual(true); + expect(isJsonCompatibleValue(true)).toEqual(true); + expect(isJsonCompatibleValue(false)).toEqual(true); + }); + + it("returns true for arrays", () => { + expect(isJsonCompatibleValue([1, 2, 3])).toEqual(true); + expect(isJsonCompatibleValue([1, "2", true, null])).toEqual(true); + expect(isJsonCompatibleValue([1, "2", true, null, [1, "2", true, null]])).toEqual(true); + expect(isJsonCompatibleValue([{ a: 123 }])).toEqual(true); + }); + + it("returns true for simple dicts", () => { + expect(isJsonCompatibleValue({ a: 123 })).toEqual(true); + expect(isJsonCompatibleValue({ a: "abc" })).toEqual(true); + expect(isJsonCompatibleValue({ a: true })).toEqual(true); + expect(isJsonCompatibleValue({ a: null })).toEqual(true); + }); + + it("returns true for dict with array", () => { + expect(isJsonCompatibleValue({ a: [1, 2, 3] })).toEqual(true); + expect(isJsonCompatibleValue({ a: [1, "2", true, null] })).toEqual(true); + }); + + it("returns true for nested dicts", () => { + expect(isJsonCompatibleValue({ a: { b: 123 } })).toEqual(true); + }); + + it("returns false for functions", () => { + expect(isJsonCompatibleValue(sum)).toEqual(false); + }); + + it("returns true for empty dicts", () => { + expect(isJsonCompatibleValue({})).toEqual(true); + }); + }); + + describe("isJsonCompatibleArray", () => { + it("returns false for primitive types", () => { + expect(isJsonCompatibleArray(null)).toEqual(false); + expect(isJsonCompatibleArray(undefined)).toEqual(false); + expect(isJsonCompatibleArray(0)).toEqual(false); + expect(isJsonCompatibleArray(1)).toEqual(false); + expect(isJsonCompatibleArray("abc")).toEqual(false); + expect(isJsonCompatibleArray(true)).toEqual(false); + expect(isJsonCompatibleArray(false)).toEqual(false); + }); + + it("returns true for arrays", () => { + expect(isJsonCompatibleArray([1, 2, 3])).toEqual(true); + expect(isJsonCompatibleArray([1, "2", true, null])).toEqual(true); + expect(isJsonCompatibleArray([1, "2", true, null, [1, "2", true, null]])).toEqual(true); + expect(isJsonCompatibleArray([{ a: 123 }])).toEqual(true); + }); + + it("returns false for dicts", () => { + expect(isJsonCompatibleArray({ a: 123 })).toEqual(false); + expect(isJsonCompatibleArray({ a: "abc" })).toEqual(false); + expect(isJsonCompatibleArray({ a: true })).toEqual(false); + expect(isJsonCompatibleArray({ a: null })).toEqual(false); + }); + + it("returns false for functions", () => { + expect(isJsonCompatibleArray(sum)).toEqual(false); + }); + }); + + describe("isJsonCompatibleDictionary", () => { + it("returns false for primitive types", () => { + expect(isJsonCompatibleDictionary(null)).toEqual(false); + expect(isJsonCompatibleDictionary(undefined)).toEqual(false); + expect(isJsonCompatibleDictionary(0)).toEqual(false); + expect(isJsonCompatibleDictionary(1)).toEqual(false); + expect(isJsonCompatibleDictionary("abc")).toEqual(false); + expect(isJsonCompatibleDictionary(true)).toEqual(false); + expect(isJsonCompatibleDictionary(false)).toEqual(false); + }); + + it("returns false for other objects", () => { + expect(isJsonCompatibleDictionary(new Uint8Array([0x00]))).toEqual(false); + expect(isJsonCompatibleDictionary(/123/)).toEqual(false); + expect(isJsonCompatibleDictionary(new Date())).toEqual(false); + }); + + it("returns false for arrays", () => { + expect(isJsonCompatibleDictionary([1, 2, 3])).toEqual(false); + }); + + it("returns false for functions", () => { + expect(isJsonCompatibleDictionary(sum)).toEqual(false); + }); + + it("returns true for empty dicts", () => { + expect(isJsonCompatibleDictionary({})).toEqual(true); + }); + + it("returns true for simple dicts", () => { + expect(isJsonCompatibleDictionary({ a: 123 })).toEqual(true); + expect(isJsonCompatibleDictionary({ a: "abc" })).toEqual(true); + expect(isJsonCompatibleDictionary({ a: true })).toEqual(true); + expect(isJsonCompatibleDictionary({ a: null })).toEqual(true); + }); + + it("returns true for dict with array", () => { + expect(isJsonCompatibleDictionary({ a: [1, 2, 3] })).toEqual(true); + expect(isJsonCompatibleDictionary({ a: [1, "2", true, null] })).toEqual(true); + }); + + it("returns true for nested dicts", () => { + expect(isJsonCompatibleDictionary({ a: { b: 123 } })).toEqual(true); + }); + }); +}); diff --git a/packages/json-rpc/src/compatibility.ts b/packages/json-rpc/src/compatibility.ts new file mode 100644 index 00000000..0906f384 --- /dev/null +++ b/packages/json-rpc/src/compatibility.ts @@ -0,0 +1,73 @@ +/** + * A single JSON value. This is the missing return type of JSON.parse(). + */ +export type JsonCompatibleValue = + | JsonCompatibleDictionary + | JsonCompatibleArray + | string + | number + | boolean + | null; + +/** + * An array of JsonCompatibleValue + */ +// Use interface extension instead of type alias to make circular declaration possible. +export interface JsonCompatibleArray extends ReadonlyArray {} + +/** + * A string to json value dictionary. + */ +export interface JsonCompatibleDictionary { + readonly [key: string]: JsonCompatibleValue | readonly JsonCompatibleValue[]; +} + +export function isJsonCompatibleValue(value: unknown): value is JsonCompatibleValue { + if ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" || + value === null || + // eslint-disable-next-line @typescript-eslint/no-use-before-define + isJsonCompatibleArray(value) || + // eslint-disable-next-line @typescript-eslint/no-use-before-define + isJsonCompatibleDictionary(value) + ) { + return true; + } else { + return false; + } +} + +export function isJsonCompatibleArray(value: unknown): value is JsonCompatibleArray { + if (!Array.isArray(value)) { + return false; + } + + for (const item of value) { + if (!isJsonCompatibleValue(item)) { + return false; + } + } + + // all items okay + return true; +} + +export function isJsonCompatibleDictionary(data: unknown): data is JsonCompatibleDictionary { + if (typeof data !== "object" || data === null) { + // data must be a non-null object + return false; + } + + // Exclude special kind of objects like Array, Date or Uint8Array + // Object.prototype.toString() returns a specified value: + // http://www.ecma-international.org/ecma-262/7.0/index.html#sec-object.prototype.tostring + if (Object.prototype.toString.call(data) !== "[object Object]") { + return false; + } + + // TODO: replace with Object.values when available (ES2017+) + const values = Object.getOwnPropertyNames(data).map((key) => (data as any)[key]); + return values.every(isJsonCompatibleValue); +} diff --git a/packages/json-rpc/types/compatibility.d.ts b/packages/json-rpc/types/compatibility.d.ts new file mode 100644 index 00000000..9187f0d8 --- /dev/null +++ b/packages/json-rpc/types/compatibility.d.ts @@ -0,0 +1,23 @@ +/** + * A single JSON value. This is the missing return type of JSON.parse(). + */ +export declare type JsonCompatibleValue = + | JsonCompatibleDictionary + | JsonCompatibleArray + | string + | number + | boolean + | null; +/** + * An array of JsonCompatibleValue + */ +export interface JsonCompatibleArray extends ReadonlyArray {} +/** + * A string to json value dictionary. + */ +export interface JsonCompatibleDictionary { + readonly [key: string]: JsonCompatibleValue | readonly JsonCompatibleValue[]; +} +export declare function isJsonCompatibleValue(value: unknown): value is JsonCompatibleValue; +export declare function isJsonCompatibleArray(value: unknown): value is JsonCompatibleArray; +export declare function isJsonCompatibleDictionary(data: unknown): data is JsonCompatibleDictionary;