Pharos 0.7.23
Modern Web Framework & Web App Hosting Service.
Loading...
Searching...
No Matches
native_dynamic_dispatch.c
Go to the documentation of this file.
1/**
2 * @file
3 * @brief Route dispatch for in-process dynamic-app request handling.
4 *
5 * Reads the app's route fixture JSON, matches an incoming request path
6 * against declared routes, and extracts parameterised segments.
7 */
8
10
13#include "native_support.h"
14
15#include <stdlib.h>
16#include <string.h>
17
18/* ------------------------------------------------------------------ */
19/* Route fixture path resolution */
20/* ------------------------------------------------------------------ */
21
22/**
23 * @brief Attempts to locate the route fixture path from the artifact directory.
24 *
25 * Reads the finalized artifact-local app manifest from @p artifact_dir and
26 * extracts the route_fixture path. Falls back to the app-root fixture
27 * path when the manifest stores a relative value.
28 *
29 * @param artifact_dir App artifact directory containing the manifest.
30 * @return Newly allocated route fixture path, or NULL. Caller must free.
31 */
32/**
33 * @brief Builds the conventional packaged-app route fixture path.
34 *
35 * Packaged app artifacts place the checked route fixture at
36 * @c fixtures/route-fixture.json under the artifact root.
37 *
38 * @param artifact_dir App artifact directory.
39 * @return Newly allocated path, or NULL on allocation failure.
40 */
41static char *construct_packaged_route_fixture_path(const char *artifact_dir) {
42 char *fixtures_dir;
43 char *result;
44
45 if (artifact_dir == NULL || artifact_dir[0] == '\0') {
46 return NULL;
47 }
48
49 fixtures_dir = path_join(artifact_dir, "fixtures");
50 if (fixtures_dir == NULL) {
51 return NULL;
52 }
53 result = path_join(fixtures_dir, "route-fixture.json");
54 free(fixtures_dir);
55 return result;
56}
57
58/**
59 * @brief Builds the framework-data route fixture fallback path.
60 *
61 * Dynamic-app artifact layouts may also publish route fixtures under
62 * @c data/framework/framework-<app_id>-route-fixture-v1.json.
63 *
64 * @param artifact_dir App artifact directory.
65 * @param app_id App identifier used in the framework fixture name.
66 * @return Newly allocated path, or NULL on allocation failure.
67 */
68static char *construct_direct_route_fixture_path(const char *artifact_dir, const char *app_id) {
69 char *dir = NULL;
70 char *filename = NULL;
71 char *result = NULL;
72 size_t filename_len;
73
74 if (artifact_dir == NULL || app_id == NULL || app_id[0] == '\0') {
75 return NULL;
76 }
77
78 dir = path_join(artifact_dir, "data/framework");
79 if (dir == NULL) {
80 return NULL;
81 }
82
83 /* Build filename: framework-<app_id>-route-fixture-v1.json */
84 filename_len = strlen("framework-") + strlen(app_id) + strlen("-route-fixture-v1.json") + 1;
85 filename = (char *)malloc(filename_len);
86 if (filename == NULL) {
87 free(dir);
88 return NULL;
89 }
90 snprintf(filename, filename_len, "framework-%s-route-fixture-v1.json", app_id);
91
92 result = path_join(dir, filename);
93 free(dir);
94 free(filename);
95 return result;
96}
97
98static char *resolve_route_fixture_path(const char *artifact_dir, const char *app_id) {
99 char *manifest_path = NULL;
100 char *manifest_text = NULL;
101 char *route_fixture = NULL;
102 char *app_root = NULL;
103
104 if (artifact_dir == NULL) {
105 return NULL;
106 }
107
108 /* Read the finalized artifact-local app manifest. */
109 manifest_path = path_join(artifact_dir, "pharos.app.json");
110 if (manifest_path == NULL || !path_exists(manifest_path)) {
111 free(manifest_path);
112 } else {
113 manifest_text = read_file_text(manifest_path);
114 free(manifest_path);
115 }
116
117 /* Extract route_fixture from the app manifest JSON */
118 if (manifest_text != NULL) {
119 yyjson_doc *doc = native_json_doc_load_text(manifest_text);
120 yyjson_val *root;
121 yyjson_val *fixtures;
122 free(manifest_text);
123 if (doc != NULL) {
124 root = yyjson_doc_get_root(doc);
125 fixtures = native_json_obj_get(root, "fixtures");
126 if (fixtures != NULL) {
127 route_fixture = native_json_obj_get_string_dup(fixtures, "route_fixture");
128 }
129 if (route_fixture == NULL) {
130 route_fixture = native_json_obj_get_string_dup(root, "route_fixture");
131 }
132 app_root = native_json_obj_get_string_dup(root, "app_root");
134 }
135 }
136
137 if (route_fixture != NULL) {
138 char *resolved;
139 /* Resolve relative route fixture path against the artifact directory. */
140 if (route_fixture[0] != '/') {
141 resolved = path_join(artifact_dir, route_fixture);
142 } else {
143 resolved = route_fixture;
144 route_fixture = NULL;
145 }
146 free(route_fixture);
147 free(app_root);
148 if (resolved != NULL && path_exists(resolved)) {
149 return resolved;
150 }
151 /* Manifest resolved path does not exist on disk; fall through to
152 * direct construction below. */
153 free(resolved);
154 } else {
155 free(app_root);
156 }
157
158 {
159 char *packaged_route_fixture = construct_packaged_route_fixture_path(artifact_dir);
160 if (packaged_route_fixture != NULL && path_exists(packaged_route_fixture)) {
161 return packaged_route_fixture;
162 }
163 free(packaged_route_fixture);
164 }
165
166 /* Fallback: construct the framework-data route fixture path directly from
167 * the app_id without relying on the manifest. This handles cases where
168 * the manifest is present but the route_fixture value resolves to a
169 * non-existent path or where the artifact uses the framework-data layout. */
170 {
171 char *framework_route_fixture = construct_direct_route_fixture_path(artifact_dir, app_id);
172 if (framework_route_fixture != NULL && path_exists(framework_route_fixture)) {
173 return framework_route_fixture;
174 }
175 free(framework_route_fixture);
176 }
177
178 return NULL;
179}
180
181/* ------------------------------------------------------------------ */
182/* Pattern matching */
183/* ------------------------------------------------------------------ */
184
185/**
186 * @brief Counts the number of path segments in a pattern or path.
187 */
188static int count_segments(const char *path) {
189 int count = 0;
190 const char *cursor;
191
192 if (path == NULL || path[0] == '\0') {
193 return 0;
194 }
195
196 cursor = path;
197 if (*cursor == '/') {
198 cursor++;
199 }
200 while (*cursor != '\0') {
201 count++;
202 while (*cursor != '\0' && *cursor != '/') {
203 cursor++;
204 }
205 if (*cursor == '/') {
206 cursor++;
207 }
208 }
209 return count;
210}
211
212/**
213 * @brief Extracts the N-th segment (0-indexed) from a path.
214 *
215 * @return Pointer into the original string (not allocated), or NULL.
216 */
217static const char *get_segment(const char *path, int index) {
218 const char *cursor;
219 int current = 0;
220
221 if (path == NULL || path[0] == '\0') {
222 return NULL;
223 }
224
225 cursor = path;
226 if (*cursor == '/') {
227 cursor++;
228 }
229
230 while (*cursor != '\0' && current < index) {
231 while (*cursor != '\0' && *cursor != '/') {
232 cursor++;
233 }
234 if (*cursor == '/') {
235 cursor++;
236 }
237 current++;
238 }
239
240 if (current != index || *cursor == '\0') {
241 return NULL;
242 }
243 return cursor;
244}
245
246/**
247 * @brief Returns the length of a segment starting at @p start.
248 */
249static size_t segment_length(const char *start) {
250 const char *end = start;
251 while (*end != '\0' && *end != '/') {
252 end++;
253 }
254 return (size_t)(end - start);
255}
256
257/**
258 * @brief Scores a route pattern by match specificity.
259 *
260 * Higher scores mean a more specific route. Exact literal segments outrank
261 * parameter segments, and parameter segments outrank wildcards. This lets the
262 * dispatcher prefer a static route like @c /appointments/diagnose/ over a
263 * broader parameterized route like @c /appointments/:appointment_id/ when both
264 * patterns match the same request path.
265 *
266 * @param pattern Route pattern from the fixture.
267 * @return Specificity score, or @c -1 when the pattern is missing.
268 */
269static int route_pattern_specificity_score(const char *pattern) {
270 int segment_count = 0;
271 int score = 0;
272 int index = 0;
273
274 if (pattern == NULL) {
275 return -1;
276 }
277
278 segment_count = count_segments(pattern);
279 for (index = 0; index < segment_count; index++) {
280 const char *segment = get_segment(pattern, index);
281 size_t segment_len = 0;
282 if (segment == NULL) {
283 continue;
284 }
285 segment_len = segment_length(segment);
286 score += 1;
287 if (segment_len == 1 && segment[0] == '*') {
288 continue;
289 }
290 if (segment_len > 0 && segment[0] == ':') {
291 score += 10;
292 continue;
293 }
294 score += 100;
295 }
296 return score;
297}
298
299/**
300 * @brief Tests whether a pattern matches a concrete path.
301 *
302 * Supports:
303 * - Exact segment matches (e.g. @c /accounts/login/ )
304 * - Parameterised segments prefixed with @c : (e.g. @c /businesses/:slug/ )
305 * - Wildcard segment @c * (matches any single segment)
306 */
307static int pattern_matches(const char *pattern, const char *path) {
308 int pat_segs;
309 int path_segs;
310 int i;
311
312 if (pattern == NULL || path == NULL) {
313 return 0;
314 }
315
316 pat_segs = count_segments(pattern);
317 path_segs = count_segments(path);
318
319 if (pat_segs != path_segs) {
320 return 0;
321 }
322
323 for (i = 0; i < pat_segs; i++) {
324 const char *pat_seg = get_segment(pattern, i);
325 const char *path_seg = get_segment(path, i);
326 size_t pat_len;
327 size_t path_len;
328
329 if (pat_seg == NULL || path_seg == NULL) {
330 return 0;
331 }
332
333 pat_len = segment_length(pat_seg);
334 path_len = segment_length(path_seg);
335
336 /* Parameterised segment: matches any non-empty segment */
337 if (pat_len > 0 && pat_seg[0] == ':') {
338 if (path_len == 0) {
339 return 0;
340 }
341 continue;
342 }
343
344 /* Wildcard segment: matches any single segment */
345 if (pat_len == 1 && pat_seg[0] == '*') {
346 continue;
347 }
348
349 /* Exact match */
350 if (pat_len != path_len) {
351 return 0;
352 }
353 if (strncmp(pat_seg, path_seg, pat_len) != 0) {
354 return 0;
355 }
356 }
357
358 return 1;
359}
360
361/* ------------------------------------------------------------------ */
362/* Public API */
363/* ------------------------------------------------------------------ */
364
366 const char *route_fixture_path,
367 const char *method,
368 const char *path,
369 NativeDynamicRouteMatch **match_out
370) {
371 yyjson_doc *doc = NULL;
372 yyjson_val *root = NULL;
373 yyjson_val *routes = NULL;
374 NativeDynamicRouteMatch *best_match = NULL;
375 NativeDynamicRouteMatch *fallback_match = NULL;
376 const char *preferred_kind = NULL;
377 int best_score = -1;
378 int fallback_score = -1;
379 size_t route_count;
380 size_t i;
381
382 if (match_out != NULL) {
383 *match_out = NULL;
384 }
385
386 if (route_fixture_path == NULL || path == NULL || match_out == NULL) {
387 return -1;
388 }
389
390 if (method != NULL) {
391 if (strcmp(method, "GET") == 0 || strcmp(method, "HEAD") == 0 || strcmp(method, "OPTIONS") == 0) {
392 preferred_kind = "page";
393 } else if (strcmp(method, "POST") == 0 || strcmp(method, "PUT") == 0 || strcmp(method, "PATCH") == 0 || strcmp(method, "DELETE") == 0) {
394 preferred_kind = "action";
395 }
396 }
397
398 doc = native_json_doc_load_file(route_fixture_path);
399 if (doc == NULL) {
400 return -1;
401 }
402
403 root = yyjson_doc_get_root(doc);
404 routes = native_json_obj_get_array(root, "routes");
405 if (routes == NULL) {
407 return 1; /* no routes found */
408 }
409
410 route_count = yyjson_arr_size(routes);
411 for (i = 0; i < route_count; i++) {
412 yyjson_val *route = yyjson_arr_get(routes, i);
413 char *route_path = NULL;
414 const char *route_id = NULL;
415 const char *kind = NULL;
416 NativeDynamicRouteMatch *match = NULL;
417 int score = -1;
418
419 if (route == NULL) {
420 continue;
421 }
422
423 route_path = native_json_obj_get_string_dup(route, "path");
424 if (route_path == NULL) {
425 continue;
426 }
427
428 if (!pattern_matches(route_path, path)) {
429 free(route_path);
430 continue;
431 }
432
433 match = (NativeDynamicRouteMatch *)calloc(1, sizeof(NativeDynamicRouteMatch));
434 if (match == NULL) {
435 free(route_path);
438 return -1;
439 }
440
441 route_id = yyjson_get_str(native_json_obj_get(route, "route_id"));
442 kind = yyjson_get_str(native_json_obj_get(route, "kind"));
443
444 match->route_id = strdup(route_id != NULL ? route_id : "");
445 match->path = route_path; /* transferred ownership */
446 match->kind = strdup(kind != NULL ? kind : "page");
447 match->page_id = native_json_obj_get_string_dup(route, "page_id");
448 match->action_id = native_json_obj_get_string_dup(route, "action_id");
449 match->layout_id = native_json_obj_get_string_dup(route, "layout_id");
450 match->auth_gate = native_json_obj_get_string_dup(route, "auth_gate");
451 match->policy_gate = native_json_obj_get_string_dup(route, "policy_gate");
452
453 score = route_pattern_specificity_score(route_path);
454
455 if (preferred_kind == NULL || (kind != NULL && strcmp(kind, preferred_kind) == 0)) {
456 if (best_match == NULL || score > best_score) {
458 best_match = match;
459 best_score = score;
460 } else {
462 }
463 continue;
464 }
465
466 if (fallback_match == NULL || score > fallback_score) {
468 fallback_match = match;
469 fallback_score = score;
470 } else {
472 }
473 }
474
476 if (best_match != NULL) {
478 *match_out = best_match;
479 return 0;
480 }
481 if (fallback_match != NULL) {
482 *match_out = fallback_match;
483 return 0;
484 }
485 return 1; /* no match */
486}
487
489 if (match == NULL) {
490 return;
491 }
492 free(match->route_id);
493 free(match->path);
494 free(match->kind);
495 free(match->page_id);
496 free(match->action_id);
497 free(match->layout_id);
498 free(match->auth_gate);
499 free(match->policy_gate);
500 free(match);
501}
502
504 const char *artifact_dir,
505 const char *method,
506 const char *path
507) {
508 (void)method;
509 /* Delegate to the existing dispatch table helper which already knows
510 * how to resolve API routes against the dispatch-table.json. */
511 /* Note: native_dispatch_response_file_for_request is declared in
512 * native_dispatch_table.h but the current implementation only matches
513 * paths starting with "/api/". For the dynamic runtime we expand this
514 * to also check the full dispatch table for non-API paths. */
515 return native_dispatch_response_file_for_request(artifact_dir, method, path);
516}
517
519 const char *pattern,
520 const char *path,
521 const char *param_name
522) {
523 int pat_segs;
524 int path_segs;
525 int i;
526 char param_marker[256];
527
528 if (pattern == NULL || path == NULL || param_name == NULL) {
529 return NULL;
530 }
531
532 /* Build the expected parameter token, e.g. ":business_slug" */
533 if (snprintf(param_marker, sizeof(param_marker), ":%s", param_name) >= (int)sizeof(param_marker)) {
534 return NULL;
535 }
536
537 pat_segs = count_segments(pattern);
538 path_segs = count_segments(path);
539
540 if (pat_segs != path_segs) {
541 return NULL;
542 }
543
544 for (i = 0; i < pat_segs; i++) {
545 const char *pat_seg = get_segment(pattern, i);
546 const char *path_seg = get_segment(path, i);
547 size_t pat_len;
548 size_t seg_len;
549
550 if (pat_seg == NULL || path_seg == NULL) {
551 return NULL;
552 }
553
554 pat_len = segment_length(pat_seg);
555 seg_len = segment_length(path_seg);
556
557 if (pat_len > 0 && strncmp(pat_seg, param_marker, pat_len) == 0 && pat_len == strlen(param_marker)) {
558 return dup_range(path_seg, seg_len);
559 }
560 }
561
562 return NULL;
563}
564
565char *native_dynamic_dispatch_resolve_route_fixture(const char *artifact_dir, const char *app_id) {
566 return resolve_route_fixture_path(artifact_dir, app_id);
567}
char * native_dispatch_response_file_for_request(const char *root, const char *method, const char *path)
Resolves the response artifact file for one request using the dispatch table.
Declares dispatch-table request path helpers.
static int pattern_matches(const char *pattern, const char *path)
Tests whether a pattern matches a concrete path.
static const char * get_segment(const char *path, int index)
Extracts the N-th segment (0-indexed) from a path.
static int route_pattern_specificity_score(const char *pattern)
Scores a route pattern by match specificity.
static char * construct_direct_route_fixture_path(const char *artifact_dir, const char *app_id)
Builds the framework-data route fixture fallback path.
char * native_dynamic_dispatch_response_file(const char *artifact_dir, const char *method, const char *path)
Resolves the response artifact file for a matched page route.
int native_dynamic_dispatch_match(const char *route_fixture_path, const char *method, const char *path, NativeDynamicRouteMatch **match_out)
Matches a request path against the route fixture for an app.
static char * construct_packaged_route_fixture_path(const char *artifact_dir)
Attempts to locate the route fixture path from the artifact directory.
static size_t segment_length(const char *start)
Returns the length of a segment starting at start.
static char * resolve_route_fixture_path(const char *artifact_dir, const char *app_id)
char * native_dynamic_dispatch_resolve_route_fixture(const char *artifact_dir, const char *app_id)
Resolves the route fixture path from the artifact directory.
char * native_dynamic_dispatch_extract_param(const char *pattern, const char *path, const char *param_name)
Returns the URL path parameter value extracted from a matched route.
static int count_segments(const char *path)
Counts the number of path segments in a pattern or path.
void native_dynamic_dispatch_free_match(NativeDynamicRouteMatch *match)
Frees a route match returned by native_dynamic_dispatch_match().
Route dispatch for in-process dynamic-app request handling.
char * path_join(const char *left, const char *right)
Joins two path segments using the native path separator.
char * dup_range(const char *start, size_t len)
Duplicates a byte range into a newly allocated NUL-terminated string.
char * read_file_text(const char *path)
Reads an entire file into a newly allocated text buffer.
int path_exists(const char *path)
Tests whether a filesystem path currently exists.
Declares shared low-level support helpers used across the Pharos native runtime.
yyjson_doc * native_json_doc_load_file(const char *path)
Parses a JSON file into an owned yyjson document.
void native_json_doc_free(yyjson_doc *doc)
Releases a document returned by the native yyjson helpers.
yyjson_doc * native_json_doc_load_text(const char *text)
Parses JSON text into an owned yyjson document.
char * native_json_obj_get_string_dup(yyjson_val *obj, const char *key)
Duplicates a string-valued key from a JSON object.
yyjson_val * native_json_obj_get_array(yyjson_val *obj, const char *key)
Looks up an array-valued key from a JSON object value.
yyjson_val * native_json_obj_get(yyjson_val *obj, const char *key)
Looks up a key from a JSON object value.
Declares shared helper wrappers around vendored yyjson parsing and serialization APIs.
Describes one matched route from the route fixture.
yyjson_api_inline yyjson_val * yyjson_arr_get(const yyjson_val *arr, size_t idx)
Definition yyjson.h:5603
yyjson_api_inline yyjson_val * yyjson_doc_get_root(const yyjson_doc *doc)
Definition yyjson.h:5323
yyjson_api_inline size_t yyjson_arr_size(const yyjson_val *arr)
Definition yyjson.h:5599
yyjson_api_inline const char * yyjson_get_str(const yyjson_val *val)
Definition yyjson.h:5469