• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valkyrjaio / sindri-java / 27918874223

21 Jun 2026 10:03PM UTC coverage: 95.037% (-5.0%) from 100.0%
27918874223

Pull #31

github

web-flow
Merge 6e0924991 into e3a362b61
Pull Request #31: [Generation] Fix HTTP routing data and framework-provider resolution

322 of 358 branches covered (89.94%)

Branch coverage included in aggregate %.

130 of 168 new or added lines in 5 files covered. (77.38%)

1095 of 1133 relevant lines covered (96.65%)

4.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.97
/src/main/java/io/sindri/ast/HttpRouteAttributeReader.java
1
/*
2
 * This file is part of the Sindri package.
3
 *
4
 * (c) Melech Mizrachi <melechmizrachi@gmail.com>
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9

10
package io.sindri.ast;
11

12
import com.github.javaparser.StaticJavaParser;
13
import com.github.javaparser.ast.CompilationUnit;
14
import com.github.javaparser.ast.body.MethodDeclaration;
15
import com.github.javaparser.ast.body.TypeDeclaration;
16
import com.github.javaparser.ast.expr.AnnotationExpr;
17
import com.github.javaparser.ast.expr.ArrayInitializerExpr;
18
import com.github.javaparser.ast.expr.Expression;
19
import com.github.javaparser.ast.expr.MemberValuePair;
20
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
21
import io.sindri.ast.abstract_.AstReader;
22
import io.sindri.ast.contract.HttpRouteAttributeReaderContract;
23
import io.sindri.ast.data.HandlerData;
24
import io.sindri.ast.data.HttpParameterData;
25
import io.sindri.ast.data.HttpRouteData;
26
import io.sindri.ast.data.result.HttpRouteAttributeResult;
27
import java.util.ArrayList;
28
import java.util.LinkedHashMap;
29
import java.util.List;
30
import java.util.Map;
31
import java.util.Optional;
32
import org.jspecify.annotations.Nullable;
33

34
public class HttpRouteAttributeReader extends AstReader
2✔
35
        implements HttpRouteAttributeReaderContract {
36

37
    private final HttpRouteParameterReader parameterReader = new HttpRouteParameterReader();
6✔
38

39
    @Override
40
    public HttpRouteAttributeResult readFile(String filePath) {
41
        CompilationUnit cu = parseFile(filePath);
4✔
42
        Map<String, String> importMap = buildImportMap(cu);
4✔
43
        String pkg = getPackageName(cu);
4✔
44

45
        TypeDeclaration<?> type =
2✔
46
                findType(cu)
3✔
47
                        .orElseThrow(() -> new RuntimeException("No type found in: " + filePath));
9✔
48

49
        Map<String, Expression> routeMap = new LinkedHashMap<>();
4✔
50
        Map<String, HttpRouteData> routeDataMap = new LinkedHashMap<>();
4✔
51

52
        for (MethodDeclaration method : type.getMethods()) {
11✔
53
            List<AnnotationExpr> routeAnnotations =
1✔
54
                    method.getAnnotations().stream()
3✔
55
                            .filter(a -> a.getNameAsString().equals("Route"))
6✔
56
                            .toList();
2✔
57

58
            if (routeAnnotations.isEmpty()) {
3✔
59
                continue;
1✔
60
            }
61

62
            Optional<AnnotationExpr> handlerAnnotation =
1✔
63
                    method.getAnnotations().stream()
3✔
64
                            .filter(a -> a.getNameAsString().equals("RouteHandler"))
6✔
65
                            .findFirst();
2✔
66

67
            String handlerClass = "";
2✔
68
            String handlerMethod = "";
2✔
69
            if (handlerAnnotation.isPresent()
3✔
70
                    && handlerAnnotation.get() instanceof NormalAnnotationExpr handler) {
9✔
71
                for (MemberValuePair pair : handler.getPairs()) {
11✔
72
                    if (pair.getNameAsString().equals("handlerClass")) {
5✔
73
                        handlerClass = extractClassExprFqn(pair.getValue(), importMap, pkg);
8✔
74
                    } else if (pair.getNameAsString().equals("handlerMethod")) {
5✔
75
                        handlerMethod = extractStringLiteral(pair.getValue());
5✔
76
                    }
77
                }
1✔
78
            }
79

80
            for (AnnotationExpr routeAnnotation : routeAnnotations) {
10✔
81
                if (!(routeAnnotation instanceof NormalAnnotationExpr normalRoute)) {
6✔
82
                    continue;
83
                }
84
                String path = "";
2✔
85
                String name = "";
2✔
86
                List<String> requestMethods = List.of("HEAD", "GET");
4✔
87
                for (MemberValuePair pair : normalRoute.getPairs()) {
11✔
88
                    switch (pair.getNameAsString()) {
9✔
89
                        case "path" -> path = extractStringLiteral(pair.getValue());
6✔
90
                        case "name" -> name = extractStringLiteral(pair.getValue());
6✔
91
                        case "requestMethods" ->
92
                                requestMethods = extractRequestMethods(pair.getValue());
6✔
93
                        default -> {}
94
                    }
95
                }
1✔
96
                HandlerData handler =
97
                        !handlerClass.isEmpty()
3✔
98
                                ? new HandlerData(handlerClass, handlerMethod)
6✔
99
                                : null;
2✔
100

101
                // A path containing a {placeholder} is a dynamic route: collect its parameters and
102
                // precompute the matching regex (so the cached data needs no runtime processing).
103
                boolean isDynamic = path.contains("{");
4✔
104
                List<HttpParameterData> parameters =
105
                        isDynamic
2✔
106
                                ? parameterReader.updateParameters(method, importMap, pkg)
7✔
107
                                : List.of();
2✔
108
                String regex = isDynamic ? computeRegex(path, name, parameters) : "";
10✔
109

110
                HttpRouteData data =
6✔
111
                        new HttpRouteData(
112
                                path,
113
                                name,
114
                                handler,
115
                                requestMethods,
116
                                List.of(),
1✔
117
                                List.of(),
1✔
118
                                List.of(),
1✔
119
                                List.of(),
1✔
120
                                List.of(),
8✔
121
                                null,
122
                                null,
123
                                isDynamic,
124
                                parameters,
125
                                regex);
126
                routeDataMap.put(name, data);
5✔
127
                routeMap.put(
12✔
128
                        name,
129
                        buildRouteValue(
1✔
130
                                path, name, handler, requestMethods, isDynamic, parameters, regex));
131
            }
1✔
132
        }
1✔
133
        return new HttpRouteAttributeResult(routeMap, routeDataMap);
6✔
134
    }
135

136
    /**
137
     * Build the {@code Supplier<RouteContract>} expression stored as a route's value, e.g. {@code
138
     * () -> new Route("/path", "name", Handler::method, List.of(RequestMethod.GET))}. Fully
139
     * qualified names are used so the generated file needs no extra imports.
140
     */
