Files
core_ng/@sammo/util/src/jsonify.ts
T
2023-09-22 20:46:23 +09:00

238 lines
7.9 KiB
TypeScript

import { every, isString } from "lodash-es";
import type { BsonifiableClassObj, PlainBson } from "./bsonify.js";
export type PlainJson =
string | number | boolean | null | PlainJson[] | { [key: string]: PlainJson };
//엄밀히는 PlainJsonItem 스스로가 PlainJson이어야 하지만, Bson의 document와 호환을 맞추기 위해 object만 허용
export type PlainJsonObj =
{ [key: string]: PlainJson };
type JsonifiableLite =
string | number | boolean | bigint | null | undefined;
export type Jsonifiable =
JsonifiableLite |
Date | ArrayBuffer | SharedArrayBuffer | ArrayBufferView |
Array<Jsonifiable> |
Map<JsonifiableLite, Jsonifiable> |
Set<JsonifiableLite> |
JsonifiableClassObj<PlainJson> |
BsonifiableClassObj<PlainBson> |
{ [key: string]: Jsonifiable } |
ReadonlyArray<Jsonifiable>;
export type JsonifiableObj =
Map<string, Jsonifiable> |
JsonifiableClassObj<PlainJsonObj> |
BsonifiableClassObj<PlainBson> |
{ [key: string]: Jsonifiable };
export interface JsonifiableClassObj<T extends PlainJson> {
jsonify(): T;
}
type MaybeJsonified<T> = T extends Jsonifiable ? Jsonified<T> : never;
type MaybeJsonifiedObj<T> = T extends JsonifiableObj ? JsonifiedObj<T> : never;
export type Jsonified<T extends Jsonifiable|unknown> =
T extends string ? T :
T extends number ? T :
T extends boolean ? T :
T extends bigint ? ReturnType<T['toString']> :
T extends null ? T :
T extends undefined ? T :
T extends Date ? string :
T extends ArrayBuffer ? string :
T extends SharedArrayBuffer ? string :
T extends ArrayBufferView ? string :
T extends Map<infer K extends string, infer V> ? { [key in K]: MaybeJsonified<V> } :
T extends Map<infer K, infer V> ? [MaybeJsonified<K>, MaybeJsonified<V>][] :
T extends Set<infer V> ? MaybeJsonified<V>[] :
T extends JsonifiableClassObj<PlainJson> ? ReturnType<T['jsonify']> :
T extends BsonifiableClassObj<PlainBson> ? Jsonified<ReturnType<T['bsonify']>> :
T extends object ? { [key in keyof T]: MaybeJsonified<T[key]> } :
T extends unknown ? unknown :
never;
export type JsonifiedObj<T extends JsonifiableObj|unknown> =
T extends Map<infer K extends string, infer V> ? { [key in K]: MaybeJsonifiedObj<V> } :
T extends JsonifiableClassObj<PlainJson> ? ReturnType<T['jsonify']> :
T extends BsonifiableClassObj<PlainBson> ? Jsonified<ReturnType<T['bsonify']>> :
T extends object ? { [key in keyof T]: MaybeJsonified<T[key]> } :
T extends unknown ? unknown :
never;
/**
* Convert any object to JSON-safe object
*/
export function jsonify<T extends Jsonifiable>(item: T): Jsonified<T> {
if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
return item as Jsonified<typeof item>;
}
if (typeof item === 'bigint') {
return item.toString() as Jsonified<typeof item>;
}
if (typeof item === 'undefined') {
return item as Jsonified<typeof item>;
}
if (item === null) {
return item as Jsonified<typeof item>;
}
if (item instanceof Date) {
return item.toISOString() as Jsonified<typeof item>;
}
if (item instanceof ArrayBuffer) {
return Buffer.from(item).toString('base64') as Jsonified<typeof item>;
}
if ('SharedArrayBuffer' in globalThis && item instanceof SharedArrayBuffer) {
return Buffer.from(item).toString('base64') as Jsonified<typeof item>;
}
if (ArrayBuffer.isView(item)) {
return Buffer.from(item.buffer, item.byteOffset, item.byteLength).toString('base64') as Jsonified<typeof item>;
}
//HACK: depth hack, escape ts(2589)
if (item instanceof Map) {
const onlyStringKey = every(item.keys, isString);
if (onlyStringKey) {
const result: { [key: string]: string } = {};
for (const [k, v] of item.entries()) {
result[k as string] = jsonify(v as string);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}
const result: [string, string][] = [];
for (const [k, v] of item.entries()) {
result.push([jsonify(k as string), jsonify(v as string)]);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}
if (item instanceof Set) {
const result: string[] = [];
for (const v of item.values()) {
result.push(jsonify(v as string));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}
if (Array.isArray(item)) {
const result: string[] = [];
for (const v of item) {
result.push(jsonify(v as string));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}
if (typeof item !== 'object') {
throw new Error(`jsonify: invalid type ${typeof item}`);
}
if ('jsonify' in item && typeof item.jsonify === 'function') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return item.jsonify() as any;
}
if ('bsonify' in item && typeof item.bsonify === 'function') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return jsonify(item.bsonify() as Record<string,string>) as any;
}
const result: { [key: string]: string } = {};
for (const [k, v] of Object.entries(item)) {
if (typeof k !== 'string') {
continue;
}
if (k === 'prototype') {
continue;
}
if (k === '__proto__ ') {
continue;
}
if (typeof v === 'function') {
continue;
}
result[k] = jsonify(v as string);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}
export function jsonifyObj<T extends JsonifiableObj>(item: T): JsonifiedObj<T> {
if (item === null) {
throw new Error(`jsonifyObj: invalid type ${typeof item}`);
}
if (item === undefined) {
throw new Error(`jsonifyObj: invalid type ${typeof item}`);
}
//HACK: depth hack, escape ts(2589)
if (item instanceof Map) {
const onlyStringKey = every(item.keys, isString);
if (onlyStringKey) {
const result: { [key: string]: unknown } = {};
for (const [k, v] of item.entries()) {
result[k as string] = jsonify(v as Record<string,string>);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}
throw new Error(`jsonifyObj: invalid type ${typeof item}`);
}
if (Array.isArray(item)) {
throw new Error(`jsonifyObj: invalid type ${typeof item}`);
}
if (item instanceof Set) {
throw new Error(`jsonifyObj: invalid type ${typeof item}`);
}
if (typeof item !== 'object') {
throw new Error(`jsonify: invalid type ${typeof item}`);
}
if ('jsonify' in item && typeof item.jsonify === 'function') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return item.jsonify() as any;
}
if ('bsonify' in item && typeof item.bsonify === 'function') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return jsonify(item.bsonify() as Record<string,string>) as any;
}
const result: { [key: string]: unknown } = {};
for (const [k, v] of Object.entries(item)) {
if (typeof k !== 'string') {
continue;
}
if (k === 'prototype') {
continue;
}
if (k === '__proto__ ') {
continue;
}
if (typeof v === 'function') {
continue;
}
result[k] = jsonify(v as Record<string,string>);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}