mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(routes): canonicalize path parameters so client calls match handlers
Route QNs (__route__METHOD__/path) are the rendezvous key for joining a client
HTTP_CALLS to the server handler that serves it, but they were built from the raw
path. Frameworks write path parameters with different placeholder syntaxes
(":id" Express/clients, "{id}" Axum/Spring/OpenAPI, "<id>" Flask/Rocket), so a
client call and its handler produced different QNs and never joined. Static
routes joined; parameterized routes (most of a REST surface) did not.
Add cbm_route_canon_path() which collapses each parameter token (":name",
"{name}", "<name>", "${...}") to a single "{}" token, and apply it to the path
at every HTTP-route QN construction site (pass_route_nodes, pass_parallel,
pass_calls, pass_cross_repo). Broker/async/infra QNs are left untouched. Only the
QN (identity/dedup key) is canonicalized; the Route node display name keeps the
original path. Parameter names are intentionally discarded so the same logical
endpoint matches across services that name the variable differently.
Adds tests/test_route_canon.c (11 cases). Fixes #505.
Signed-off-by: imateusdev <imateusdev@gmail.com>
This commit is contained in:
1 parent
684e35b0e2
commit
8b813675be
8 files changed
+244
-14
No files matched your search
+1
-1
@@ -329,7 +329,7 @@ TEST_DISCOVER_SRCS = \
|
||||
|
||||
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c
|
||||
|
||||
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c
|
||||
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c
|
||||
|
||||
TEST_WATCHER_SRCS = tests/test_watcher.c
|
||||
|
||||
|
||||
@@ -187,8 +187,9 @@ static void handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *ca
|
||||
const char **imp_keys, const char **imp_vals, int imp_count) {
|
||||
const char *method = cbm_service_pattern_route_method(call->callee_name);
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
char cpath[CBM_SZ_256];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method ? method : "ANY",
|
||||
call->first_string_arg);
|
||||
cbm_route_canon_path(call->first_string_arg, cpath, sizeof(cpath)));
|
||||
char route_props[CBM_SZ_256];
|
||||
snprintf(route_props, sizeof(route_props), "{\"method\":\"%s\"}", method ? method : "ANY");
|
||||
int64_t route_id = cbm_gbuf_upsert_node(ctx->gbuf, "Route", call->first_string_arg, route_qn,
|
||||
@@ -225,12 +226,15 @@ static int64_t create_svc_route_node(cbm_pipeline_ctx_t *ctx, const char *url, c
|
||||
const char *method, const char *broker) {
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
const char *prefix;
|
||||
char cpath[CBM_SZ_256];
|
||||
const char *qpath = url;
|
||||
if (svc == CBM_SVC_HTTP) {
|
||||
prefix = method ? method : "ANY";
|
||||
qpath = cbm_route_canon_path(url, cpath, sizeof(cpath));
|
||||
} else {
|
||||
prefix = broker ? broker : "async";
|
||||
}
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", prefix, url);
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", prefix, qpath);
|
||||
const char *rp;
|
||||
if (svc == CBM_SVC_HTTP) {
|
||||
rp = method ? method : "{}";
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* get a CROSS_* edge so the link is visible from either side.
|
||||
*/
|
||||
#include "pipeline/pass_cross_repo.h"
|
||||
#include "pipeline/pipeline_internal.h" // cbm_route_canon_path
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/platform.h"
|
||||
@@ -283,10 +284,13 @@ static int match_http_routes(cbm_store_t *src_store, const char *src_project,
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Build the expected Route QN in the target project */
|
||||
/* Build the expected Route QN in the target project (param-canonicalized
|
||||
* so client url_path matches the server handler regardless of framework
|
||||
* placeholder syntax). */
|
||||
char route_qn[CR_QN_BUF];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method[0] ? method : "ANY",
|
||||
url_path);
|
||||
char cpath[CBM_SZ_256];
|
||||
const char *curl = cbm_route_canon_path(url_path, cpath, sizeof(cpath));
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method[0] ? method : "ANY", curl);
|
||||
|
||||
char handler_name[CBM_SZ_256] = {0};
|
||||
char handler_file[CBM_SZ_512] = {0};
|
||||
@@ -295,7 +299,7 @@ static int match_http_routes(cbm_store_t *src_store, const char *src_project,
|
||||
handler_file, sizeof(handler_file));
|
||||
if (handler_id == 0) {
|
||||
/* Try without method (ANY) */
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s", url_path);
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s", curl);
|
||||
handler_id = find_route_handler(tgt_store, route_qn, handler_name, sizeof(handler_name),
|
||||
handler_file, sizeof(handler_file));
|
||||
}
|
||||
|
||||
@@ -502,7 +502,9 @@ static void insert_def_into_gbuf(extract_worker_state_t *ws, const cbm_file_info
|
||||
if (def->route_path && def->route_path[0] != '\0') {
|
||||
const char *rm = def->route_method ? def->route_method : "ANY";
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", rm, def->route_path);
|
||||
char cpath[CBM_SZ_256];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", rm,
|
||||
cbm_route_canon_path(def->route_path, cpath, sizeof(cpath)));
|
||||
char rprops[CBM_SZ_256];
|
||||
snprintf(rprops, sizeof(rprops), "{\"method\":\"%s\",\"source\":\"decorator\"}", rm);
|
||||
int64_t route_id =
|
||||
@@ -1210,12 +1212,15 @@ static int64_t build_service_route(cbm_gbuf_t *gbuf, const char *arg, const char
|
||||
const char *broker, cbm_svc_kind_t svc) {
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
const char *prefix;
|
||||
char cpath[CBM_SZ_256];
|
||||
const char *qpath = arg;
|
||||
if (svc == CBM_SVC_HTTP) {
|
||||
prefix = method ? method : "ANY";
|
||||
qpath = cbm_route_canon_path(arg, cpath, sizeof(cpath));
|
||||
} else {
|
||||
prefix = broker ? broker : "async";
|
||||
}
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", prefix, arg);
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", prefix, qpath);
|
||||
char route_props[CBM_SZ_256];
|
||||
if (method) {
|
||||
snprintf(route_props, sizeof(route_props), "{\"method\":\"%s\"}", method);
|
||||
@@ -1291,7 +1296,9 @@ static void emit_route_registration(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *sou
|
||||
const char **ik, const char **iv, int ic) {
|
||||
const char *method = cbm_service_pattern_route_method(call->callee_name);
|
||||
char rqn[CBM_ROUTE_QN_SIZE];
|
||||
snprintf(rqn, sizeof(rqn), "__route__%s__%s", method ? method : "ANY", route_path);
|
||||
char cpath[CBM_SZ_256];
|
||||
snprintf(rqn, sizeof(rqn), "__route__%s__%s", method ? method : "ANY",
|
||||
cbm_route_canon_path(route_path, cpath, sizeof(cpath)));
|
||||
char rp[CBM_SZ_256];
|
||||
snprintf(rp, sizeof(rp), "{\"method\":\"%s\"}", method ? method : "ANY");
|
||||
int64_t rid = cbm_gbuf_upsert_node(gbuf, "Route", route_path, rqn, "", 0, 0, rp);
|
||||
@@ -1384,7 +1391,9 @@ static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
|
||||
continue;
|
||||
}
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s", norm);
|
||||
char cpath[CBM_SZ_256];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s",
|
||||
cbm_route_canon_path(norm, cpath, sizeof(cpath)));
|
||||
int64_t route_id = cbm_gbuf_upsert_node(gbuf, "Route", norm, route_qn, "", 0, 0,
|
||||
"{\"source\":\"arg_url\"}");
|
||||
char esc_c[CBM_SZ_256];
|
||||
|
||||
@@ -39,6 +39,95 @@ enum {
|
||||
|
||||
bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *callee_name);
|
||||
|
||||
/* True for characters that may appear in a ":name" route parameter. */
|
||||
static inline bool is_route_ident_char(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
|
||||
}
|
||||
|
||||
/* Canonicalize route-path parameter placeholders to a single "{}" token so that
|
||||
* client call sites and server handlers rendezvous on the same Route QN
|
||||
* regardless of framework syntax. Each parameter token collapses to "{}":
|
||||
*
|
||||
* :name Express / React-Router / Rails / typical JS API clients
|
||||
* {name} Axum / Spring / OpenAPI / ASP.NET
|
||||
* <name> Flask / Rocket (incl. typed "<int:id>")
|
||||
* ${...} JS template interpolation captured into the path
|
||||
*
|
||||
* Parameter names are intentionally discarded so the same logical endpoint
|
||||
* matches across services that name the path variable differently. Static path
|
||||
* text is copied verbatim; the result never exceeds the input length. */
|
||||
const char *cbm_route_canon_path(const char *in, char *out, size_t out_sz) {
|
||||
if (out == NULL || out_sz == 0) {
|
||||
/* WHY: cppcheck value-flow follows a caller that passes an as-yet
|
||||
* uninitialized output buffer (e.g. `char cpath[256];`) and flags this
|
||||
* `return out` as uninitvar. False positive: we only return the pointer
|
||||
* here and never read the buffer; returning it (NULL, or a buffer too
|
||||
* small to write into) is the intended early-out contract. */
|
||||
// cppcheck-suppress uninitvar
|
||||
return out;
|
||||
}
|
||||
if (in == NULL) {
|
||||
out[0] = '\0';
|
||||
return out;
|
||||
}
|
||||
const size_t last = out_sz - 1;
|
||||
size_t oi = 0;
|
||||
size_t i = 0;
|
||||
while (in[i] != '\0' && oi < last) {
|
||||
char c = in[i];
|
||||
bool at_seg_start = (oi == 0) || (out[oi - 1] == '/');
|
||||
bool is_param = false;
|
||||
|
||||
if (c == ':' && at_seg_start && is_route_ident_char(in[i + 1])) {
|
||||
i++;
|
||||
while (in[i] != '\0' && is_route_ident_char(in[i])) {
|
||||
i++;
|
||||
}
|
||||
is_param = true;
|
||||
} else if (c == '{') {
|
||||
i++;
|
||||
while (in[i] != '\0' && in[i] != '}' && in[i] != '/') {
|
||||
i++;
|
||||
}
|
||||
if (in[i] == '}') {
|
||||
i++;
|
||||
}
|
||||
is_param = true;
|
||||
} else if (c == '<') {
|
||||
i++;
|
||||
while (in[i] != '\0' && in[i] != '>' && in[i] != '/') {
|
||||
i++;
|
||||
}
|
||||
if (in[i] == '>') {
|
||||
i++;
|
||||
}
|
||||
is_param = true;
|
||||
} else if (c == '$' && in[i + 1] == '{') {
|
||||
i += 2;
|
||||
while (in[i] != '\0' && in[i] != '}' && in[i] != '/') {
|
||||
i++;
|
||||
}
|
||||
if (in[i] == '}') {
|
||||
i++;
|
||||
}
|
||||
is_param = true;
|
||||
}
|
||||
|
||||
if (is_param) {
|
||||
if (oi + 2 > last) {
|
||||
break;
|
||||
}
|
||||
out[oi++] = '{';
|
||||
out[oi++] = '}';
|
||||
continue;
|
||||
}
|
||||
out[oi++] = c;
|
||||
i++;
|
||||
}
|
||||
out[oi] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Extract a JSON string value by key from properties.
|
||||
* Returns pointer into buf (caller provides buffer). NULL if not found. */
|
||||
static const char *json_extract(const char *json, const char *key, char *buf, int bufsz) {
|
||||
@@ -105,7 +194,9 @@ static void route_edge_visitor(const cbm_gbuf_edge_t *edge, void *userdata) {
|
||||
/* Build Route QN */
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
if (strcmp(edge->type, "HTTP_CALLS") == 0) {
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method ? method : "ANY", url);
|
||||
char cpath[CBM_SZ_256];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method ? method : "ANY",
|
||||
cbm_route_canon_path(url, cpath, sizeof(cpath)));
|
||||
} else {
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", broker ? broker : "async", url);
|
||||
}
|
||||
@@ -360,7 +451,9 @@ static int ensure_one_decorator_route(cbm_gbuf_t *gb, const cbm_gbuf_node_t *fun
|
||||
extract_json_prop(func->properties_json, "route_method", method, sizeof(method));
|
||||
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method, path);
|
||||
char cpath[CBM_SZ_256];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method,
|
||||
cbm_route_canon_path(path, cpath, sizeof(cpath)));
|
||||
const cbm_gbuf_node_t *existing = cbm_gbuf_find_by_qn(gb, route_qn);
|
||||
|
||||
char rprops[CBM_SZ_256];
|
||||
@@ -1056,7 +1149,9 @@ static void sveltekit_file_visitor(const cbm_gbuf_node_t *node, void *userdata)
|
||||
}
|
||||
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method, route_path);
|
||||
char cpath[CBM_SZ_256];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method,
|
||||
cbm_route_canon_path(route_path, cpath, sizeof(cpath)));
|
||||
char route_props[CBM_SZ_256];
|
||||
snprintf(route_props, sizeof(route_props),
|
||||
"{\"method\":\"%s\",\"framework\":\"sveltekit\"}", method);
|
||||
|
||||
@@ -25,6 +25,14 @@
|
||||
/* Route node QN buffer size (must fit __route__METHOD__/full/url/path) */
|
||||
#define CBM_ROUTE_QN_SIZE 768
|
||||
|
||||
/* Canonicalize route-path parameter placeholders (":id", "{id}", "<id>",
|
||||
* "${...}") to a single "{}" token so that client call sites and server
|
||||
* handlers rendezvous on the same Route QN regardless of framework syntax.
|
||||
* Parameter names are intentionally discarded ("/u/{id}" and "/u/{slug}" both
|
||||
* canonicalize to "/u/{}"). The result never exceeds the input length, so
|
||||
* out_sz >= strlen(in) + 1 always suffices. Returns out. */
|
||||
const char *cbm_route_canon_path(const char *in, char *out, size_t out_sz);
|
||||
|
||||
/* Time unit conversions */
|
||||
#define CBM_NS_PER_SEC 1000000000LL
|
||||
#define CBM_US_PER_SEC 1000000LL
|
||||
|
||||
@@ -39,6 +39,7 @@ extern void suite_graph_buffer(void);
|
||||
extern void suite_registry(void);
|
||||
extern void suite_pipeline(void);
|
||||
extern void suite_fqn(void);
|
||||
extern void suite_route_canon(void);
|
||||
extern void suite_path_alias(void);
|
||||
extern void suite_watcher(void);
|
||||
extern void suite_lz4(void);
|
||||
@@ -151,6 +152,7 @@ int main(void) {
|
||||
RUN_SUITE(registry);
|
||||
RUN_SUITE(pipeline);
|
||||
RUN_SUITE(fqn);
|
||||
RUN_SUITE(route_canon);
|
||||
RUN_SUITE(path_alias);
|
||||
|
||||
/* Watcher (M10) */
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* test_route_canon.c — Unit tests for cbm_route_canon_path().
|
||||
*
|
||||
* Verifies that framework-specific route-parameter placeholder syntaxes
|
||||
* (":id", "{id}", "<id>", "${id}") collapse to a single "{}" token so that a
|
||||
* client call site and a server handler rendezvous on the same Route QN
|
||||
* regardless of the language/framework that produced each side.
|
||||
* See src/pipeline/pass_route_nodes.c.
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "pipeline/pipeline_internal.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
TEST(route_canon_static_unchanged) {
|
||||
char b[128];
|
||||
ASSERT_STR_EQ(cbm_route_canon_path("/products/categories", b, sizeof(b)),
|
||||
"/products/categories");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(route_canon_colon_param) {
|
||||
char b[128];
|
||||
ASSERT_STR_EQ(cbm_route_canon_path("/players/:id", b, sizeof(b)), "/players/{}");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(route_canon_brace_param) {
|
||||
char b[128];
|
||||
ASSERT_STR_EQ(cbm_route_canon_path("/players/{id}", b, sizeof(b)), "/players/{}");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The core invariant: Axum "{id}" and a JS client ":id" must converge. */
|
||||
TEST(route_canon_colon_and_brace_converge) {
|
||||
char a[128];
|
||||
char c[128];
|
||||
cbm_route_canon_path("/clients/{id}/authorized-users", a, sizeof(a));
|
||||
cbm_route_canon_path("/clients/:clientId/authorized-users", c, sizeof(c));
|
||||
ASSERT_STR_EQ(a, c);
|
||||
ASSERT_STR_EQ(a, "/clients/{}/authorized-users");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Parameter names are intentionally discarded ("{id}" == ":requestId"). */
|
||||
TEST(route_canon_param_name_agnostic) {
|
||||
char a[128];
|
||||
char c[128];
|
||||
cbm_route_canon_path("/link-requests/{id}/status", a, sizeof(a));
|
||||
cbm_route_canon_path("/link-requests/:requestId/status", c, sizeof(c));
|
||||
ASSERT_STR_EQ(a, c);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(route_canon_angle_param) {
|
||||
char b[128];
|
||||
ASSERT_STR_EQ(cbm_route_canon_path("/users/<int:id>", b, sizeof(b)), "/users/{}");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(route_canon_template_interpolation) {
|
||||
char b[128];
|
||||
ASSERT_STR_EQ(cbm_route_canon_path("/players/${playerId}", b, sizeof(b)), "/players/{}");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(route_canon_multiple_params) {
|
||||
char b[128];
|
||||
ASSERT_STR_EQ(cbm_route_canon_path("/orders/{id}/items/{itemIndex}", b, sizeof(b)),
|
||||
"/orders/{}/items/{}");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A ':' that is not at a segment start is literal, not a route parameter. */
|
||||
TEST(route_canon_colon_mid_segment_is_literal) {
|
||||
char b[128];
|
||||
ASSERT_STR_EQ(cbm_route_canon_path("/a/b:c", b, sizeof(b)), "/a/b:c");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(route_canon_null_and_empty) {
|
||||
char b[8];
|
||||
ASSERT_STR_EQ(cbm_route_canon_path("", b, sizeof(b)), "");
|
||||
ASSERT_STR_EQ(cbm_route_canon_path(NULL, b, sizeof(b)), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A tight output buffer must still yield a bounded, NUL-terminated string. */
|
||||
TEST(route_canon_truncation_safe) {
|
||||
char b[6];
|
||||
const char *r = cbm_route_canon_path("/players/:id", b, sizeof(b));
|
||||
ASSERT(strlen(r) < sizeof(b));
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(route_canon) {
|
||||
RUN_TEST(route_canon_static_unchanged);
|
||||
RUN_TEST(route_canon_colon_param);
|
||||
RUN_TEST(route_canon_brace_param);
|
||||
RUN_TEST(route_canon_colon_and_brace_converge);
|
||||
RUN_TEST(route_canon_param_name_agnostic);
|
||||
RUN_TEST(route_canon_angle_param);
|
||||
RUN_TEST(route_canon_template_interpolation);
|
||||
RUN_TEST(route_canon_multiple_params);
|
||||
RUN_TEST(route_canon_colon_mid_segment_is_literal);
|
||||
RUN_TEST(route_canon_null_and_empty);
|
||||
RUN_TEST(route_canon_truncation_safe);
|
||||
}
|
||||
Reference in new issue
Block a user