/** * @fileoverview CON-03 RouteRegistry - Route Schema Linking & Envelope Automation * @description Canonical registry linking API routes to their Zod schemas for type-safe routing * @version 0.18.4-alpha */ import { z } from 'zod'; import { ROUTES } from "../routes/routes"; import * as Schemas from "../schemas"; import { ApiEnvelopeSchema } from "../envelopes/standard"; import { validateResponse, validateResponseData } from "./validateResponse"; import { validateRequest as validateRequestUtil } from "./validateRequest"; /** * Route definition interface for type-safe route registration */ export interface RouteDefinition { path: string; method: "GET" | "POST" | "PUT" | "DELETE"; request?: z.ZodSchema; response?: z.ZodSchema; description?: string; deprecated?: boolean; version?: string; } /** * Canonical RouteRegistry - Single source of truth for all route definitions * Maps ROUTES constants to their Zod schemas for type-safe routing and validation */ export const RouteRegistry: Record = { // === AUTHENTICATION ROUTES === [ROUTES.AUTH.LOGIN]: { path: ROUTES.AUTH.LOGIN, method: "POST", request: z.object({ email: z.string().email(), password: z.string().min(1) }), response: z.object({ user: z.object({ id: z.string(), email: z.string().email(), role: z.string() }), access_token: z.string(), refresh_token: z.string() }), description: "User authentication endpoint", version: "0.18.0" }, [ROUTES.AUTH.LOGOUT]: { path: ROUTES.AUTH.LOGOUT, method: "POST", response: z.object({ success: z.boolean(), message: z.string() }), description: "User logout endpoint", version: "0.18.0" }, [ROUTES.AUTH.REFRESH]: { path: ROUTES.AUTH.REFRESH, method: "POST", request: z.object({ refresh_token: z.string() }), response: z.object({ access_token: z.string(), refresh_token: z.string() }), description: "Token refresh endpoint", version: "0.18.0" }, [ROUTES.AUTH.VALIDATE]: { path: ROUTES.AUTH.VALIDATE, method: "POST", request: z.object({ token: z.string() }), response: z.object({ valid: z.boolean(), user: z.object({ id: z.string(), email: z.string().email(), role: z.string() }).optional() }), description: "Token validation endpoint", version: "0.18.0" }, [ROUTES.AUTH.WHOAMI]: { path: ROUTES.AUTH.WHOAMI, method: "GET", response: z.object({ user: z.object({ id: z.string(), email: z.string().email(), role: z.string(), permissions: z.array(z.string()) }) }), description: "Get current user information", version: "0.18.0" }, // === PORTFOLIO ROUTES === [ROUTES.INFRA.PORTFOLIO.SUMMARY]: { path: ROUTES.INFRA.PORTFOLIO.SUMMARY, method: "GET", response: z.object({ total_value: z.number(), total_cost: z.number(), total_pl: z.number(), total_pl_percent: z.number(), accounts_count: z.number(), last_updated: z.string().datetime() }), description: "Portfolio overview and summary", version: "0.18.0" }, [ROUTES.INFRA.PORTFOLIO.ACCOUNTS]: { path: ROUTES.INFRA.PORTFOLIO.ACCOUNTS, method: "GET", response: z.object({ accounts: z.array(z.object({ id: z.string(), name: z.string(), type: z.string(), balance: z.number(), currency: z.string() })) }), description: "HL accounts (SIPP, ISA, GIA)", version: "0.18.0" }, [ROUTES.INFRA.PORTFOLIO.HOLDINGS]: { path: ROUTES.INFRA.PORTFOLIO.HOLDINGS, method: "GET", response: z.object({ holdings: z.array(z.object({ symbol: z.string(), quantity: z.number(), current_price: z.number(), market_value: z.number(), currency: z.string() })) }), description: "HL holdings data", version: "0.18.0" }, [ROUTES.INFRA.PORTFOLIO.VALUE]: { path: ROUTES.INFRA.PORTFOLIO.VALUE, method: "GET", response: z.object({ current_value: z.number(), currency: z.string(), last_updated: z.string().datetime() }), description: "Current portfolio value", version: "0.18.0" }, // === MARKET DATA ROUTES === [ROUTES.INFRA.MARKET.OHLCV]: { path: ROUTES.INFRA.MARKET.OHLCV, method: "GET", response: z.object({ bars: z.array(z.object({ timestamp: z.string().datetime(), open: z.number(), high: z.number(), low: z.number(), close: z.number(), volume: z.number() })) }), description: "OHLCV bars data", version: "0.18.0" }, [ROUTES.INFRA.MARKET.INDICATORS]: { path: ROUTES.INFRA.MARKET.INDICATORS, method: "GET", response: z.object({ indicators: z.array(z.object({ symbol: z.string(), timeframe: z.string(), indicator: z.string(), values: z.array(z.number()) })) }), description: "Technical indicators data", version: "0.18.0" }, [ROUTES.INFRA.MARKET.UNIVERSE]: { path: ROUTES.INFRA.MARKET.UNIVERSE, method: "GET", response: z.object({ symbols: z.array(z.object({ symbol: z.string(), name: z.string(), exchange: z.string(), type: z.string() })) }), description: "Available symbols universe", version: "0.18.0" }, // === ML ROUTES === [ROUTES.INFRA.ML.MODELS]: { path: ROUTES.INFRA.ML.MODELS, method: "GET", response: z.object({ models: z.array(z.object({ id: z.string(), name: z.string(), type: z.string(), status: z.string(), accuracy: z.number().optional() })) }), description: "ML model registry", version: "0.18.0" }, [ROUTES.INFRA.ML.SIGNALS_LATEST]: { path: ROUTES.INFRA.ML.SIGNALS_LATEST, method: "GET", response: z.object({ signals: z.array(z.object({ id: z.string(), symbol: z.string(), action: z.string(), confidence: z.number(), timestamp: z.string().datetime() })) }), description: "Latest ML signals", version: "0.18.0" }, [ROUTES.INFRA.ML.CONSENSUS_LATEST]: { path: ROUTES.INFRA.ML.CONSENSUS_LATEST, method: "GET", response: z.object({ consensus: z.object({ symbol: z.string(), action: z.string(), confidence: z.number(), model_count: z.number(), timestamp: z.string().datetime() }) }), description: "Latest ML consensus", version: "0.18.0" }, // === TRADING ROUTES === [ROUTES.INFRA.TRADING.POSITIONS]: { path: ROUTES.INFRA.TRADING.POSITIONS, method: "GET", response: z.object({ positions: z.array(z.object({ id: z.string(), symbol: z.string(), direction: z.string(), size: z.number(), current_price: z.number(), pnl: z.number() })) }), description: "Open trading positions", version: "0.18.0" }, [ROUTES.INFRA.TRADING.TRADES]: { path: ROUTES.INFRA.TRADING.TRADES, method: "GET", response: z.object({ trades: z.array(z.object({ id: z.string(), symbol: z.string(), action: z.string(), size: z.number(), price: z.number(), pnl: z.number(), timestamp: z.string().datetime() })) }), description: "Trading history", version: "0.18.0" }, [ROUTES.INFRA.TRADING.ACCOUNTS]: { path: ROUTES.INFRA.TRADING.ACCOUNTS, method: "GET", response: z.object({ accounts: z.array(z.object({ id: z.string(), name: z.string(), type: z.string(), balance: z.number(), currency: z.string() })) }), description: "Trading accounts", version: "0.18.0" }, // === JOBS ROUTES === [ROUTES.INFRA.JOBS.LIST]: { path: ROUTES.INFRA.JOBS.LIST, method: "GET", response: z.object({ jobs: z.array(z.object({ id: z.string(), name: z.string(), status: z.string(), created_at: z.string().datetime() })) }), description: "List recent jobs", version: "0.18.0" }, [ROUTES.INFRA.JOBS.GET]: { path: ROUTES.INFRA.JOBS.GET, method: "GET", response: z.object({ job: z.object({ id: z.string(), name: z.string(), status: z.string(), progress: z.number(), logs: z.array(z.string()) }) }), description: "Get job details", version: "0.18.0" }, // === SYSTEM ROUTES === [ROUTES.INFRA.SYSTEM.HEALTH]: { path: ROUTES.INFRA.SYSTEM.HEALTH, method: "GET", response: z.object({ status: z.string(), uptime: z.number(), version: z.string(), timestamp: z.string().datetime() }), description: "Overall system health", version: "0.18.0" }, // === MARKETPLACE ROUTES (STUBS) === [ROUTES.INFRA.MARKETPLACE.MODELS_CATALOG]: { path: ROUTES.INFRA.MARKETPLACE.MODELS_CATALOG, method: "GET", response: z.object({ models: z.array(z.object({ id: z.string(), name: z.string(), description: z.string(), vendor: z.string(), tags: z.array(z.string()) })) }), description: "Public model discovery catalog", version: "0.18.0" }, // === LEGACY ROUTES (DEPRECATED) === [ROUTES.PORTFOLIO.SUMMARY]: { path: ROUTES.PORTFOLIO.SUMMARY, method: "GET", response: z.object({ total_value: z.number(), total_cost: z.number(), total_pl: z.number(), total_pl_percent: z.number() }), description: "Legacy portfolio summary (deprecated)", deprecated: true, version: "0.17.0" }, [ROUTES.MARKET.OHLCV]: { path: ROUTES.MARKET.OHLCV, method: "GET", response: z.object({ bars: z.array(z.object({ timestamp: z.string().datetime(), open: z.number(), high: z.number(), low: z.number(), close: z.number(), volume: z.number() })) }), description: "Legacy OHLCV data (deprecated)", deprecated: true, version: "0.17.0" } }; /** * Type-safe route paths - all keys from RouteRegistry */ export type RoutePaths = keyof typeof RouteRegistry; /** * Route method type helper */ export type RouteMethod = typeof RouteRegistry[K]["method"]; /** * Route request type helper */ export type RouteRequest = typeof RouteRegistry[K]["request"] extends z.ZodSchema ? T : never; /** * Route response type helper */ export type RouteResponse = typeof RouteRegistry[K]["response"] extends z.ZodSchema ? T : never; /** * Registry metadata */ export const ROUTE_REGISTRY_METADATA = { version: "0.18.4-alpha", totalRoutes: Object.keys(RouteRegistry).length, canonicalRoutes: Object.values(RouteRegistry).filter(route => !route.deprecated).length, deprecatedRoutes: Object.values(RouteRegistry).filter(route => route.deprecated).length, created: new Date().toISOString() } as const; /** * === RUNTIME VALIDATION HOOKS (Phase 5) === * Type-safe validation functions for runtime use */ /** * Asserts that a payload is a valid response for the specified route * Provides compile-time route path validation and runtime schema validation * * @param route - Route path from RouteRegistry * @param payload - Response payload to validate * @throws {ZodError} if payload doesn't match route's expected response schema * * @example * ```typescript * // In infra/frontend tests or API clients: * const response = await fetchPortfolioSummary(); * assertValidResponse(ROUTES.INFRA.PORTFOLIO.SUMMARY, response); * // TypeScript now knows response is properly typed and validated * ``` */ export function assertValidResponse( route: K, payload: unknown ): asserts payload is { success: boolean; data: RouteResponse; meta?: Record; error?: any } { const routeDefinition = RouteRegistry[route]; if (!routeDefinition || !routeDefinition.response) { throw new Error(`No response schema defined for route: ${route}`); } validateResponse(routeDefinition.response, payload); } /** * Asserts that a payload is a valid request for the specified route * Provides compile-time route path validation and runtime schema validation * * @param route - Route path from RouteRegistry * @param payload - Request payload to validate * @throws {ZodError} if payload doesn't match route's expected request schema * * @example * ```typescript * // In infra/frontend before making requests: * const requestData = { email: "user@example.com", password: "secret" }; * assertValidRequest(ROUTES.AUTH.LOGIN, requestData); * // TypeScript now knows requestData is properly typed and validated * ``` */ export function assertValidRequest( route: K, payload: unknown ): asserts payload is RouteRequest { const routeDefinition = RouteRegistry[route]; if (!routeDefinition || !routeDefinition.request) { throw new Error(`No request schema defined for route: ${route}`); } validateRequestUtil(routeDefinition.request, payload); } /** * Validates response data without the envelope wrapper * Useful for internal processing where envelope is already stripped * * @param route - Route path from RouteRegistry * @param data - Response data to validate * @throws {ZodError} if data doesn't match route's expected response schema */ export function assertValidResponseData( route: K, data: unknown ): asserts data is RouteResponse { const routeDefinition = RouteRegistry[route]; if (!routeDefinition || !routeDefinition.response) { throw new Error(`No response schema defined for route: ${route}`); } validateResponseData(routeDefinition.response, data); } /** * Type-safe route response validator for specific routes * Enhanced version that provides better error messages * * @param routePath - Route path from RouteRegistry * @param payload - Response payload to validate * @throws {ZodError} if payload doesn't match route's expected response schema */ export function validateRouteResponse( routePath: K, payload: unknown ): asserts payload is { success: boolean; data: RouteResponse; meta?: Record; error?: any } { try { assertValidResponse(routePath, payload); } catch (error) { throw new Error(`Route ${routePath} response validation failed: ${error instanceof Error ? error.message : 'Unknown validation error'}`); } } /** * Type-safe route request validator for specific routes * Enhanced version that provides better error messages * * @param routePath - Route path from RouteRegistry * @param payload - Request payload to validate * @throws {ZodError} if payload doesn't match route's expected request schema */ export function validateRouteRequest( routePath: K, payload: unknown ): asserts payload is RouteRequest { try { assertValidRequest(routePath, payload); } catch (error) { throw new Error(`Route ${routePath} request validation failed: ${error instanceof Error ? error.message : 'Unknown validation error'}`); } } /** * Utility function to get route information at runtime * Useful for debugging and dynamic route handling * * @param routePath - Route path to look up * @returns Route definition or undefined if not found */ export function getRouteInfo(routePath: K): RouteDefinition | undefined { return RouteRegistry[routePath]; } /** * Get all routes for a specific HTTP method * * @param method - HTTP method to filter by * @returns Array of route paths that use the specified method */ export function getRoutesByMethod(method: "GET" | "POST" | "PUT" | "DELETE"): RoutePaths[] { return Object.entries(RouteRegistry) .filter(([, definition]) => definition.method === method) .map(([path]) => path as RoutePaths); } /** * Get all active (non-deprecated) routes * * @returns Array of active route paths */ export function getActiveRoutes(): RoutePaths[] { return Object.entries(RouteRegistry) .filter(([, definition]) => !definition.deprecated) .map(([path]) => path as RoutePaths); } /** * Get all deprecated routes * * @returns Array of deprecated route paths */ export function getDeprecatedRoutes(): RoutePaths[] { return Object.entries(RouteRegistry) .filter(([, definition]) => definition.deprecated) .map(([path]) => path as RoutePaths); } /** * Export route paths as string values for runtime use */ export const RoutePaths = Object.keys(RouteRegistry) as RoutePaths[];