141
    private Expression buildRouteValue(
142
            String path,
143
            String name,
144
            @Nullable HandlerData handler,
145
            List<String> requestMethods,
146
            boolean isDynamic,
147
            List<HttpParameterData> parameters,
148
            String regex) {
149
        String handlerRef =
150
                handler != null ? handler.handlerClass() + "::" + handler.method() : "null";
10✔
151

152
        // The HEAD+GET default is exactly what the short Route/DynamicRoute constructors apply, so
153
        // it needs no explicit arguments; any other set needs the full constructor.
154
        String tail =
155
                requestMethods.equals(List.of("HEAD", "GET"))
6✔
156
                        ? ")"
2✔
157
                        : ", "
158
                                + buildRequestMethodsList(requestMethods)
3✔
159
                                + ", java.util.List.of(), java.util.List.of(), java.util.List.of(),"
2✔
160
                                + " java.util.List.of(), java.util.List.of(), null, null)";
161

162
        String head;
163
        if (isDynamic) {
2✔
164
            head =
4✔
165
                    "() -> new io.valkyrja.http.routing.data.DynamicRoute(\""
166
                            + path
167
                            + "\", \""
168
                            + name
169
                            + "\", \""
170
                            + escapeJava(regex)
3✔
171
                            + "\", "
172
                            + buildParameterList(parameters)
5✔
173
                            + ", "
174
                            + handlerRef;
175
        } else {
176
            head =
5✔
177
                    "() -> new io.valkyrja.http.routing.data.Route(\""
178
                            + path
179
                            + "\", \""
180
                            + name
181
                            + "\", "
182
                            + handlerRef;
183
        }
184

185
        return StaticJavaParser.parseExpression(head + tail);
5✔
186
    }
187

