/** * API 字段兼容检查器(Task 96)。 * * 为 Java/Python/Vue 三端统一响应字段契约:声明 schema(字段名、类型、 * 是否必填),对每个响应对象做兼容检查,产出缺失/类型错误清单。 * 各端接入同一 schema 描述即可在联调前发现字段漂移。 * * 语义: * - 必填字段缺失或类型不符产生 violation(kind: missing | type), * 其余字段安全跳过; * - checkBatch 遍历数组检查;空数组返回 passed,不创建无效检查记录; * - maxChecked 限制累计检查对象数(超出跳过计数),内存有界; * - 同一输入重复检查幂等,输入对象永不修改; * - schema 非法(非对象、未知类型)、maxChecked 非正数、检查对象非普通 * 对象均 fail-fast 抛错;schema 读取抛错时调用失败且零状态变更, * 依赖恢复后同一检查器继续可用。 */ export type ApiFieldType = 'number' | 'string' | 'boolean' | 'object' | 'array' export interface ApiFieldSpec { type: ApiFieldType required?: boolean } export interface ApiFieldViolation { field: string kind: 'missing' | 'type' expected: string actual?: string } export interface ApiFieldCompatOptions { /** 字段 schema:字段名 → 规格;空对象表示纯遍历(不校验) */ schema: Record /** 累计检查对象数上限,必须为正数,默认 1000 */ maxChecked?: number } export interface ApiFieldCompatResult { violations: ApiFieldViolation[] passed: boolean checkedCount: number violationCount: number } export interface ApiFieldCompatStats { checkedCount: number violationCount: number skippedCount: number /** 按检查顺序排列的对象序号(从 1 开始) */ checkedIds: number[] } export interface ApiFieldCompatChecker { /** 检查单个响应对象,返回违规清单(空数组表示通过) */ check: (obj: unknown) => ApiFieldViolation[] /** 检查响应数组,逐条走 check */ checkBatch: (objs: unknown[]) => void /** 最近一次 check/checkBatch 结果 */ lastResult: () => ApiFieldCompatResult /** 全部已检查过的字段名(按 schema 声明顺序) */ checkedFields: () => string[] stats: () => ApiFieldCompatStats } const SUPPORTED_TYPES: ApiFieldType[] = ['number', 'string', 'boolean', 'object', 'array'] function isPlainObject(value: unknown): value is Record { return ( typeof value === 'object' && value != null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype ) } export function createApiFieldCompatChecker(options: ApiFieldCompatOptions): ApiFieldCompatChecker { if (!isPlainObject(options.schema)) { throw new Error('schema 必须是对象') } const maxChecked = options.maxChecked ?? 1000 if (!(maxChecked > 0)) { throw new Error('maxChecked 必须为正数: ' + maxChecked) } const fieldOrder: string[] = [] for (const field of Object.keys(options.schema)) { const spec = options.schema[field] if (!SUPPORTED_TYPES.includes(spec.type)) { throw new Error('不支持的字段类型: ' + spec.type) } fieldOrder.push(field) } let checkedCount = 0 let violationCount = 0 let skippedCount = 0 const checkedIds: number[] = [] let lastViolations: ApiFieldViolation[] = [] function typeOf(value: unknown): ApiFieldType { if (value == null) return 'object' if (Array.isArray(value)) return 'array' if (typeof value === 'number') return 'number' if (typeof value === 'string') return 'string' if (typeof value === 'boolean') return 'boolean' if (typeof value === 'object') return 'object' return 'object' } function checkOne(obj: Record): ApiFieldViolation[] { const violations: ApiFieldViolation[] = [] for (const field of fieldOrder) { const spec = options.schema[field] const value = obj[field] if (value === undefined) { if (spec.required) { violations.push({ field, kind: 'missing', expected: spec.type }) } continue } const actual = typeOf(value) if (actual !== spec.type) { violations.push({ field, kind: 'type', expected: spec.type, actual }) } } return violations } function record(violations: ApiFieldViolation[]) { checkedCount += 1 checkedIds.push(checkedCount) if (violations.length > 0) { violationCount += violations.length } lastViolations = violations } function check(obj: unknown): ApiFieldViolation[] { if (!isPlainObject(obj)) { throw new Error('对象必须是普通对象') } if (checkedCount >= maxChecked) { skippedCount += 1 return [] } const violations = checkOne(obj) record(violations) return violations } function checkBatch(objs: unknown[]) { if (!Array.isArray(objs)) { throw new Error('对象数组必须是数组') } for (const obj of objs) { check(obj) } } function lastResult(): ApiFieldCompatResult { return { violations: lastViolations, passed: lastViolations.length === 0, checkedCount, violationCount, } } function checkedFields(): string[] { return [...fieldOrder] } function stats(): ApiFieldCompatStats { return { checkedCount, violationCount, skippedCount, checkedIds: [...checkedIds] } } return { check, checkBatch, lastResult, checkedFields, stats } }