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}