188
    /** Emit {@code List.of(new Parameter("name", "regex"), ...)} mirroring the framework collector. */
189
    private String buildParameterList(List<HttpParameterData> parameters) {
190
        StringBuilder sb = new StringBuilder("java.util.List.of(");
5✔
191
        for (int i = 0; i < parameters.size(); i++) {
8✔
192
            HttpParameterData parameter = parameters.get(i);
5✔
193
            sb.append("new io.valkyrja.http.routing.data.Parameter(\"")
4✔
194
                    .append(parameter.name())
3✔
195
                    .append("\", \"")
3✔
196
                    .append(escapeJava(parameter.regex()))
4✔
197
                    .append("\")");
2✔
198
            if (i < parameters.size() - 1) {
6!
NEW
199
                sb.append(", ");
×
200
            }
201
        }
202
        sb.append(")");
4✔
203
        return sb.toString();
3✔
204
    }
205

206
    /**
207
     * Precompute a dynamic route's match regex by running the real framework {@link
208
     * io.valkyrja.http.routing.processor.Processor}, so the cached regex is always identical to what
209
     * the framework would compute at runtime.
210
     */
211
    private String computeRegex(String path, String name, List<HttpParameterData> parameters) {
212
        List<io.valkyrja.http.routing.data.contract.ParameterContract> dataParameters =
4✔
213
                new java.util.ArrayList<>();
214
        for (HttpParameterData parameter : parameters) {
10✔
215
            dataParameters.add(
6✔
216
                    new io.valkyrja.http.routing.data.Parameter(
217
                            parameter.name(), parameter.regex()));
4✔
218
        }
1✔
219

220
        try {
221
            io.valkyrja.http.routing.data.DynamicRoute route =
9✔
222
                    new io.valkyrja.http.routing.data.DynamicRoute(
NEW
223
                            path, name, "", dataParameters, (container, matched) -> null);
×
224
            io.valkyrja.http.routing.data.contract.RouteContract processed =
4✔
225
                    new io.valkyrja.http.routing.processor.Processor().route(route);
2✔
226
            if (processed
3!
227
                    instanceof io.valkyrja.http.routing.data.contract.DynamicRouteContract dynamic) {
3✔
228
                return dynamic.getRegex();
3✔
229
            }
NEW
230
        } catch (RuntimeException e) {
×
231
            // Malformed dynamic route (e.g. a parameter with no matching placeholder) — skip it.
NEW
232
        }
×
233

NEW
234
        return "";
×
235
    }
236

237
    private String escapeJava(String value) {
238
        return value.replace("\\", "\\\\").replace("\"", "\\\"");
8✔
239
    }
240

241
    private String buildRequestMethodsList(List<String> methods) {
242
        StringBuilder sb = new StringBuilder("java.util.List.of(");
5✔
243
        for (int i = 0; i < methods.size(); i++) {
8✔
244
            // Tolerate string-literal request methods (e.g. "GET") by stripping the quotes so the
245
            // emitted RequestMethod constant stays a valid identifier.
246
            String method = methods.get(i).replace("\"", "");
8✔
247
            sb.append("io.valkyrja.http.message.enum_.RequestMethod.").append(method);
6✔
248
            if (i < methods.size() - 1) {
6!
NEW
249
                sb.append(", ");
×
250
            }
251
        }
252
        sb.append(")");
4✔
253
        return sb.toString();
3✔
254
    }
255

256
    private List<String> extractRequestMethods(Expression expr) {
257
        List<String> methods = new ArrayList<>();
4✔
258
        if (expr.isArrayInitializerExpr()) {
3✔
259
            ArrayInitializerExpr array = expr.asArrayInitializerExpr();
3✔
260
            for (Expression value : array.getValues()) {
11✔
261
                methods.add(extractEnumName(value));
6✔
262
            }
1✔
263
        } else {
1✔
264
            methods.add(extractEnumName(expr));
6✔
265
        }
266
        return methods;
2✔
267
    }
268

269
    private String extractEnumName(Expression expr) {
270
        if (expr.isFieldAccessExpr()) {
3✔
271
            return expr.asFieldAccessExpr().getNameAsString();
4✔
272
        }
273
        if (expr.isNameExpr()) {
3✔
274
            return expr.asNameExpr().getNameAsString();
4✔
275
        }
276
        return expr.toString();
3✔
277
    }
278
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc