task-96: Java/Python/Vue 三端统一 API 字段兼容检查器

This commit is contained in:
2026-08-30 23:12:27 +08:00
parent cc41b5d81b
commit 1b491d972a
2 changed files with 303 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
/**
* API 字段兼容检查器(Task 96)。
*
* 为 Java/Python/Vue 三端统一响应字段契约:声明 schema(字段名、类型、
* 是否必填),对每个响应对象做兼容检查,产出缺失/类型错误清单。
* 各端接入同一 schema 描述即可在联调前发现字段漂移。
*
* 语义:
* - 必填字段缺失或类型不符产生 violationkind: 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<string, ApiFieldSpec>
/** 累计检查对象数上限,必须为正数,默认 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<string, unknown> {
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<string, unknown>): 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 }
}