@@ -3602,6 +3602,105 @@ static void extract_route_from_decorators(CBMArena *a, TSNode func_node, const c
36023602 extract_route_from_annotations(a, func_node, source, spec, out_path, out_method);
36033603}
36043604
3605+ /* The string literal an annotation argument consists of, positional or as
3606+ * `value = "..."`. A concatenation, a constant or a template has no statically
3607+ * known value, so it yields a null node rather than one of its fragments. */
3608+ static TSNode annotation_arg_literal(TSNode arg) {
3609+ TSNode null_node = {0};
3610+ const char *kind = ts_node_type(arg);
3611+ if (strcmp(kind, "element_value_pair") == 0 || strcmp(kind, "value_argument") == 0) {
3612+ uint32_t nc = ts_node_named_child_count(arg);
3613+ if (nc == 0) {
3614+ return null_node;
3615+ }
3616+ arg = ts_node_named_child(arg, nc - SKIP_ONE);
3617+ }
3618+ if (!is_route_string_kind(ts_node_type(arg))) {
3619+ return null_node;
3620+ }
3621+ for (uint32_t i = 0; i < ts_node_named_child_count(arg); i++) {
3622+ const char *ck = ts_node_type(ts_node_named_child(arg, i));
3623+ if (strcmp(ck, "interpolated_identifier") == 0 ||
3624+ strcmp(ck, "interpolated_expression") == 0 || strcmp(ck, "string_interpolation") == 0) {
3625+ return null_node;
3626+ }
3627+ }
3628+ return arg;
3629+ }
3630+
3631+ /* Java interface_declaration, or a Kotlin class_declaration spelled with the
3632+ * `interface` keyword (the grammar has no separate node kind for it). */
3633+ static bool is_jvm_interface(TSNode class_node) {
3634+ if (ts_node_is_null(class_node)) {
3635+ return false;
3636+ }
3637+ const char *kind = ts_node_type(class_node);
3638+ if (strcmp(kind, "interface_declaration") == 0) {
3639+ return true;
3640+ }
3641+ if (strcmp(kind, "class_declaration") != 0) {
3642+ return false;
3643+ }
3644+ for (uint32_t i = 0; i < ts_node_child_count(class_node); i++) {
3645+ TSNode ch = ts_node_child(class_node, i);
3646+ if (!ts_node_is_named(ch) && strcmp(ts_node_type(ch), "interface") == 0) {
3647+ return true;
3648+ }
3649+ }
3650+ return false;
3651+ }
3652+
3653+ /* Retrofit declares a client's requests as @GET("v1/orders") / @POST(...) on an
3654+ * interface method. JAX-RS uses the same bare verb names, but its verbs are
3655+ * marker annotations (the path is a sibling @Path), so a verb that carries an
3656+ * argument is a Retrofit request, not a served endpoint. Returns true for such
3657+ * a method; *out_path is its path, or NULL when the argument is not a literal.
3658+ * Retrofit resolves a relative path against the base URL, so it is rooted the
3659+ * way JAX-RS paths are; a full URL is kept as written. */
3660+ static bool retrofit_client_route(CBMArena *a, TSNode func_node, const char *source,
3661+ const CBMLangSpec *spec, const char **out_path,
3662+ const char **out_method) {
3663+ *out_path = NULL;
3664+ *out_method = NULL;
3665+ TSNode wrappers[MAX_ATTR_WRAPPERS];
3666+ int wn = find_jvm_modifiers(func_node, spec->language, wrappers, MAX_ATTR_WRAPPERS_MINUS_1);
3667+ wrappers[wn++] = func_node;
3668+ for (int w = 0; w < wn; w++) {
3669+ uint32_t cc = ts_node_child_count(wrappers[w]);
3670+ for (uint32_t ci = 0; ci < cc; ci++) {
3671+ TSNode child = ts_node_child(wrappers[w], ci);
3672+ if (!cbm_kind_in_set(child, spec->decorator_node_types)) {
3673+ continue;
3674+ }
3675+ TSNode name_node = annotation_name_node(child);
3676+ if (ts_node_is_null(name_node)) {
3677+ continue;
3678+ }
3679+ char *name = cbm_node_text(a, name_node, source);
3680+ const char *method = annotation_route_method(name);
3681+ if (!method || strcmp(method, name) != 0) {
3682+ continue;
3683+ }
3684+ TSNode args = annotation_args_node(child);
3685+ if (ts_node_is_null(args) || ts_node_named_child_count(args) == 0) {
3686+ continue;
3687+ }
3688+ *out_method = method;
3689+ TSNode lit = annotation_arg_literal(ts_node_named_child(args, 0));
3690+ char *text = ts_node_is_null(lit) ? NULL : cbm_node_text(a, lit, source);
3691+ size_t len = text ? strlen(text) : 0;
3692+ if (len > PAIR_CHARS && (text[0] == '"' || text[0] == '\'')) {
3693+ char *path = cbm_arena_strndup(a, text + SKIP_CHAR, len - PAIR_CHARS);
3694+ *out_path = (!path || path[0] == '/' || strstr(path, "://"))
3695+ ? path
3696+ : cbm_arena_sprintf(a, "/%s", path);
3697+ }
3698+ return true;
3699+ }
3700+ }
3701+ return false;
3702+ }
3703+
36053704static const char *join_route_paths(CBMArena *a, const char *prefix, const char *path) {
36063705 if (!path || !path[0]) {
36073706 return prefix;
@@ -9321,6 +9420,17 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node,
93219420
93229421 def.decorators = extract_decorators(a, child, ctx->source, ctx->language, spec);
93239422 extract_route_from_decorators(a, child, ctx->source, spec, &def.route_path, &def.route_method);
9423+ const char *client_path = NULL;
9424+ const char *client_method = NULL;
9425+ /* Retrofit services must be interfaces; Pippo and Rapidoid put the path in
9426+ * @GET("/x") on a controller class, and those methods serve the route. */
9427+ if ((ctx->language == CBM_LANG_JAVA || ctx->language == CBM_LANG_KOTLIN) &&
9428+ is_jvm_interface(class_node) &&
9429+ retrofit_client_route(a, child, ctx->source, spec, &client_path, &client_method)) {
9430+ def.route_path = NULL;
9431+ def.client_route_path = client_path;
9432+ def.route_method = client_path ? client_method : NULL;
9433+ }
93249434 if (def.route_path && (ctx->language == CBM_LANG_JAVA || ctx->language == CBM_LANG_KOTLIN ||
93259435 ctx->language == CBM_LANG_SCALA)) {
93269436 const char *prefix = spring_class_route_prefix(a, class_node, ctx->source, spec);
0 commit comments