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

valkyrjaio / sindri-java / 30138871992

25 Jul 2026 01:37AM UTC coverage: 97.932%. First build
30138871992

Pull #38

github

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

483 of 499 branches covered (96.79%)

Branch coverage included in aggregate %.

278 of 302 new or added lines in 14 files covered. (92.05%)

1411 of 1435 relevant lines covered (98.33%)

4.3 hits per line

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

95.93
/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
    private final CliRouteAttributeReader cliRouteAttributeReader = new CliRouteAttributeReader();
5✔
48
    private final HttpRouteAttributeReader httpRouteAttributeReader =
5✔
49
            new HttpRouteAttributeReader();
50

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

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

77
    protected abstract ContainerDataFileGeneratorContract getContainerDataFileGenerator();
78

79
    protected abstract EventDataFileGeneratorContract getEventDataFileGenerator();
80

81
    protected abstract CliDataFileGeneratorContract getCliDataFileGenerator();
82

83
    protected abstract HttpDataFileGeneratorContract getHttpDataFileGenerator();
84

85
    protected abstract GrpcDataFileGeneratorContract getGrpcDataFileGenerator();
86

87
    public void run(String configFilePath) {
88
        ConfigResult config = configReader.readFile(configFilePath);
5✔
89

90
        ComponentProviderResult allProviderData = collectProviderData(config);
4✔
91

92
        Map<String, String[]> publishers =
2✔
93
                collectPublishers(allProviderData.serviceProviders(), config);
4✔
94

95
        Map<String, String> listeners =
2✔
96
                collectListeners(allProviderData.listenerProviders(), config);
4✔
97

98
        Map<String, String> cliRoutes =
2✔
99
                collectCliRoutes(allProviderData.cliRouteProviders(), config);
4✔
100

101
        Map<String, String> httpRoutes = new LinkedHashMap<>();
4✔
102
        Map<String, HttpRouteData> httpRouteData = new LinkedHashMap<>();
4✔
103
        collectHttpRoutes(allProviderData.httpRouteProviders(), config, httpRoutes, httpRouteData);
7✔
104

105
        Map<String, String> grpcRoutes =
2✔
106
                collectGrpcRoutes(allProviderData.grpcRouteProviders(), config);
4✔
107

108
        String dataClassName = "AppContainerData";
2✔
109
        String dataDir = config.dataPath();
3✔
110
        String dataNamespace = config.dataNamespace();
3✔
111

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

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

155
    private ComponentProviderResult collectProviderData(ConfigResult config) {
156
        ComponentProviderResult combined = new ComponentProviderResult();
4✔
157

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

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

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

175
            ComponentProviderResult data = componentProviderReader.readFile(filePath);
5✔
176
            combined = combined.merge(data);
4✔
177
            queue.addAll(data.componentProviders());
5✔
178
        }
1✔
179

180
        return combined;
2✔
181
    }
182

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

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

