import { inferCategory } from './category.js' import { inferContractsFromRouteSchema } from './schema-to-contract.js' // Reuse empty arrays to avoid allocation import type { HttpMethod, OutboundBinding, RouteContract, ValidatedFormula } from '../types.js' const EMPTY_REQUIRES: ValidatedFormula[] = [] const EMPTY_ENSURES: ValidatedFormula[] = [] const EMPTY_INVARIANTS: ValidatedFormula[] = [] // Two-level cache: WeakMap> // Preserves automatic GC of schema objects while correctly caching per-route contracts const contractCache = new WeakMap, Map>() export const extractContract = ( path: string, method: string, schema: Record | undefined ): RouteContract => { const s = schema ?? {} // Fast path: two-level cache lookup (guard against null — WeakMap rejects null keys) if (schema != null) { let routeMap = contractCache.get(schema) if (routeMap === undefined) { routeMap = new Map() contractCache.set(schema, routeMap) } const key = `${method.toUpperCase()} ${path}` const cached = routeMap.get(key) if (cached !== undefined) { return cached } } const override = typeof s['x-category'] === 'string' ? s['x-category'] : undefined const category = inferCategory(path, method, override) // APOPHIS annotations may live on the top-level schema OR nested inside // response.statusCode (e.g. schema.response[200]['x-ensures']). // We merge both levels so contracts are never silently dropped. const responseSchema = (s.response ?? {}) as Record> const firstStatus = Object.values(responseSchema)[0] ?? {} const topRequires = s['x-requires'] const nestedRequires = firstStatus['x-requires'] const requires = Array.isArray(topRequires) && topRequires.length > 0 ? (topRequires as string[]) : Array.isArray(nestedRequires) && nestedRequires.length > 0 ? (nestedRequires as string[]) : EMPTY_REQUIRES const topEnsures = s['x-ensures'] const nestedEnsures = firstStatus['x-ensures'] const explicitEnsures = Array.isArray(topEnsures) && topEnsures.length > 0 ? (topEnsures as string[]) : Array.isArray(nestedEnsures) && nestedEnsures.length > 0 ? (nestedEnsures as string[]) : [] // Infer contracts from JSON Schema constraints (required, minimum, maximum, pattern, etc.) // These supplement explicit x-ensures — never replace them. const inferred = inferContractsFromRouteSchema(s) const inferredSet = new Set(inferred) const explicitSet = new Set(explicitEnsures) // Deduplicate: don't add inferred formulas that the user already wrote explicitly const additionalInferred = inferred.filter(f => !explicitSet.has(f)) // Merge: explicit first, then inferred const ensures = explicitEnsures.length > 0 || additionalInferred.length > 0 ? [...explicitEnsures, ...additionalInferred] : EMPTY_ENSURES const validateRuntime = (s['x-validate-runtime'] !== false) && (firstStatus['x-validate-runtime'] !== false) // Extract timeout from schema annotation const timeoutValue = s['x-timeout'] ?? firstStatus['x-timeout'] const timeout = typeof timeoutValue === 'number' && timeoutValue > 0 ? timeoutValue : undefined // Parse x-outbound annotation const outboundRaw = s['x-outbound'] const outbound: OutboundBinding[] | undefined = Array.isArray(outboundRaw) ? (outboundRaw as OutboundBinding[]) : undefined // Parse x-variants annotation const variantsRaw = s['x-variants'] const variants = Array.isArray(variantsRaw) ? variantsRaw.map((v: unknown) => { if (typeof v === 'string') { return { name: v } } if (v !== null && typeof v === 'object') { const vo = v as Record return { name: String(vo.name || 'unnamed'), headers: vo.headers as Record | undefined, when: vo.when as string | undefined, } } return { name: String(v) } }) : undefined const contract: RouteContract = { path, method: method.toUpperCase() as HttpMethod, category, requires: requires as ValidatedFormula[], ensures: ensures as ValidatedFormula[], invariants: EMPTY_INVARIANTS, regexPatterns: {}, validateRuntime, schema: s, timeout, outbound, variants, } if (schema !== undefined) { const key = `${method.toUpperCase()} ${path}` let routeMap = contractCache.get(schema) if (routeMap === undefined) { routeMap = new Map() contractCache.set(schema, routeMap) } routeMap.set(key, contract) } return contract }