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

valkyrjaio / sindri-java / 30150631134

25 Jul 2026 08:10AM UTC coverage: 98.672%. First build
30150631134

Pull #38

github

web-flow
Merge bae2a9dfc into 04c800ff7
Pull Request #38: [Grpc] Add gRPC data-class generation

512 of 525 branches covered (97.52%)

Branch coverage included in aggregate %.

377 of 391 new or added lines in 16 files covered. (96.42%)

1494 of 1508 relevant lines covered (99.07%)

4.34 hits per line

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

95.96
/src/main/java/io/sindri/generate/abstract_/GenerateDataFromAst.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.generate.abstract_;
11

12
import com.github.javaparser.ast.expr.Expression;
13
import com.github.javaparser.ast.expr.FieldAccessExpr;
14
import io.sindri.ast.CliRouteAttributeReader;
15
import io.sindri.ast.ComponentProviderReader;
16
import io.sindri.ast.ConfigReader;
17
import io.sindri.ast.GrpcRouteAttributeReader;
18
import io.sindri.ast.HttpRouteAttributeReader;
19
import io.sindri.ast.ListenerProviderReader;
20
import io.sindri.ast.MiddlewareClassifier;
21
import io.sindri.ast.RouteProviderReader;
22
import io.sindri.ast.ServiceProviderReader;
23
import io.sindri.ast.data.HttpRouteData;
24
import io.sindri.ast.data.result.CliRouteAttributeResult;
25
import io.sindri.ast.data.result.ComponentProviderResult;
26
import io.sindri.ast.data.result.ConfigResult;
27
import io.sindri.ast.data.result.GrpcRouteAttributeResult;
28
import io.sindri.ast.data.result.HttpRouteAttributeResult;
29
import io.sindri.ast.data.result.RouteProviderResult;
30
import io.sindri.generator.cli.contract.CliDataFileGeneratorContract;
31
import io.sindri.generator.container.contract.ContainerDataFileGeneratorContract;
32
import io.sindri.generator.enum_.GenerateStatus;
33
import io.sindri.generator.event.contract.EventDataFileGeneratorContract;
34
import io.sindri.generator.grpc.contract.GrpcDataFileGeneratorContract;
35
import io.sindri.generator.http.contract.HttpDataFileGeneratorContract;
36
import io.sindri.generator.throwable.exception.DataFileWriteException;
37
import java.util.LinkedHashMap;
38
import java.util.Map;
39

