code wiki / (root) / nx_http_router.nx

nx_http_router.nx source

↩ module page · 187 lines · 6883 B

1// nx_http_router.nx -- method+path route dispatch table. 2// 3// Final missing piece for porting an Elder AI service to NishiLang. 4// Existing substrate: 5// nx_http.nx -- request parser (64-header HttpRequest struct) 6// nx_http_io.nx -- minimal server-side parse + write 7// nx_http_resp.nx -- response builder 8// nx_http_client.nx -- HTTP client (outbound) 9// nx_http_date.nx -- date formatter 10// nx_syscalls + nx_syscalls_x86_64 -- socket bind/listen/accept 11// 12// This module adds the dispatch layer: a Route table of 13// (method, path_pattern, handler_id) triples, and a match function 14// that returns the handler_id for an incoming (method, path). 15// 16// NishiLang doesn't have first-class function pointers, so the caller 17// dispatches on handler_id via switch-style if/else chain. Same 18// pattern Flask + Express + FastAPI use internally before the 19// reflection layer. 20// 21// Path matching v1: exact match or prefix match (controlled per-route). 22// Path parameters (e.g. /api/character/:id) queued for v2 -- v1 routes 23// match prefix and let the handler peel the remainder. 24// 25// genealogy_id: flask_routing + express_dispatch + actix_web_routes 26// lineage_id: substrate_http_router_v1 27 28// nx_safety_envelope: 29// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 30// sil_target: SIL1 31// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 32// verdict: NOT_YET_EVALUATED 33 34import "nx_syscalls.nx" 35import "nx_tier.nx" 36 37// ===== Sealed-enum: HttpMethod ==================================== 38// 39// Matches nx_http_io's method_kind codes so the same numeric value 40// flows through parse -> route -> handler. 41 42const NX_HTTP_METHOD_UNKNOWN: nx_int = 0 43const NX_HTTP_METHOD_GET: nx_int = 1 44const NX_HTTP_METHOD_POST: nx_int = 2 45const NX_HTTP_METHOD_OPTIONS: nx_int = 3 46const NX_HTTP_METHOD_PUT: nx_int = 4 47const NX_HTTP_METHOD_DELETE: nx_int = 5 48const NX_HTTP_METHOD_PATCH: nx_int = 6 49const NX_HTTP_METHOD_HEAD: nx_int = 7 50const NX_HTTP_METHOD_ANY: nx_int = 8 // wildcard 51const NX_HTTP_METHOD_N_KINDS: nx_int = 9 52 53func nx_http_method_is_valid(m: nx_int) -> nx_int { 54 if m < 0 { return 0 } 55 if m >= NX_HTTP_METHOD_N_KINDS { return 0 } 56 return 1 57} 58 59// ===== Sealed-enum: PathMatchKind ================================= 60 61const NX_HTTP_MATCH_EXACT: nx_int = 0 62const NX_HTTP_MATCH_PREFIX: nx_int = 1 63const NX_HTTP_MATCH_N_KINDS: nx_int = 2 64 65// ===== Route record (flat-array layout) =========================== 66// 67// Each route occupies NX_ROUTE_FIELDS i64 slots. pattern_ptr is a 68// raw *u8 cast to i64; pattern_len is the byte length. handler_id 69// is caller-defined (typically a small enum). 70 71const NX_ROUTE_F_METHOD: nx_int = 0 72const NX_ROUTE_F_MATCH_KIND: nx_int = 1 73const NX_ROUTE_F_PATTERN_PTR: nx_int = 2 74const NX_ROUTE_F_PATTERN_LEN: nx_int = 3 75const NX_ROUTE_F_HANDLER_ID: nx_int = 4 76const NX_ROUTE_FIELDS: nx_int = 5 77 78// ===== Router struct ============================================== 79// 80// Routes table is a flat *i64 buffer; routes_count must be <= cap. 81 82struct HttpRouter { 83 routes_count: nx_int, 84 routes_cap: nx_int, 85 routes: *i64 86} 87 88const NX_HTTP_ROUTER_BYTES: nx_int = 24 89 90func nx_http_router_alloc(cap: nx_int) -> *HttpRouter { 91 let r: *HttpRouter = (sys_mmap(NX_HTTP_ROUTER_BYTES)) as *HttpRouter 92 r.routes_count = 0 93 r.routes_cap = cap 94 let bytes: nx_int = cap * NX_ROUTE_FIELDS * NX_SIZEOF_NX_INT 95 r.routes = (sys_mmap(bytes)) as *i64 96 return r 97} 98 99// ===== Add a route ================================================ 100// 101// Returns the route's slot index (>=0) or -1 if full. 102// pattern is a caller-owned *u8 buffer; we just store the pointer. 103 104func nx_http_router_add(r: *HttpRouter, 105 method: nx_int, match_kind: nx_int, 106 pattern: *u8, pattern_len: nx_int, 107 handler_id: nx_int) -> nx_int { 108 if r.routes_count >= r.routes_cap { return 0 - 1 } 109 if nx_http_method_is_valid(method) == 0 { return 0 - 1 } 110 let slot: nx_int = r.routes_count 111 let base: nx_int = slot * NX_ROUTE_FIELDS 112 r.routes[base + NX_ROUTE_F_METHOD] = method 113 r.routes[base + NX_ROUTE_F_MATCH_KIND] = match_kind 114 r.routes[base + NX_ROUTE_F_PATTERN_PTR] = pattern as nx_int 115 r.routes[base + NX_ROUTE_F_PATTERN_LEN] = pattern_len 116 r.routes[base + NX_ROUTE_F_HANDLER_ID] = handler_id 117 r.routes_count = r.routes_count + 1 118 return slot 119} 120 121// ===== Byte equality on (path, pattern, len) ====================== 122 123func _hr_bytes_eq(a: *u8, b: *u8, n: nx_int) -> nx_int { 124 var i: nx_int = 0 125 while i < n { 126 if a[i] != b[i] { return 0 } 127 i = i + 1 128 } 129 return 1 130} 131 132// ===== Match an incoming (method, path) =========================== 133// 134// Returns the handler_id of the first matching route, or -1 if none. 135// Routes are matched in registration order -- caller responsible for 136// adding more-specific routes BEFORE less-specific ones (common 137// dispatch convention; same as Flask + Express). 138 139func nx_http_router_match(r: *HttpRouter, 140 method: nx_int, 141 path: *u8, path_len: nx_int) -> nx_int { 142 var i: nx_int = 0 143 while i < r.routes_count { 144 let base: nx_int = i * NX_ROUTE_FIELDS 145 let m: nx_int = r.routes[base + NX_ROUTE_F_METHOD] 146 147 // Method must match (or wildcard ANY) 148 var method_ok: nx_int = 0 149 if m == method { method_ok = 1 } 150 if m == NX_HTTP_METHOD_ANY { method_ok = 1 } 151 152 if method_ok == 1 { 153 let kind: nx_int = r.routes[base + NX_ROUTE_F_MATCH_KIND] 154 let pat: *u8 = r.routes[base + NX_ROUTE_F_PATTERN_PTR] as *u8 155 let plen: nx_int = r.routes[base + NX_ROUTE_F_PATTERN_LEN] 156 157 if kind == NX_HTTP_MATCH_EXACT { 158 if path_len == plen { 159 if _hr_bytes_eq(path, pat, plen) == 1 { 160 return r.routes[base + NX_ROUTE_F_HANDLER_ID] 161 } 162 } 163 } 164 if kind == NX_HTTP_MATCH_PREFIX { 165 if path_len >= plen { 166 if _hr_bytes_eq(path, pat, plen) == 1 { 167 return r.routes[base + NX_ROUTE_F_HANDLER_ID] 168 } 169 } 170 } 171 } 172 i = i + 1 173 } 174 return 0 - 1 175} 176 177// ===== Suffix-after-prefix helper ================================= 178// 179// For prefix-matched routes, the handler typically wants the 180// remainder of the path (e.g. /api/character/42 matched by /api/ 181// yields character/42). Returns the byte offset within `path` 182// after the matched prefix length. Caller computed plen during 183// match; we just provide the convention. 184 185func nx_http_router_suffix_offset(matched_prefix_len: nx_int) -> nx_int { 186 return matched_prefix_len 187}