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

valkyrjaio / sindri-java / 30419723982

29 Jul 2026 03:29AM UTC coverage: 95.429% (-4.5%) from 99.952%
30419723982

Pull #62

github

web-flow
Merge 31a86ee9f into 6968676cc
Pull Request #62: [Cli] Generate the argument and option parameters a command declares

574 of 618 branches covered (92.88%)

Branch coverage included in aggregate %.

93 of 154 new or added lines in 2 files covered. (60.39%)

1618 of 1679 relevant lines covered (96.37%)

4.23 hits per line

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

80.33
/src/main/java/io/sindri/ast/CliRouteAttributeReader.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.Expression;
18
import com.github.javaparser.ast.expr.MemberValuePair;
19
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
20
import io.sindri.ast.abstract_.AstReader;
21
import io.sindri.ast.contract.CliRouteAttributeReaderContract;
22
import io.sindri.ast.data.CliArgumentParameterData;
23
import io.sindri.ast.data.CliOptionParameterData;
24
import io.sindri.ast.data.result.CliRouteAttributeResult;
25
import java.util.LinkedHashMap;
26
import java.util.List;
27
import java.util.Map;
28
import java.util.Optional;
29

30
public class CliRouteAttributeReader extends AstReader implements CliRouteAttributeReaderContract {
31

32
    /** The route-level middleware stage contracts, in {@code cli.routing.data.Route} order. */
33
    private static final List<String> STAGE_CONTRACTS =
5✔
34
            List.of(
2✔
35
                    "io.valkyrja.cli.middleware.contract.RouteMatchedMiddlewareContract",
36
                    "io.valkyrja.cli.middleware.contract.RouteDispatchedMiddlewareContract",
37
                    "io.valkyrja.cli.middleware.contract.ThrowableCaughtMiddlewareContract",
38
                    "io.valkyrja.cli.middleware.contract.ProcessExitingMiddlewareContract");
39

40
    private final MiddlewareClassifier classifier = new MiddlewareClassifier();
5✔
41
    private final CliRouteParameterReader parameterReader = new CliRouteParameterReader();
5✔
42
    private final MiddlewareClassifier.SourceResolver resolver;
43

44
    /** No-op resolver: without a way to resolve middleware sources, no middleware is classified. */
45
    public CliRouteAttributeReader() {
46
        this(fqn -> Optional.empty());
5✔
47
    }
1✔
48

49
    public CliRouteAttributeReader(MiddlewareClassifier.SourceResolver resolver) {
2✔
50
        this.resolver = resolver;
3✔
51
    }
1✔
52

53
    /** Classify the method's {@code @Middleware} into the stage lists, in constructor order. */
54
    private List<List<String>> classifyMiddleware(
55
            MethodDeclaration method, Map<String, String> imports, String pkg) {
56
        Map<String, List<String>> byContract =
8✔
57
                classifier.classifyMethod(
2✔
58
                        method, imports, pkg, resolver, java.util.Set.copyOf(STAGE_CONTRACTS));
1✔
59
        List<List<String>> lists = new java.util.ArrayList<>();
4✔
60
        for (String contract : STAGE_CONTRACTS) {
10✔
61
            lists.add(byContract.getOrDefault(contract, List.of()));
8✔
62
        }
1✔
63
        return lists;
2✔
64
    }
65

66
    /** Emit the four stage middleware lists as positional {@code List.of(...)} constructor args. */
67
    private String emitMiddlewareLists(List<List<String>> middleware) {
68
        List<String> parts = new java.util.ArrayList<>();
4✔
69
        for (List<String> classes : middleware) {
10✔
70
            parts.add(
3✔
71
                    classes.isEmpty()
3✔
72
                            ? "java.util.List.of()"
2✔
73
                            : "java.util.List.of(" + String.join(".class, ", classes) + ".class)");
4✔
74
        }
1✔
75
        return String.join(", ", parts);
4✔
76
    }
77

78
    @Override
79
    public CliRouteAttributeResult readFile(String filePath) {
80
        CompilationUnit cu = parseFile(filePath);
4✔
81
        Map<String, String> importMap = buildImportMap(cu);
4✔
82
        String pkg = getPackageName(cu);
4✔
83

84
        TypeDeclaration<?> type =
2✔
85
                findType(cu)
3✔
86
                        .orElseThrow(() -> new RuntimeException("No type found in: " + filePath));
9✔
87

88
        Map<String, Expression> routeMap = new LinkedHashMap<>();
4✔
89
        for (MethodDeclaration method : type.getMethods()) {
11✔
90
            List<AnnotationExpr> routeAnnotations =
1✔
91
                    method.getAnnotations().stream()
3✔
92
                            .filter(a -> a.getNameAsString().equals("Route"))
6✔
93
                            .toList();
2✔
94

95
            if (routeAnnotations.isEmpty()) {
3✔
96
                continue;
1✔
97
            }
98

99
            String handlerClass = "";
2✔
100
            String handlerMethod = "";
2✔
101
            Optional<AnnotationExpr> handlerAnnotation =
1✔
102
                    method.getAnnotations().stream()
3✔
103
                            .filter(a -> a.getNameAsString().equals("RouteHandler"))
6✔
104
                            .findFirst();
2✔
105
            if (handlerAnnotation.isPresent()
3✔
106
                    && handlerAnnotation.get() instanceof NormalAnnotationExpr handler) {
9✔
107
                for (MemberValuePair pair : handler.getPairs()) {
11✔
108
                    if (pair.getNameAsString().equals("handlerClass")) {
5✔
109
                        handlerClass = extractClassExprFqn(pair.getValue(), importMap, pkg);
8✔
110
                    } else if (pair.getNameAsString().equals("handlerMethod")) {
5✔
111
                        handlerMethod = extractStringLiteral(pair.getValue());
5✔
112
                    }
113
                }
1✔
114
            }
115

116
            for (AnnotationExpr routeAnnotation : routeAnnotations) {
10✔
117
                if (!(routeAnnotation instanceof NormalAnnotationExpr normalRoute)) {
6✔
118
                    continue;
119
                }
120
                String name = "";
2✔
121
                String description = "";
2✔
122
                for (MemberValuePair pair : normalRoute.getPairs()) {
11✔
123
                    switch (pair.getNameAsString()) {
9✔
124
                        case "name" -> name = extractStringLiteral(pair.getValue());
6✔
125
                        case "description" -> description = extractStringLiteral(pair.getValue());
6✔
126
                        default -> {}
127
                    }
128
                }
1✔
129
                List<List<String>> middleware = classifyMiddleware(method, importMap, pkg);
6✔
130
                List<CliArgumentParameterData> arguments =
5✔
131
                        parameterReader.updateArguments(method, importMap, pkg);
2✔
132
                List<CliOptionParameterData> options =
5✔
133
                        parameterReader.updateOptions(method, importMap, pkg);
2✔
134
                routeMap.put(
12✔
135
                        name,
136
                        buildRouteValue(
1✔
137
                                name,
138
                                description,
139
                                handlerClass,
140
                                handlerMethod,
141
                                middleware,
142
                                arguments,
143
                                options));
144
            }
1✔
145
        }
1✔
146
        return new CliRouteAttributeResult(routeMap);
5✔
147
    }
148

149
    /**
150
     * Build the {@code Supplier<RouteContract>} expression stored as a route's value, e.g. {@code
151
     * () -> new Route("name", "description", Handler::method)}. Fully qualified names are used so
152
     * the generated file needs no extra imports.
153
     */
154
    private Expression buildRouteValue(
155
            String name,
156
            String description,
157
            String handlerClass,
158
            String handlerMethod,
159
            List<List<String>> middleware,
160
            List<CliArgumentParameterData> arguments,
161
            List<CliOptionParameterData> options) {
162
        String handlerRef = !handlerClass.isEmpty() ? handlerClass + "::" + handlerMethod : "null";
9✔
163

164
        StringBuilder supplier =
4✔
165
                new StringBuilder(
166
                        "() -> new io.valkyrja.cli.routing.data.Route(\""
167
                                + escapeJava(name)
3✔
168
                                + "\", \""
169
                                + escapeJava(description)
5✔
170
                                + "\", "
171
                                + handlerRef);
172
        // The short constructor covers a route with no middleware, arguments or options; anything
173
        // more needs the full constructor, whose null helpText matches the short default.
174
        // Otherwise the middleware, arguments or options would be dropped.
175
        if (middleware.stream().anyMatch(list -> !list.isEmpty())
13✔
176
                || !arguments.isEmpty()
3✔
177
                || !options.isEmpty()) {
2!
178
            supplier.append(", null, ")
5✔
179
                    .append(emitMiddlewareLists(middleware))
3✔
180
                    .append(", ")
3✔
181
                    .append(emitArguments(arguments))
3✔
182
                    .append(", ")
3✔
183
                    .append(emitOptions(options));
3✔
184
        }
185
        supplier.append(")");
4✔
186

187
        return StaticJavaParser.parseExpression(supplier.toString());
4✔
188
    }
189

190
    /** Emit the argument parameters a command declares. */
191
    private String emitArguments(List<CliArgumentParameterData> arguments) {
192
        StringBuilder sb = new StringBuilder("java.util.List.of(");
5✔
193

194
        for (int i = 0; i < arguments.size(); i++) {
8✔
195
            CliArgumentParameterData argument = arguments.get(i);
5✔
196
            sb.append("new io.valkyrja.cli.routing.data.ArgumentParameter(\"")
5✔
197
                    .append(escapeJava(argument.name()))
4✔
198
                    .append("\", \"")
3✔
199
                    .append(escapeJava(argument.description()))
4✔
200
                    .append("\", io.valkyrja.cli.routing.enum_.ArgumentMode.")
2✔
201
                    .append(argument.mode())
3✔
202
                    .append(", io.valkyrja.cli.routing.enum_.ArgumentValueMode.")
2✔
203
                    .append(argument.valueMode())
3✔
204
                    .append(", java.util.List.of())");
2✔
205

206
            if (i < arguments.size() - 1) {
6!
NEW
207
                sb.append(", ");
×
208
            }
209
        }
210

211
        return sb.append(")").toString();
5✔
212
    }
213

214
    /** Emit the option parameters a command declares. */
215
    private String emitOptions(List<CliOptionParameterData> options) {
216
        StringBuilder sb = new StringBuilder("java.util.List.of(");
5✔
217

218
        for (int i = 0; i < options.size(); i++) {
6!
NEW
219
            CliOptionParameterData option = options.get(i);
×
NEW
220
            sb.append("new io.valkyrja.cli.routing.data.OptionParameter(\"")
×
NEW
221
                    .append(escapeJava(option.name()))
×
NEW
222
                    .append("\", \"")
×
NEW
223
                    .append(escapeJava(option.description()))
×
NEW
224
                    .append("\", \"")
×
NEW
225
                    .append(escapeJava(option.valueDisplayName()))
×
NEW
226
                    .append("\", \"")
×
NEW
227
                    .append(escapeJava(option.defaultValue()))
×
NEW
228
                    .append("\", ")
×
NEW
229
                    .append(emitStringList(option.shortNames()))
×
NEW
230
                    .append(", ")
×
NEW
231
                    .append(emitStringList(option.validValues()))
×
NEW
232
                    .append(", java.util.List.of(), io.valkyrja.cli.routing.enum_.OptionMode.")
×
NEW
233
                    .append(option.mode())
×
NEW
234
                    .append(", io.valkyrja.cli.routing.enum_.OptionValueMode.")
×
NEW
235
                    .append(option.valueMode())
×
NEW
236
                    .append(")");
×
237

NEW
238
            if (i < options.size() - 1) {
×
NEW
239
                sb.append(", ");
×
240
            }
241
        }
242

243
        return sb.append(")").toString();
5✔
244
    }
245

246
    /** Emit a list of string literals. */
247
    private String emitStringList(List<String> values) {
NEW
248
        StringBuilder sb = new StringBuilder("java.util.List.of(");
×
249

NEW
250
        for (int i = 0; i < values.size(); i++) {
×
NEW
251
            sb.append("\"").append(escapeJava(values.get(i))).append("\"");
×
252

NEW
253
            if (i < values.size() - 1) {
×
NEW
254
                sb.append(", ");
×
255
            }
256
        }
257

NEW
258
        return sb.append(")").toString();
×
259
    }
260

261
    private String escapeJava(String value) {
262
        return value.replace("\\", "\\\\").replace("\"", "\\\"");
8✔
263
    }
264
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc