import { klona } from 'klona'; import { isVmArray, isVmConst, isVmRecord, type VmConst, type VmRecord, type VmValue } from '@mirascript/mirascript'; import { Expression, isExpression, type ExpressionOrValue } from '../expression.js'; import type { ChoiceParameterType, Parameter, ParameterMap } from './parameter.js'; import { TypeInfo } from '../type.js'; import type { ArgumentMap, ArgumentValue } from './argument.js'; import type { ParameterGroup } from './parameter-group.js'; const { isArray } = Array; /** 转换参数类型 */ export type ArgumentConverter = ( value: VmValue | null | undefined, defaults?: D, ) => R | Exclude; /** 回退值 */ function toFallback(fallback: F | undefined): Exclude { if (fallback === undefined) { throw new TypeError('Conversion failed and no fallback value provided'); } return fallback as Exclude; } /** * 通过枚举参数定义推测枚举类型 */ export function choiceParameterType( definition: Pick, ): TypeInfo | '' { // 根据参数类型确定的类型 const typeByType = definition.type === 'logical' ? 'boolean' : 'number'; // 根据默认值确定的类型 let typeByDefault: TypeInfo | '' = ''; let confidentDefault = false; const { value, choices } = definition; if (definition.type === 'multiSelect' && Array.isArray(value)) { const v: unknown = value[0]; if (isChoiceValue(v)) { typeByDefault = typeof v as TypeInfo; // 多选中的值信任度较高,因为新建参数定义的默认值为 [] confidentDefault = true; } } else { if (isChoiceValue(value)) { typeByDefault = typeof value as TypeInfo; // 新建参数定义的默认值都为假值 // 只有当默认值为真值时才信任类型推断 confidentDefault = !!value; } } if (typeof choices == 'function' || isExpression(choices)) { // 由于 choices 是动态的,可能生成任意类型的选项 // 对默认值类型推断的信心较低,完全不信任参数类型 // 避免 { type: 'multiSelect', value: [] } 导致类型推断为 'number' return confidentDefault ? typeByDefault : ''; } if (!choices?.length) { // 没有选项的情况下必须依赖默认值或参数类型来推断类型 return typeByDefault || typeByType; } // 认为枚举选项值类型一致,推测枚举类型 for (const choice of choices) { if (choice == null) continue; if (isChoiceValue(choice)) return typeof choice as TypeInfo; const { key } = choice; if (isChoiceValue(key)) return typeof key as TypeInfo; } return typeByDefault || typeByType; } /** 是否为枚举值类型 */ export function isChoiceValue(value: unknown): value is ChoiceParameterType { return typeof value == 'string' || typeof value == 'number' || typeof value == 'boolean'; } const defaultConverter: ArgumentConverter = (value, defaults) => { if (value != null) return value as ArgumentValue; return toFallback(defaults); }; /** 转换数组参数类型 */ function createArrayConverter( itemConverter: ArgumentConverter | null = null, ): ArgumentConverter { if (itemConverter == null) { return (value, defaults) => { if (value == null) return []; if (isVmArray(value)) return value as T[]; if (isVmConst(value)) return [value] as T[]; return toFallback(defaults); }; } return (value, defaults) => { if (value == null) return []; let arr: ArgumentValue[]; if (isVmArray(value)) arr = value as ArgumentValue[]; else if (isVmConst(value)) arr = [value]; else return toFallback(defaults); if (arr.length === 0) return arr as T[]; let ret: T[] | null = null; for (let i = 0; i < arr.length; i++) { const el = arr[i]; const r = itemConverter(el, FALLBACK); if (r === FALLBACK) return toFallback(defaults); if (!Object.is(r, el)) { ret ??= [...(arr as T[])]; ret[i] = r; } } return ret ?? (arr as T[]); }; } /** 转换记录参数类型 */ function createRecordConverter = VmRecord>( fieldConverters: ReadonlyArray<[key: keyof T, converter: ArgumentConverter | null]> | null = null, ): ArgumentConverter { if (fieldConverters == null) { return (value, defaults) => { if (isVmRecord(value)) return value as T; if (isVmArray(value)) { const obj: Record = {}; for (let i = 0; i < value.length; i++) { obj[i] = value[i] ?? null; } return obj as T; } return toFallback(defaults); }; } return (value, defaults) => { let record: VmRecord; if (isVmRecord(value)) record = value; else if (isVmArray(value)) { const obj: Record = {}; for (let i = 0; i < value.length; i++) { obj[i] = value[i] ?? null; } record = obj; } else return toFallback(defaults); let ret: T | null = null; for (const [key, converter] of fieldConverters) { if (typeof converter != 'function') continue; const fieldValue = record[key as string]; const r = converter(fieldValue, FALLBACK); if (r === FALLBACK) return toFallback(defaults); if (!Object.is(r, fieldValue)) { ret ??= { ...(record as T) }; ret[key] = r as T[typeof key]; } } return ret ?? (record as T); }; } const FALLBACK = Symbol('FALLBACK'); /** 转换推测参数类型,不回退到 {@link Parameter.value} 提供的默认值 */ export function argumentConverter(definition: Parameter): ArgumentConverter { switch (definition.type) { case 'real': case 'integer': return TypeInfo.converter('number'); case 'text': case 'file': case 'code': case 'pinLike' as never: case 'cssColor' as never: case 'cssBackground' as never: case 'resourceId' as never: return TypeInfo.converter('string'); case 'choice': case 'logical': { const type = choiceParameterType(definition); return TypeInfo.converter(type); } case 'multiSelect': { const type = choiceParameterType(definition); const to = TypeInfo.converter(type); return createArrayConverter(to); } case 'table': { const { columns } = definition; if (!isArray(columns) || !columns.length) { return createArrayConverter(); } if (columns.length === 1) { const item = argumentConverter(columns[0]!); return createArrayConverter(item); } else { return createArrayConverter( createRecordConverter( columns.map((col, index) => [col.key || String(index), argumentConverter(col)] as const), ), ); } } case 'grouped': case 'record': { return createRecordConverter(); } default: return defaultConverter; } } /** * 转换参数值的类型,不回退到 {@link Parameter.value} 提供的默认值 * 转换失败时返回 null */ export function toArgumentValue(value: VmValue | null | undefined, definition: Parameter): ArgumentValue | null { const converter = argumentConverter(definition); return converter(value, null); } /** 参数定义数组 */ type ParameterDefinitions = ReadonlyArray>; /** 是否为包含参数组的参数定义数组 */ function checkParameterDefinitions(value: ParameterDefinitions | undefined): value is ParameterDefinitions { return value != null && isArray(value) && value.length > 0; } /** 查找指定参数 */ function findParameterImpl(items: readonly Parameter[], key: string): Parameter | undefined { if (items.length === 0) return undefined; for (const item of items) { if (item == null) continue; if (item.key === key) return item; } return undefined; } /** 查找指定参数 */ export function findParameter(definition: ParameterDefinitions | undefined, key: string): Parameter | undefined { if (!checkParameterDefinitions(definition)) return undefined; for (const group of definition) { if (group == null || !isArray(group.items)) continue; const found = findParameterImpl(group.items, key); if (found) return found; } return undefined; } /** 获取参数默认值,修复现有的参数,填充缺失的参数,删除未定义的参数 */ export function fillArgumentMap>( definition: ParameterDefinitions | undefined, args?: ArgumentMap | null, ): ArgumentMap { args ??= {}; if (!checkParameterDefinitions(definition)) return args; /** 参数键的类型 */ type K = keyof T; /** 参数值的类型 */ type V = NonNullable; const keys = new Set(Object.keys(args) as K[]); // 根据定义填充或修复参数 for (const group of definition) { for (const param of group.items) { const key = param.key as K; if (!key) continue; const currentValue = keys.has(key) ? args[key] : undefined; if (currentValue == null) { // 使用默认值填充 const defaultValue = param.value as ExpressionOrValue; if (isExpression(defaultValue)) { args[key] = Expression(defaultValue.source); } else { const value = defaultValue != null && typeof defaultValue == 'object' ? klona(defaultValue) : defaultValue; args[key] = (toArgumentValue(value, param) as V | null) ?? value; } } else { // 已存在的参数值 if (isExpression(currentValue)) { // 保留表达式 } else { // 修复现有的参数值 const castedValue = toArgumentValue(currentValue, param) as V | null; if (castedValue != null && castedValue !== currentValue) { args[key] = castedValue; } } } keys.delete(key); } } // 删除多余的参数 for (const key of keys) { // 保留特殊参数和私有字段 if (typeof key != 'string') continue; const k0 = key[0]; if (k0 === '_' || k0 === '$' || k0 === '@' || k0 === 'ɵ') continue; delete args[key]; } return args; }