diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index c40a2e9dd..2b3ddb8cc 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -463,61 +463,66 @@ static const lib_pattern_t trpc_libraries[] = { {NULL, CBM_SVC_NONE, NULL}, }; -/* Method suffix type (used by both route registration and HTTP client tables) */ typedef struct { const char *suffix; const char *method; } method_suffix_t; +typedef struct { + const char *suffix; + const char *method; + bool allows_no_handler; +} route_reg_suffix_t; + /* Route registration method suffixes — matched on callee name. * These are methods on router objects that register handlers. */ -static const method_suffix_t route_reg_suffixes[] = { +static const route_reg_suffix_t route_reg_suffixes[] = { /* HTTP method registrations */ - {".GET", "GET"}, - {".Get", "GET"}, - {".get", "GET"}, - {".POST", "POST"}, - {".Post", "POST"}, - {".post", "POST"}, - {".PUT", "PUT"}, - {".Put", "PUT"}, - {".put", "PUT"}, - {".DELETE", "DELETE"}, - {".Delete", "DELETE"}, - {".delete", "DELETE"}, - {".PATCH", "PATCH"}, - {".Patch", "PATCH"}, - {".patch", "PATCH"}, + {".GET", "GET", false}, + {".Get", "GET", false}, + {".get", "GET", false}, + {".POST", "POST", false}, + {".Post", "POST", false}, + {".post", "POST", false}, + {".PUT", "PUT", false}, + {".Put", "PUT", false}, + {".put", "PUT", false}, + {".DELETE", "DELETE", false}, + {".Delete", "DELETE", false}, + {".delete", "DELETE", false}, + {".PATCH", "PATCH", false}, + {".Patch", "PATCH", false}, + {".patch", "PATCH", false}, /* Handle/HandleFunc (Go stdlib, gorilla) */ - {".Handle", "ANY"}, - {".HandleFunc", "ANY"}, - {".handle", "ANY"}, + {".Handle", "ANY", false}, + {".HandleFunc", "ANY", false}, + {".handle", "ANY", false}, /* Framework-specific route registration */ - {".Route", "ANY"}, - {".route", "ANY"}, - {".websocket_route", "ANY"}, - {".websocket", "ANY"}, - {"::get", "GET"}, - {"::post", "POST"}, - {"::put", "PUT"}, - {"::delete", "DELETE"}, - {"::patch", "PATCH"}, + {".Route", "ANY", false}, + {".route", "ANY", false}, + {".websocket_route", "ANY", true}, + {".websocket", "ANY", true}, + {"::get", "GET", false}, + {"::post", "POST", false}, + {"::put", "PUT", false}, + {"::delete", "DELETE", false}, + {"::patch", "PATCH", false}, /* Minimal API (C# ASP.NET) */ - {".MapGet", "GET"}, - {".MapPost", "POST"}, - {".MapPut", "PUT"}, - {".MapDelete", "DELETE"}, + {".MapGet", "GET", false}, + {".MapPost", "POST", false}, + {".MapPut", "PUT", false}, + {".MapDelete", "DELETE", false}, /* Router mounting / prefix registration (any method) */ - {".include_router", "ANY"}, - {".mount", "ANY"}, - {".add_url_rule", "ANY"}, - {".register_blueprint", "ANY"}, - {".use", "ANY"}, - {".register", "ANY"}, - {".add_route", "ANY"}, - {".add_api_route", "ANY"}, - {".add_api_websocket_route", "ANY"}, - {NULL, NULL}, + {".include_router", "ANY", true}, + {".mount", "ANY", false}, + {".add_url_rule", "ANY", true}, + {".register_blueprint", "ANY", true}, + {".use", "ANY", false}, + {".register", "ANY", false}, + {".add_route", "ANY", true}, + {".add_api_route", "ANY", true}, + {".add_api_websocket_route", "ANY", true}, + {NULL, NULL, false}, }; /* ── HTTP method inference from function/method name suffix ───── */ @@ -893,6 +898,20 @@ const char *cbm_service_pattern_route_method(const char *callee_name) { return NULL; } +bool cbm_service_pattern_route_suffix_allows_no_handler(const char *callee_name) { + if (!callee_name) { + return false; + } + size_t clen = strlen(callee_name); + for (int i = 0; route_reg_suffixes[i].suffix != NULL; i++) { + size_t slen = strlen(route_reg_suffixes[i].suffix); + if (clen >= slen && strcmp(callee_name + clen - slen, route_reg_suffixes[i].suffix) == 0) { + return route_reg_suffixes[i].allows_no_handler; + } + } + return false; +} + const char *cbm_service_pattern_broker(const char *resolved_qn) { if (!resolved_qn) { return NULL; diff --git a/internal/cbm/service_patterns.h b/internal/cbm/service_patterns.h index 0c2a9ba92..21677e690 100644 --- a/internal/cbm/service_patterns.h +++ b/internal/cbm/service_patterns.h @@ -53,6 +53,11 @@ const char *cbm_service_pattern_http_method(const char *callee_name); * Returns NULL if not a known route registration method. */ const char *cbm_service_pattern_route_method(const char *callee_name); +/* True when a route-registration suffix is specific enough to use without a + * handler argument in suffix-only fallback. Generic HTTP verbs like .get/.Get + * return false so HTTP client wrappers get normal resolution first. */ +bool cbm_service_pattern_route_suffix_allows_no_handler(const char *callee_name); + /* Classify a string literal as a genuine HTTP route path. Returns true for * real routes ("/api/orders", "/users/:id", "https://..."); false for file * paths ("/tmp/foo.md"), CLI slash-commands ("/ar:allow"), description strings diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index aecc76b54..1bbc107e0 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -330,7 +330,9 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, if (cbm_service_pattern_route_method(call->callee_name) != NULL) { const char *handler_ref = NULL; const char *route_path = cbm_pipeline_call_route_path_and_handler(call, &handler_ref); - if (route_path) { + if (route_path && + ((handler_ref && handler_ref[0] != '\0') || + cbm_service_pattern_route_suffix_allows_no_handler(call->callee_name))) { handle_route_registration(ctx, call, source_node, route_path, handler_ref, module_qn, imp_keys, imp_vals, imp_count); return SKIP_ONE; @@ -342,7 +344,9 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, if (cbm_service_pattern_route_method(call->callee_name) != NULL) { const char *handler_ref = NULL; const char *route_path = cbm_pipeline_call_route_path_and_handler(call, &handler_ref); - if (route_path) { + if (route_path && + ((handler_ref && handler_ref[0] != '\0') || + cbm_service_pattern_route_suffix_allows_no_handler(call->callee_name))) { handle_route_registration(ctx, call, source_node, route_path, handler_ref, module_qn, imp_keys, imp_vals, imp_count); return SKIP_ONE; diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index eb64e5a59..e735efbdc 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -1303,7 +1303,10 @@ static void emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, if (svc == CBM_SVC_ROUTE_REG) { const char *handler_ref = NULL; const char *route_path = cbm_pipeline_call_route_path_and_handler(call, &handler_ref); - if (route_path) { + bool has_route_handler = handler_ref && handler_ref[0] != '\0'; + bool handlerless_route_api = + cbm_service_pattern_route_suffix_allows_no_handler(call->callee_name); + if (route_path && (!suffix_only_route_reg || has_route_handler || handlerless_route_api)) { emit_route_registration(gbuf, source, call, route_path, handler_ref, module_qn, registry, main_gbuf, imp_keys, imp_vals, imp_count); return;