40
public abstract class GenerateDataFromAst {
2✔
41

42
    private final ConfigReader configReader = new ConfigReader();
5✔
43
    private final ComponentProviderReader componentProviderReader = new ComponentProviderReader();
5✔
44
    private final ServiceProviderReader serviceProviderReader = new ServiceProviderReader();
5✔
45
    private final RouteProviderReader routeProviderReader = new RouteProviderReader();
5✔
46
    private final ListenerProviderReader listenerProviderReader = new ListenerProviderReader();
5✔
47

48
    /**
49
     * Resolve a class to its parsed source for middleware classification: app classes from the
50
     * source tree, framework classes from the sources jar on the classpath. An unresolvable class
51
     * simply stops that branch of the hierarchy walk.
52
     *
53
     * @param config the generation config
54
     * @return the resolver
55
     */
56
    private MiddlewareClassifier.SourceResolver middlewareSourceResolver(ConfigResult config) {
57
        return fqn -> {
4✔
NEW
58
            String path = fqnToFilePath(fqn, config.namespace(), config.dir());
×
NEW
59
            if (path.isEmpty()) {
×
NEW
60
                return java.util.Optional.empty();
×
61
            }
62
            try {
NEW
63
                return java.util.Optional.of(
×
NEW
64
                        com.github.javaparser.StaticJavaParser.parse(new java.io.File(path)));
×
NEW
65
            } catch (java.io.FileNotFoundException e) {
×
NEW
66
                return java.util.Optional.empty();
×
67
            }
68
        };
69
    }
70

71
    /** FQN → staged temp source path (or "" when unresolvable) for classpath-resolved sources. */
72
    private final Map<String, String> classpathSourceCache = new java.util.HashMap<>();
6✔
73

74
    protected abstract ContainerDataFileGeneratorContract getContainerDataFileGenerator();
75

76
    protected abstract EventDataFileGeneratorContract getEventDataFileGenerator();
77

78
    protected abstract CliDataFileGeneratorContract getCliDataFileGenerator();
79

80
    protected abstract HttpDataFileGeneratorContract getHttpDataFileGenerator();
81

82
    protected abstract GrpcDataFileGeneratorContract getGrpcDataFileGenerator();
83

84
    public void run(String configFilePath) {
85
        ConfigResult config = configReader.readFile(configFilePath);
5✔
86

87
        ComponentProviderResult allProviderData = collectProviderData(config);
4✔
88

89
        Map<String, String[]> publishers =
2✔
90
                collectPublishers(allProviderData.serviceProviders(), config);
4✔
91

92
        Map<String, String> listeners =
2✔
93
                collectListeners(allProviderData.listenerProviders(), config);
4✔
94

95
        Map<String, String> cliRoutes =
2✔
96
                collectCliRoutes(allProviderData.cliRouteProviders(), config);
4✔
97

98
        Map<String, String> httpRoutes = new LinkedHashMap<>();
4✔
99
        Map<String, HttpRouteData> httpRouteData = new LinkedHashMap<>();
4✔
100
        collectHttpRoutes(allProviderData.httpRouteProviders(), config, httpRoutes, httpRouteData);
7✔
101

102
        Map<String, String> grpcRoutes =
2✔
103
                collectGrpcRoutes(allProviderData.grpcRouteProviders(), config);
4✔
104

105
        String dataClassName = "AppContainerData";
2✔
106
        String dataDir = config.dataPath();
3✔
107
        String dataNamespace = config.dataNamespace();
3✔
108

109
        requireWritten(
4✔
110
                dataClassName,
111
                getContainerDataFileGenerator()
5✔
112
                        .generateFile(dataDir, dataClassName, dataNamespace, publishers));
1✔
113
        requireWritten(
4✔
114
                "AppEventData",
115
                getEventDataFileGenerator()
5✔
116
                        .generateFile(dataDir, "AppEventData", dataNamespace, listeners));
1✔
117
        requireWritten(
4✔
118
                "AppCliRoutingData",
119
                getCliDataFileGenerator()
5✔
120
                        .generateFile(dataDir, "AppCliRoutingData", dataNamespace, cliRoutes));
1✔
121
        requireWritten(
4✔
122
                "AppHttpRoutingData",
123
                getHttpDataFileGenerator()
6✔
124
                        .generateFile(
1✔
125
                                dataDir,
126
                                "AppHttpRoutingData",
127
                                dataNamespace,
128
                                httpRoutes,
129
                                httpRouteData));
130
        requireWritten(
4✔
131
                "AppGrpcRoutingData",
132
                getGrpcDataFileGenerator()
5✔
133
                        .generateFile(dataDir, "AppGrpcRoutingData", dataNamespace, grpcRoutes));
1✔
134
    }
1✔
135

136
    /**
137
     * Fail loudly when a data file could not be written.
138
     *
139
     * <p>A discarded {@link GenerateStatus#FAILURE} leaves the previous generation's data class on
140
     * disk (or none at all) while the build reports success — the application then boots against a
141
     * stale route map. Surface it instead.
142
     *
143
     * @param dataClassName the data class being written, for the error message
144
     * @param status the generator's result
145
     */
146
    private void requireWritten(String dataClassName, GenerateStatus status) {
147
        if (status == GenerateStatus.FAILURE) {
3!
NEW
148
            throw new DataFileWriteException("Failed to write " + dataClassName + ".");
×
149
        }
150
    }
1✔
151

152
    private ComponentProviderResult collectProviderData(ConfigResult config) {
153
        ComponentProviderResult combined = new ComponentProviderResult();
4✔
154

155
        // Walk the full component-provider graph breadth-first. Each provider can nest further
156
        // component providers (e.g. an app provider pulling in framework providers, which pull in
157
        // their own), so recurse to any depth, guarding against cycles with a visited set.
158
        java.util.Deque<String> queue = new java.util.ArrayDeque<>(config.providers());
6✔
159
        java.util.Set<String> visited = new java.util.HashSet<>();
4✔
160

161
        while (!queue.isEmpty()) {
3✔
162
            String providerFqn = queue.poll();
4✔
163
            if (!visited.add(providerFqn)) {
4✔
164
                continue;
1✔
165
            }
166

167
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
168
            if (filePath.isEmpty()) {
3✔
169
                continue;
1✔
170
            }
171

172
            ComponentProviderResult data = componentProviderReader.readFile(filePath);
5✔
173
            combined = combined.merge(data);
4✔
174
            queue.addAll(data.componentProviders());
5✔
175
        }
1✔
176

177
        return combined;
2✔
178
    }
179

180
    private Map<String, String[]> collectPublishers(
181
            java.util.List<String> serviceProviders, ConfigResult config) {
182
        Map<String, String[]> publishers = new LinkedHashMap<>();
4✔
183
        for (String providerFqn : serviceProviders) {
10✔
184
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
185
            if (filePath.isEmpty()) {
3✔
186
                continue;
1✔
187
            }
188
            publishers.putAll(serviceProviderReader.readFile(filePath).publishers());
7✔
189
        }
1✔
190
        return publishers;
2✔
191
    }
192

193
    private Map<String, String> collectListeners(
194
            java.util.List<String> listenerProviders, ConfigResult config) {
195
        Map<String, String> listeners = new LinkedHashMap<>();
4✔
196
        for (String providerFqn : listenerProviders) {
10✔
197
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
198
            if (filePath.isEmpty()) {
3✔
199
                continue;
1✔
200
            }
201
            listenerProviderReader
3✔
202
                    .readFile(filePath)
1✔
203
                    .listeners()
3✔
204
                    .forEach(expr -> listeners.put(expr.toString(), expr.toString()));
9✔
205
        }
1✔
206
        return listeners;
2✔
207
    }
208

209
    private Map<String, String> collectCliRoutes(
210
            java.util.List<String> cliRouteProviders, ConfigResult config) {
211
        // Built per config so the reader can resolve each @Middleware class's source and classify
212
        // it
213
        // into its stages before the route is cached.
214
        CliRouteAttributeReader cliRouteAttributeReader =
4✔
215
                new CliRouteAttributeReader(middlewareSourceResolver(config));
3✔
216
        Map<String, String> routes = new LinkedHashMap<>();
4✔
217
        for (String providerFqn : cliRouteProviders) {
10✔
218
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
219
            if (filePath.isEmpty()) {
3✔
220
                continue;
1✔
221
            }
222
            RouteProviderResult routeProvider = routeProviderReader.readFile(filePath);
5✔
223
            for (String controllerFqn : routeProvider.controllerClasses()) {
11✔
224
                String controllerPath =
3✔
225
                        fqnToFilePath(controllerFqn, config.namespace(), config.dir());
5✔
226
                if (!controllerPath.isEmpty()) {
3✔
227
                    CliRouteAttributeResult result =
2✔
228
                            cliRouteAttributeReader.readFile(controllerPath);
2✔
229
                    result.routes().forEach((name, expr) -> routes.put(name, expr.toString()));
12✔
230
                }
231
            }
1✔
232

233
            // Manually-defined provider routes (getRoutes()) — the CLI Route's name is its first
234
            // constructor argument.
235
            for (Expression routeExpr : routeProvider.routes()) {
11✔
236
                String name = extractRouteArgString(routeExpr, 0);
5✔
237
                if (!name.isEmpty()) {
3✔
238
                    routes.put(name, "() -> " + routeExpr);
7✔
239
                }
240
            }
1✔
241
        }
1✔
242
        return routes;
2✔
243
    }
244

245
    private Map<String, String> collectGrpcRoutes(
246
            java.util.List<String> grpcRouteProviders, ConfigResult config) {
247
        Map<String, String> routes = new LinkedHashMap<>();
4✔
248
        for (String providerFqn : grpcRouteProviders) {
10✔
249
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
250
            if (filePath.isEmpty()) {
3✔
251
                continue;
1✔
252
            }
253
            // Built per config so the reader can resolve each @Middleware class's source and
254
            // classify it into its stages before the route is cached.
255
            GrpcRouteAttributeReader grpcRouteAttributeReader =
4✔
256
                    new GrpcRouteAttributeReader(middlewareSourceResolver(config));
3✔
257
            RouteProviderResult routeProvider = routeProviderReader.readFile(filePath);
5✔
258
            for (String controllerFqn : routeProvider.controllerClasses()) {
11✔
259
                String controllerPath =
3✔
260
                        fqnToFilePath(controllerFqn, config.namespace(), config.dir());
5✔
261
                if (!controllerPath.isEmpty()) {
3✔
262
                    GrpcRouteAttributeResult result =
2✔
263
                            grpcRouteAttributeReader.readFile(controllerPath);
2✔
264
                    result.routes().forEach((method, expr) -> routes.put(method, expr.toString()));
12✔
265
                }
266
            }
1✔
267

268
            // Manually-defined provider routes (getRoutes()) — the gRPC Route's fully-qualified
269
            // method is its first constructor argument.
270
            for (Expression routeExpr : routeProvider.routes()) {
11✔
271
                String method = extractRouteArgString(routeExpr, 0);
5✔
272
                if (!method.isEmpty()) {
3✔
273
                    routes.put(method, "() -> " + routeExpr);
7✔
274
                }
275
            }
1✔
276
        }
1✔
277
        return routes;
2✔
278
    }
279

280
    private void collectHttpRoutes(
281
            java.util.List<String> httpRouteProviders,
282
            ConfigResult config,
283
            Map<String, String> httpRoutes,
284
            Map<String, HttpRouteData> httpRouteData) {
285
        // Built per config so the reader can resolve each @Middleware class's source and classify
286
        // it
287
        // into its stages before the route is cached.
288
        HttpRouteAttributeReader httpRouteAttributeReader =
4✔
289
                new HttpRouteAttributeReader(middlewareSourceResolver(config));
3✔
290
        for (String providerFqn : httpRouteProviders) {
10✔
291
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
292
            if (filePath.isEmpty()) {
3✔
293
                continue;
1✔
294
            }
295
            RouteProviderResult routeProvider = routeProviderReader.readFile(filePath);
5✔
296
            for (String controllerFqn : routeProvider.controllerClasses()) {
11✔
297
                String controllerPath =
3✔
298
                        fqnToFilePath(controllerFqn, config.namespace(), config.dir());
5✔
299
                if (!controllerPath.isEmpty()) {
3✔
300
                    HttpRouteAttributeResult result =
2✔
301
                            httpRouteAttributeReader.readFile(controllerPath);
2✔
302
                    result.routes().forEach((name, expr) -> httpRoutes.put(name, expr.toString()));
12✔
303
                    httpRouteData.putAll(result.routeData());
4✔
304
                }
305
            }
1✔
306

307
            // Manually-defined provider routes (getRoutes()) are inlined too, so the cached data
308
            // holds every route and the runtime never has to iterate providers.
309
            for (Expression routeExpr : routeProvider.routes()) {
11✔
310
                String name = extractRouteArgString(routeExpr, 1);
5✔
311
                if (name.isEmpty()) {
3✔
312
                    continue;
1✔
313
                }
314
                httpRoutes.put(name, "() -> " + routeExpr);
7✔
315
                httpRouteData.put(name, buildHttpRouteDataFromExpr(routeExpr, name));
8✔
316
            }
1✔
317
        }
1✔
318
    }
1✔
319

320
    private String extractRouteArgString(Expression routeExpr, int index) {
321
        // Callers only pass object-creation route expressions (RouteProviderReader filters them).
322
        var arguments = routeExpr.asObjectCreationExpr().getArguments();
4✔
323
        if (index < arguments.size() && arguments.get(index).isStringLiteralExpr()) {
10✔
324
            return arguments.get(index).asStringLiteralExpr().getValue();
7✔
325
        }
326
        return "";
2✔
327
    }
328

329
    private HttpRouteData buildHttpRouteDataFromExpr(Expression routeExpr, String name) {
330
        String path = extractRouteArgString(routeExpr, 0);
5✔
331
        boolean isDynamic = path.contains("{");
4✔
332
        String regex = isDynamic ? extractRouteArgString(routeExpr, 2) : "";
9✔
333

334
        return new HttpRouteData(
8✔
335
                path,
336
                name,
337
                null,
338
                extractRequestMethodsFromExpr(routeExpr),
1✔
339
                java.util.List.of(),
1✔
340
                java.util.List.of(),
1✔
341
                java.util.List.of(),
1✔
342
                java.util.List.of(),
1✔
343
                java.util.List.of(),
4✔
344
                null,
345
                null,
346
                isDynamic,
347
                java.util.List.of(),
3✔
348
                regex);
349
    }
350

351
    /** Pull RequestMethod constants out of a route expression, defaulting to HEAD + GET. */
352
    private java.util.List<String> extractRequestMethodsFromExpr(Expression routeExpr) {
353
        java.util.List<String> methods = new java.util.ArrayList<>();
4✔
354
        for (FieldAccessExpr fieldAccess : routeExpr.findAll(FieldAccessExpr.class)) {
12✔
355
            if (fieldAccess.getScope().toString().endsWith("RequestMethod")) {
6✔
356
                methods.add(fieldAccess.getNameAsString());
5✔
357
            }
358
        }
1✔
359
        return methods.isEmpty() ? java.util.List.of("HEAD", "GET") : methods;
9✔
360
    }
361

362
    private String fqnToFilePath(String fqn, String namespace, String srcDir) {
363
        String namespacePkg = namespace.toLowerCase(java.util.Locale.ROOT);
4✔
364
        if (fqn.startsWith(namespacePkg + ".")) {
5✔
365
            String relative = fqn.substring(namespacePkg.length() + 1).replace('.', '/');
10✔
366
            return srcDir + "/" + relative + ".java";
4✔
367
        }
368

369
        // Framework/vendor classes live outside the app source tree. Resolve their .java from the
370
        // sources jar on the classpath (the portable equivalent of PHP's
371
        // ReflectionClass::getFileName()) so their publishers()/providers can be scanned too.
372
        return resolveSourceFromClasspath(fqn);
4✔
373
    }
374

375
    /**
376
     * Resolve a class's {@code .java} source from a sources jar on the classpath and stage it as a
377
     * temp file (the AST readers accept only file paths). Returns {@code ""} when not found.
378
     */
379
    protected String resolveSourceFromClasspath(String fqn) {
380
        String cached = classpathSourceCache.get(fqn);
6✔
381
        if (cached != null) {
2✔
382
            return cached;
2✔
383
        }
384

385
        String resource = fqn.replace('.', '/') + ".java";
6✔
386
        java.io.InputStream in = getClass().getClassLoader().getResourceAsStream(resource);
6✔
387
        if (in == null) {
2✔
388
            classpathSourceCache.put(fqn, "");
6✔
389
            return "";
2✔
390
        }
391

392
        // A plain try/catch (rather than try-with-resources) keeps both outcomes fully reachable;
393
        // stageSource owns closing the stream.
394
        try {
395
            String path = stageSource(in);
4✔
396
            classpathSourceCache.put(fqn, path);
6✔
397

398
            return path;
2✔
399
        } catch (java.io.IOException e) {
1✔
400
            classpathSourceCache.put(fqn, "");
6✔
401
            return "";
2✔
402
        }
403
    }
404

405
    /** Copy a resolved source stream to a temp file, closing the stream, and return its path. */
406
    protected String stageSource(java.io.InputStream in) throws java.io.IOException {
407
        java.nio.file.Path temp = java.nio.file.Files.createTempFile("sindri-src-", ".java");
6✔
408
        temp.toFile().deleteOnExit();
3✔
409
        java.nio.file.Files.copy(in, temp, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
10✔
410
        in.close();
2✔
411

412
        return temp.toString();
3✔
413
    }
414
}
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