-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add
isCustomJsonable
and isJsonable
- Loading branch information
1 parent
d2075df
commit 142f99b
Showing
8 changed files
with
459 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/** | ||
* Represents an object that has a custom `toJSON` method. | ||
* | ||
* See {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior|toJSON() behavior} of `JSON.stringify()` for more information. | ||
*/ | ||
export type CustomJsonable = { | ||
toJSON(key: string | number): unknown; | ||
}; | ||
|
||
/** | ||
* Returns true if `x` has own custom `toJSON` method ({@linkcode CustomJsonable}), false otherwise. | ||
* | ||
* Use {@linkcode [is/jsonable].isJsonable|isJsonable} to check if the type of `x` is a JSON-serializable. | ||
* | ||
* ```ts | ||
* import { is, CustomJsonable } from "@core/unknownutil"; | ||
* | ||
* const a: unknown = Object.assign(42n, { | ||
* toJSON() { | ||
* return `${this}n`; | ||
* } | ||
* }); | ||
* if (is.CustomJsonable(a)) { | ||
* const _: CustomJsonable = a; | ||
* } | ||
* ``` | ||
*/ | ||
export function isCustomJsonable(x: unknown): x is CustomJsonable { | ||
return x != null && typeof (x as CustomJsonable).toJSON === "function"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { assert } from "@std/assert"; | ||
import { isCustomJsonable } from "./custom_jsonable.ts"; | ||
|
||
const repeats = Array.from({ length: 100 }); | ||
const positive: unknown = { toJSON: () => "custom" }; | ||
const negative: unknown = {}; | ||
|
||
Deno.bench({ | ||
name: "current", | ||
fn() { | ||
assert(repeats.every(() => isCustomJsonable(positive))); | ||
}, | ||
group: "isCustomJsonable (positive)", | ||
}); | ||
|
||
Deno.bench({ | ||
name: "current", | ||
fn() { | ||
assert(repeats.every(() => !isCustomJsonable(negative))); | ||
}, | ||
group: "isCustomJsonable (negative)", | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { assertEquals } from "@std/assert"; | ||
import { isCustomJsonable } from "./custom_jsonable.ts"; | ||
|
||
const testcases: [name: string, value: unknown][] = [ | ||
undefined, | ||
null, | ||
"", | ||
0, | ||
true, | ||
[], | ||
{}, | ||
0n, | ||
() => {}, | ||
Symbol(), | ||
].map((x) => { | ||
const t = typeof x; | ||
switch (t) { | ||
case "object": | ||
if (x === null) { | ||
return ["null", x]; | ||
} else if (Array.isArray(x)) { | ||
return ["array", x]; | ||
} | ||
return ["object", x]; | ||
} | ||
return [t, x]; | ||
}); | ||
|
||
Deno.test("isCustomJsonable", async (t) => { | ||
for (const [name, value] of testcases) { | ||
await t.step(`return false for ${name}`, () => { | ||
assertEquals(isCustomJsonable(value), false); | ||
}); | ||
} | ||
|
||
for (const [name, value] of testcases) { | ||
switch (name) { | ||
// Skip undefined, null that is not supported by Object.assign. | ||
case "undefined": | ||
case "null": | ||
continue; | ||
} | ||
await t.step( | ||
`return false for ${name} even if it has wrapped by Object.assign`, | ||
() => { | ||
assertEquals( | ||
isCustomJsonable( | ||
Object.assign(value as NonNullable<unknown>, { a: 0 }), | ||
), | ||
false, | ||
); | ||
}, | ||
); | ||
} | ||
|
||
for (const [name, value] of testcases) { | ||
switch (name) { | ||
// Skip undefined, null that is not supported by Object.assign. | ||
case "undefined": | ||
case "null": | ||
continue; | ||
} | ||
await t.step( | ||
`return true for ${name} if it has own custom toJSON method`, | ||
() => { | ||
assertEquals( | ||
isCustomJsonable( | ||
Object.assign(value as NonNullable<unknown>, { | ||
toJSON: () => "custom", | ||
}), | ||
), | ||
true, | ||
); | ||
}, | ||
); | ||
} | ||
|
||
for (const [name, value] of testcases) { | ||
switch (name) { | ||
// Skip undefined, null that is not supported by Object.assign. | ||
case "undefined": | ||
case "null": | ||
continue; | ||
} | ||
await t.step( | ||
`return true for ${name} if it class defines custom toJSON method`, | ||
() => { | ||
// deno-lint-ignore no-explicit-any | ||
(value as any).constructor.prototype.toJSON = () => "custom"; | ||
try { | ||
assertEquals(isCustomJsonable(value), true); | ||
} finally { | ||
// deno-lint-ignore no-explicit-any | ||
delete (value as any).constructor.prototype.toJSON; | ||
} | ||
}, | ||
); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { type CustomJsonable, isCustomJsonable } from "./custom_jsonable.ts"; | ||
|
||
/** | ||
* Represents a JSON-serializable value. | ||
* | ||
* See {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description|Description} of `JSON.stringify()` for more information. | ||
*/ | ||
export type Jsonable = | ||
| string | ||
| number | ||
| boolean | ||
| null | ||
| Jsonable[] | ||
| { [key: string]: Jsonable } | ||
| CustomJsonable; | ||
|
||
/** | ||
* Returns true if `x` is a JSON-serializable value, false otherwise. | ||
* | ||
* Use {@linkcode [is/custom_jsonable].isCustomJsonable|isCustomJsonable} to check if the type of `x` has a custom `toJSON` method. | ||
* | ||
* ```ts | ||
* import { is, Jsonable } from "@core/unknownutil"; | ||
* | ||
* const a: unknown = "Hello, world!"; | ||
* if (is.Jsonable(a)) { | ||
* const _: Jsonable = a; | ||
* } | ||
* ``` | ||
*/ | ||
export function isJsonable(x: unknown): x is Jsonable { | ||
switch (typeof x) { | ||
case "undefined": | ||
return false; | ||
case "string": | ||
case "number": | ||
case "boolean": | ||
return true; | ||
case "bigint": | ||
return isCustomJsonable(x); | ||
case "object": { | ||
if (x === null) return false; | ||
const p = Object.getPrototypeOf(x); | ||
if (p === BigInt.prototype || p === Symbol.prototype) { | ||
return isCustomJsonable(x); | ||
} | ||
return true; | ||
} | ||
case "symbol": | ||
case "function": | ||
return isCustomJsonable(x); | ||
default: | ||
throw new Error(`Unexpected type: ${typeof x}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { assert } from "@std/assert"; | ||
import { isJsonable } from "./jsonable.ts"; | ||
|
||
const repeats = Array.from({ length: 100 }); | ||
const testcases: [name: string, value: unknown][] = [ | ||
undefined, | ||
null, | ||
"", | ||
0, | ||
true, | ||
[], | ||
{}, | ||
0n, | ||
() => {}, | ||
Symbol(), | ||
].map((x) => { | ||
const t = typeof x; | ||
switch (t) { | ||
case "object": | ||
if (x === null) { | ||
return ["null", x]; | ||
} else if (Array.isArray(x)) { | ||
return ["array", x]; | ||
} | ||
return ["object", x]; | ||
} | ||
return [t, x]; | ||
}); | ||
|
||
for (const [name, value] of testcases) { | ||
switch (name) { | ||
case "undefined": | ||
case "null": | ||
case "bigint": | ||
case "function": | ||
case "symbol": | ||
Deno.bench({ | ||
name: "current", | ||
fn() { | ||
assert(repeats.every(() => !isJsonable(value))); | ||
}, | ||
group: `isJsonable (${name})`, | ||
}); | ||
break; | ||
default: | ||
Deno.bench({ | ||
name: "current", | ||
fn() { | ||
assert(repeats.every(() => isJsonable(value))); | ||
}, | ||
group: `isJsonable (${name})`, | ||
}); | ||
} | ||
} | ||
|
||
for (const [name, value] of testcases) { | ||
switch (name) { | ||
case "bigint": | ||
case "function": | ||
case "symbol": | ||
Deno.bench({ | ||
name: "current", | ||
fn() { | ||
const v = Object.assign(value as NonNullable<unknown>, { | ||
toJSON: () => "custom", | ||
}); | ||
assert(repeats.every(() => isJsonable(v))); | ||
}, | ||
group: `isJsonable (${name} with own custom toJSON method)`, | ||
}); | ||
} | ||
} | ||
|
||
for (const [name, value] of testcases) { | ||
switch (name) { | ||
case "bigint": | ||
case "function": | ||
case "symbol": | ||
Deno.bench({ | ||
name: "current", | ||
fn() { | ||
// deno-lint-ignore no-explicit-any | ||
(value as any).constructor.prototype.toJSON = () => "custom"; | ||
try { | ||
assert(repeats.every(() => isJsonable(value))); | ||
} finally { | ||
// deno-lint-ignore no-explicit-any | ||
delete (value as any).constructor.prototype.toJSON; | ||
} | ||
}, | ||
group: `isJsonable (${name} with class defines custom toJSON method)`, | ||
}); | ||
} | ||
} |
Oops, something went wrong.