212
    private Map<String, String> collectCliRoutes(
213
            java.util.List<String> cliRouteProviders, ConfigResult config) {
214
        Map<String, String> routes = new LinkedHashMap<>();
4✔
215
        for (String providerFqn : cliRouteProviders) {
10✔
216
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
217
            if (filePath.isEmpty()) {
3✔
218
                continue;
1✔
219
            }
220
            RouteProviderResult routeProvider = routeProviderReader.readFile(filePath);
5✔
221
            for (String controllerFqn : routeProvider.controllerClasses()) {
11✔
222
                String controllerPath =
3✔
223
                        fqnToFilePath(controllerFqn, config.namespace(), config.dir());
5✔
224
                if (!controllerPath.isEmpty()) {
3✔
225
                    CliRouteAttributeResult result =
3✔
226
                            cliRouteAttributeReader.readFile(controllerPath);
2✔
227
                    result.routes().forEach((name, expr) -> routes.put(name, expr.toString()));
12✔
228
                }
229
            }
1✔
230

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

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

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

278
    private void collectHttpRoutes(
279
            java.util.List<String> httpRouteProviders,
280
            ConfigResult config,
281
            Map<String, String> httpRoutes,
282
            Map<String, HttpRouteData> httpRouteData) {
283
        for (String providerFqn : httpRouteProviders) {
10✔
284
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
285
            if (filePath.isEmpty()) {
3✔
286
                continue;
1✔
287
            }
288
            RouteProviderResult routeProvider = routeProviderReader.readFile(filePath);
5✔
289
            for (String controllerFqn : routeProvider.controllerClasses()) {
11✔
290
                String controllerPath =
3✔
291
                        fqnToFilePath(controllerFqn, config.namespace(), config.dir());
5✔
292
                if (!controllerPath.isEmpty()) {
3✔
293
                    HttpRouteAttributeResult result =
3✔
294
                            httpRouteAttributeReader.readFile(controllerPath);
2✔
295
                    result.routes().forEach((name, expr) -> httpRoutes.put(name, expr.toString()));
12✔
296
                    httpRouteData.putAll(result.routeData());
4✔
297
                }
298
            }
1✔
299

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

313
    private String extractRouteArgString(Expression routeExpr, int index) {
314
        // Callers only pass object-creation route expressions (RouteProviderReader filters them).
315
        var arguments = routeExpr.asObjectCreationExpr().getArguments();
4✔
316
        if (index < arguments.size() && arguments.get(index).isStringLiteralExpr()) {
10✔
317
            return arguments.get(index).asStringLiteralExpr().getValue();
7✔
318
        }
319
        return "";
2✔
320
    }
321

322
    private HttpRouteData buildHttpRouteDataFromExpr(Expression routeExpr, String name) {
323
        String path = extractRouteArgString(routeExpr, 0);
5✔
324
        boolean isDynamic = path.contains("{");
4✔
325
        String regex = isDynamic ? extractRouteArgString(routeExpr, 2) : "";
9✔
326

327
        return new HttpRouteData(
8✔
328
                path,
329
                name,
330
                null,
331
                extractRequestMethodsFromExpr(routeExpr),
1✔
332
                java.util.List.of(),
1✔
333
                java.util.List.of(),
1✔
334
                java.util.List.of(),
1✔
335
                java.util.List.of(),
1✔
336
                java.util.List.of(),
4✔
337
                null,
338
                null,
339
                isDynamic,
340
                java.util.List.of(),
3✔
341
                regex);
342
    }
343

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

355
    private String fqnToFilePath(String fqn, String namespace, String srcDir) {
356
        String namespacePkg = namespace.toLowerCase(java.util.Locale.ROOT);
4✔
357
        if (fqn.startsWith(namespacePkg + ".")) {
5✔
358
            String relative = fqn.substring(namespacePkg.length() + 1).replace('.', '/');
10✔
359
            return srcDir + "/" + relative + ".java";
4✔
360
        }
361

362
        // Framework/vendor classes live outside the app source tree. Resolve their .java from the
363
        // sources jar on the classpath (the portable equivalent of PHP's
364
        // ReflectionClass::getFileName()) so their publishers()/providers can be scanned too.
365
        return resolveSourceFromClasspath(fqn);
4✔
366
    }
367

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

378
        String resource = fqn.replace('.', '/') + ".java";
6✔
379
        java.io.InputStream in = getClass().getClassLoader().getResourceAsStream(resource);
6✔
380
        if (in == null) {
2✔
381
            classpathSourceCache.put(fqn, "");
6✔
382
            return "";
2✔
383
        }
384

385
        // A plain try/catch (rather than try-with-resources) keeps both outcomes fully reachable;
386
        // stageSource owns closing the stream.
387
        try {
388
            String path = stageSource(in);
4✔
389
            classpathSourceCache.put(fqn, path);
6✔
390

391
            return path;
2✔
392
        } catch (java.io.IOException e) {
1✔
393
            classpathSourceCache.put(fqn, "");
6✔
394
            return "";
2✔
395
        }
396
    }
397

398
    /** Copy a resolved source stream to a temp file, closing the stream, and return its path. */
399
    protected String stageSource(java.io.InputStream in) throws java.io.IOException {
400
        java.nio.file.Path temp = java.nio.file.Files.createTempFile("sindri-src-", ".java");
6✔
401
        temp.toFile().deleteOnExit();
3✔
402
        java.nio.file.Files.copy(in, temp, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
10✔
403
        in.close();
2✔
404

405
        return temp.toString();
3✔
406
    }
407
}
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