UNPKG

@ayonli/jsext

Version:

A JavaScript extension package for building strong and modern applications.

226 lines (211 loc) 8.09 kB
/** * Functions for parsing JSONs to specific structures. * @module */ import { Constructor } from "./types.ts"; import { isValid } from "./object.ts"; import { fromObject } from "./error.ts"; import bytes from "./bytes.ts"; const typeRegistry = new Map<Object, { [prop: string | symbol]: Constructor<any>; }>(); /** * Converts a JSON string into an object of the given type. * * The type can be `String`, `Number`, `BigInt`, `Boolean`, `Date`, `Array`, `Object` or any * class constructor (including Error types). If the class has a static * `fromJSON(data: any): T` method, it will be invoked to create the instance (unless the * parsed `data` is `null`, which will be skipped without further processing). * * This function generally does not perform loose conversion between types, for example, * `parseAs('"123"', Number)` will not work, it only reverses to the same type before the * data are encoded. * * However, for compatibility support, there are some exceptions allowed, which are: * * - `string` => `Date` * - `number` or `string` => `bigint` * - `array` => `Buffer` or `TypedArray` (e.g. `Uint8Array`), when the data only contains * integers. * - `object` => `Buffer` or `TypedArray` (e.g. `Uint8Array`), if the data are encoded by * `JSON.stringify()`. * - customized in `fromJSON()` * * If the data cannot be converted to the given type, this function returns `null`. */ export function parseAs(text: string, type: StringConstructor): string | null; export function parseAs(text: string, type: NumberConstructor): number | null; export function parseAs(text: string, type: BigIntConstructor): bigint | null; export function parseAs(text: string, type: BooleanConstructor): boolean | null; export function parseAs<T>(text: string, type: Constructor<T> & { fromJSON?(data: any): T; }): T | null; export function parseAs<T>( text: string, type: Function & { fromJSON?(data: any): T; } ): T | null { const data = JSON.parse(text); return as(data, type as any) as T | null; } /** * Converts the data into the given type. * * This function is primarily used in {@link parseAs} and shares the same conversion rules, but it * can be used in other scenarios too, for example, inside the `fromJSON` function. */ export function as(data: unknown, type: StringConstructor): string | null; export function as(data: unknown, type: NumberConstructor): number | null; export function as(data: unknown, type: BigIntConstructor): bigint | null; export function as(data: unknown, type: BooleanConstructor): boolean | null; export function as<T>(data: unknown, type: Constructor<T> & { fromJSON?(data: any): T; }): T | null; export function as(data: unknown, type: any): any { if (data === null || data === undefined) { return null; } else if (typeof type.fromJSON === "function") { return type.fromJSON(data); } else if (typeof data === "boolean") { if (type === Boolean) { return data; } else { return null; } } else if (typeof data === "number") { if (type === Number) { return data; } else if (type === BigInt) { try { return BigInt(data); } catch { return null; } } else { return null; } } else if (typeof data === "string") { if (type === String) { return data; } else if (type === Date) { const date = new Date(data); return isValid(date) ? date : null; } else if (type === BigInt) { try { return BigInt(data); } catch { return null; } } else { return null; } } else if (Array.isArray(data)) { if (type === Array) { // Array return data; } else if (type.prototype instanceof Array) { // Array-derivative return (type as ArrayConstructor).from(data); } else if (typeof (type.prototype as any)[Symbol.iterator] === "function" && typeof (type as any)["from"] === "function" ) { // ArrayLike or TypedArray try { return (type as ArrayConstructor).from(data); } catch { return null; } } else { return null; } } else if (!([String, Number, Boolean, Date, Array] as Function[]).includes(type)) { if ((data as any).type === "Buffer" && Array.isArray((data as any).data)) { // Node.js Buffer if (typeof Buffer === "function" && type === Buffer) { try { return Buffer.from((data as any).data); } catch { return null; } } else if (typeof (type.prototype as any)[Symbol.iterator] === "function" && typeof (type as any)["from"] === "function" ) { try { // Convert Node.js Buffer to TypedArray. return (type as ArrayConstructor).from((data as any).data); } catch { return null; } } else { return null; } } else if ((data as any).type === "ByteArray" && Array.isArray((data as any).data)) { // ByteArray try { return bytes((data as any).data); } catch { return null; } } const keys = Object.getOwnPropertyNames(data); const values = Object.values(data); if (keys.slice(0, 50).map(Number).every(i => !Number.isNaN(i)) && values.slice(0, 50).map(Number).every(i => !Number.isNaN(i)) && typeof (type.prototype as any)[Symbol.iterator] === "function" && typeof (type as any)["from"] === "function" ) { // Assert the data is a TypedArray. try { return (type as ArrayConstructor).from(Object.values(data)); } catch { return null; } } else if (type.prototype instanceof Error) { const err = fromObject(data); if (err) { // Support @JSON.type() decorator in Error constructors. const typeRecords = typeRegistry.get(type.prototype as Object); if (typeRecords) { for (const key of Reflect.ownKeys(data)) { const ctor = typeRecords[key]; if (ctor) { (err as any)[key] = as((data as any)[key], ctor); } } } } return err; } else { const ins = Object.create(type.prototype as Object); const typeRecords = typeRegistry.get(type.prototype as Object); if (typeRecords) { for (const key of Reflect.ownKeys(data)) { const ctor = typeRecords[key]; ins[key] = ctor ? as((data as any)[key], ctor) : (data as any)[key]; } } else { Object.assign(ins, data); } return ins; } } else { return null; } } /** * A decorator to instruct that the target property in the class is of a specific type. * * When parsing JSON via {@link parseAs}, this property is guaranteed to be of the given type. * * **NOTE:** This decorator only supports TypeScript's `experimentalDecorators`. * * @example * ```ts * import { type } from "@ayonli/jsext/json"; * * class Example { * \@type(Date) * date: Date; * } * ``` */ export function type(ctor: Constructor<any>): PropertyDecorator { return (proto, prop) => { const record = typeRegistry.get(proto); if (record) { record[prop] = ctor; } else { typeRegistry.set(proto, { [prop]: ctor }); } }; }