import { createDataAccessor } from './data-accessor.js'; import { toArray as toArrayValue, toBoolean as toBooleanValue, toInteger as toIntegerValue, toNumber as toNumberValue, toObject as toObjectValue, toString as toStringValue, } from './value-type.js'; interface BaseNode { readonly kind: string; readonly path?: string; } export interface ValueNode extends BaseNode { readonly kind: 'value'; readonly transform: 'string' | 'number' | 'integer' | 'boolean'; } export type ASTNode = ValueNode | ObjectNode | ArrayNode; export interface ObjectNode extends BaseNode { readonly kind: 'object'; readonly fields: Readonly>; } export interface ArrayNode extends BaseNode { readonly kind: 'array'; readonly item: ASTNode; } export interface Field { readonly toAST: () => ASTNode; readonly __type?: T; // 仅用于 TS 类型推导,运行时不存在 } type InferShape>> = { [K in keyof T]: T[K] extends Field ? U : never; }; const createValueField = ( transform: ValueNode['transform'], path?: string, ): Field => ({ toAST: () => ({ kind: 'value', path, transform }), }); export const toString = (path?: string) => createValueField('string', path); export const toNumber = (path?: string) => createValueField('number', path); export const toInteger = (path?: string) => createValueField('integer', path); export const toBoolean = (path?: string) => createValueField('boolean', path); const buildObjectFields = ( fields: Record>, ): Record => { const result: Record = {}; for (const key in fields) { if (Object.prototype.hasOwnProperty.call(fields, key)) { result[key] = fields[key].toAST(); } } return result; }; export function toObject>>( fields: T ): Field>; export function toObject>>( path: string, fields: T ): Field>; export function toObject( arg1: string | Record>, arg2?: Record>, ): Field { const path = typeof arg1 === 'string' ? arg1 : undefined; const fields = typeof arg1 === 'string' ? arg2! : arg1; return { toAST: () => ({ kind: 'object', path, fields: buildObjectFields(fields), }), }; } export function toArray(field: Field): Field; export function toArray(path: string, field: Field): Field; export function toArray(arg1: string | Field, arg2?: Field): Field { const path = typeof arg1 === 'string' ? arg1 : undefined; const field = typeof arg1 === 'string' ? arg2! : arg1; return { toAST: () => ({ kind: 'array', path, item: field.toAST(), }), }; } type Executor = (data: unknown) => T; const VALUE_TRANSFORMERS = { string: toStringValue, number: toNumberValue, integer: toIntegerValue, boolean: toBooleanValue, } as const; const createAccessor = (path?: string) => path ? createDataAccessor(path) : (v: unknown) => v; const compileValue = (node: ValueNode): Executor => { const accessor = createAccessor(node.path); const transformer = VALUE_TRANSFORMERS[node.transform]; return (data) => transformer(accessor(data)); }; const compileObject = ( node: ObjectNode, compileNode: (n: ASTNode) => Executor, ): Executor => { const accessor = createAccessor(node.path); const keys = Object.keys(node.fields); const fieldExecutors = keys.map(key => ({ key, exec: compileNode(node.fields[key]), })); return (data: unknown) => { const source = toObjectValue(accessor(data)) as Record; const result: Record = {}; for (let i = 0; i < fieldExecutors.length; i++) { const { key, exec } = fieldExecutors[i]; result[key] = exec(source); } return result; }; }; const compileArray = ( node: ArrayNode, compileNode: (n: ASTNode) => Executor, ): Executor => { const accessor = createAccessor(node.path); const itemExecutor = compileNode(node.item); return (data: unknown) => { const arr = toArrayValue(accessor(data)); return arr.map(itemExecutor); }; }; const compileAST = (node: ASTNode): Executor => { switch (node.kind) { case 'value': return compileValue(node); case 'object': return compileObject(node, compileAST); case 'array': return compileArray(node, compileAST); default: { const neverKind: never = node; throw new Error(`Unhandled node kind: ${neverKind}`); } } }; export const compile = (field: Field): Executor => { return compileAST(field.toAST()) as Executor; };