-
-
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(isJsonable): add
isJsonable
function
- Loading branch information
1 parent
1ef2706
commit 39e4eb1
Showing
5 changed files
with
282 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,54 @@ | ||
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 | ||
| unknown[] | ||
| { [key: string]: unknown } | ||
| CustomJsonable; | ||
|
||
/** | ||
* Returns true if `x` is a JSON-serializable value, false otherwise. | ||
* | ||
* It does not check array or object properties recursively. | ||
* | ||
* 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": | ||
case "symbol": | ||
case "function": | ||
return isCustomJsonable(x); | ||
case "object": { | ||
if (x === null || Array.isArray(x)) return true; | ||
const p = Object.getPrototypeOf(x); | ||
if (p === BigInt.prototype || p === Function.prototype) { | ||
return isCustomJsonable(x); | ||
} | ||
return true; | ||
} | ||
} | ||
} |
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,71 @@ | ||
import { assert } from "@std/assert"; | ||
import { isJsonable } from "./jsonable.ts"; | ||
import { buildTestcases } from "./custom_jsonable_test.ts"; | ||
|
||
const repeats = Array.from({ length: 100 }); | ||
|
||
for (const [name, value] of buildTestcases()) { | ||
switch (name) { | ||
case "undefined": | ||
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 buildTestcases()) { | ||
switch (name) { | ||
case "undefined": | ||
case "null": | ||
continue; | ||
case "bigint": | ||
case "function": | ||
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 toJSON method)`, | ||
}); | ||
} | ||
} | ||
|
||
for (const [name, value] of buildTestcases()) { | ||
switch (name) { | ||
case "bigint": | ||
case "function": | ||
Deno.bench({ | ||
name: "current", | ||
fn() { | ||
const proto = Object.getPrototypeOf(value); | ||
proto.toJSON = () => "custom"; | ||
try { | ||
assert(repeats.every(() => isJsonable(value))); | ||
} finally { | ||
delete proto.toJSON; | ||
} | ||
}, | ||
group: | ||
`isJsonable (${name} with class prototype defines toJSON method)`, | ||
}); | ||
} | ||
} |
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,136 @@ | ||
import { assertEquals } from "@std/assert"; | ||
import { isJsonable } from "./jsonable.ts"; | ||
import { buildTestcases } from "./custom_jsonable_test.ts"; | ||
|
||
Deno.test("isJsonable", async (t) => { | ||
for (const [name, value] of buildTestcases()) { | ||
switch (name) { | ||
case "undefined": | ||
case "bigint": | ||
case "function": | ||
case "symbol": | ||
await t.step(`return false for ${name}`, () => { | ||
assertEquals(isJsonable(value), false); | ||
}); | ||
break; | ||
default: | ||
await t.step(`return true for ${name}`, () => { | ||
assertEquals(isJsonable(value), true); | ||
}); | ||
} | ||
} | ||
|
||
for (const [name, value] of buildTestcases()) { | ||
switch (name) { | ||
case "undefined": | ||
case "null": | ||
// Skip undefined, null that is not supported by Object.assign. | ||
continue; | ||
case "bigint": | ||
case "function": | ||
// Object.assign() doesn't make bigint, function Jsonable. | ||
await t.step( | ||
`return false for ${name} even if it is wrapped by Object.assign()`, | ||
() => { | ||
assertEquals( | ||
isJsonable( | ||
Object.assign(value as NonNullable<unknown>, { a: 0 }), | ||
), | ||
false, | ||
); | ||
}, | ||
); | ||
break; | ||
default: | ||
// Object.assign() makes other values Jsonable. | ||
await t.step( | ||
`return true for ${name} if it is wrapped by Object.assign()`, | ||
() => { | ||
assertEquals( | ||
isJsonable( | ||
Object.assign(value as NonNullable<unknown>, { a: 0 }), | ||
), | ||
true, | ||
); | ||
}, | ||
); | ||
} | ||
} | ||
|
||
for (const [name, value] of buildTestcases()) { | ||
switch (name) { | ||
case "undefined": | ||
case "null": | ||
// Skip undefined, null that is not supported by Object.assign. | ||
continue; | ||
case "bigint": | ||
case "function": | ||
// toJSON method assigned with Object.assign() makes bigint, function Jsonable. | ||
default: | ||
// toJSON method assigned with Object.assign() makes other values Jsonable. | ||
await t.step( | ||
`return true for ${name} if it has own toJSON method`, | ||
() => { | ||
assertEquals( | ||
isJsonable( | ||
Object.assign(value as NonNullable<unknown>, { | ||
toJSON: () => "custom", | ||
}), | ||
), | ||
true, | ||
); | ||
}, | ||
); | ||
} | ||
} | ||
|
||
for (const [name, value] of buildTestcases()) { | ||
switch (name) { | ||
case "undefined": | ||
case "null": | ||
// Skip undefined, null that does not have prototype | ||
continue; | ||
case "symbol": | ||
// toJSON method defined in the class prototype does not make symbol Jsonable. | ||
await t.step( | ||
`return false for ${name} if the class prototype defines toJSON method`, | ||
() => { | ||
const proto = Object.getPrototypeOf(value); | ||
proto.toJSON = () => "custom"; | ||
try { | ||
assertEquals(isJsonable(value), false); | ||
} finally { | ||
delete proto.toJSON; | ||
} | ||
}, | ||
); | ||
break; | ||
case "bigint": | ||
case "function": | ||
// toJSON method defined in the class prototype makes bigint, function Jsonable. | ||
default: | ||
// toJSON method defined in the class prototype makes other values Jsonable. | ||
await t.step( | ||
`return true for ${name} if the class prototype defines toJSON method`, | ||
() => { | ||
const proto = Object.getPrototypeOf(value); | ||
proto.toJSON = () => "custom"; | ||
try { | ||
assertEquals(isJsonable(value), true); | ||
} finally { | ||
delete proto.toJSON; | ||
} | ||
}, | ||
); | ||
} | ||
} | ||
|
||
await t.step( | ||
"returns true on circular reference (unwilling behavior)", | ||
() => { | ||
const circular = { a: {} }; | ||
circular["a"] = circular; | ||
assertEquals(isJsonable(circular), true); | ||
}, | ||
); | ||
}); |
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