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

valkyrjaio / sindri-java / 30064673705

24 Jul 2026 03:37AM UTC coverage: 99.829%. First build
30064673705

Pull #38

github

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

439 of 441 branches covered (99.55%)

Branch coverage included in aggregate %.

179 of 180 new or added lines in 11 files covered. (99.44%)

1312 of 1313 relevant lines covered (99.92%)

4.32 hits per line

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

99.23
/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.RouteProviderReader;
21
import io.sindri.ast.ServiceProviderReader;
22
import io.sindri.ast.data.HttpRouteData;
23
import io.sindri.ast.data.result.CliRouteAttributeResult;
24
import io.sindri.ast.data.result.ComponentProviderResult;
25
import io.sindri.ast.data.result.ConfigResult;
26
import io.sindri.ast.data.result.GrpcRouteAttributeResult;
27
import io.sindri.ast.data.result.HttpRouteAttributeResult;
28
import io.sindri.ast.data.result.RouteProviderResult;
29
import io.sindri.generator.cli.contract.CliDataFileGeneratorContract;
30
import io.sindri.generator.container.contract.ContainerDataFileGeneratorContract;
31
import io.sindri.generator.enum_.GenerateStatus;
32
import io.sindri.generator.event.contract.EventDataFileGeneratorContract;
33
import io.sindri.generator.grpc.contract.GrpcDataFileGeneratorContract;
34
import io.sindri.generator.http.contract.HttpDataFileGeneratorContract;
35
import java.util.LinkedHashMap;
36
import java.util.Map;
37

38
public abstract class GenerateDataFromAst {
2✔
39

40
    private final ConfigReader configReader = new ConfigReader();
5✔
41
    private final ComponentProviderReader componentProviderReader = new ComponentProviderReader();
5✔
42
    private final ServiceProviderReader serviceProviderReader = new ServiceProviderReader();
5✔
43
    private final RouteProviderReader routeProviderReader = new RouteProviderReader();
5✔
44
    private final ListenerProviderReader listenerProviderReader = new ListenerProviderReader();
5✔
45
    private final CliRouteAttributeReader cliRouteAttributeReader = new CliRouteAttributeReader();
5✔
46
    private final HttpRouteAttributeReader httpRouteAttributeReader =
5✔
47
            new HttpRouteAttributeReader();
48
    private final GrpcRouteAttributeReader grpcRouteAttributeReader =
5✔
49
            new GrpcRouteAttributeReader();
50

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

54
    protected abstract ContainerDataFileGeneratorContract getContainerDataFileGenerator();
55

56
    protected abstract EventDataFileGeneratorContract getEventDataFileGenerator();
57

58
    protected abstract CliDataFileGeneratorContract getCliDataFileGenerator();
59

60
    protected abstract HttpDataFileGeneratorContract getHttpDataFileGenerator();
61

62
    protected abstract GrpcDataFileGeneratorContract getGrpcDataFileGenerator();
63

64
    public void run(String configFilePath) {
65
        ConfigResult config = configReader.readFile(configFilePath);
5✔
66

67
        ComponentProviderResult allProviderData = collectProviderData(config);
4✔
68

69
        Map<String, String[]> publishers =
2✔
70
                collectPublishers(allProviderData.serviceProviders(), config);
4✔
71

72
        Map<String, String> listeners =
2✔
73
                collectListeners(allProviderData.listenerProviders(), config);
4✔
74

75
        Map<String, String> cliRoutes =
2✔
76
                collectCliRoutes(allProviderData.cliRouteProviders(), config);
4✔
77

78
        Map<String, String> httpRoutes = new LinkedHashMap<>();
4✔
79
        Map<String, HttpRouteData> httpRouteData = new LinkedHashMap<>();
4✔
80
        collectHttpRoutes(allProviderData.httpRouteProviders(), config, httpRoutes, httpRouteData);
7✔
81

82
        Map<String, String> grpcRoutes =
2✔
83
                collectGrpcRoutes(allProviderData.grpcRouteProviders(), config);
4✔
84

85
        String dataClassName = "AppContainerData";
2✔
86
        String dataDir = config.dataPath();
3✔
87
        String dataNamespace = config.dataNamespace();
3✔
88

89
        requireWritten(
4✔
90
                dataClassName,
91
                getContainerDataFileGenerator()
5✔
92
                        .generateFile(dataDir, dataClassName, dataNamespace, publishers));
1✔
93
        requireWritten(
4✔
94
                "AppEventData",
95
                getEventDataFileGenerator()
5✔
96
                        .generateFile(dataDir, "AppEventData", dataNamespace, listeners));
1✔
97
        requireWritten(
4✔
98
                "AppCliRoutingData",
99
                getCliDataFileGenerator()
5✔
100
                        .generateFile(dataDir, "AppCliRoutingData", dataNamespace, cliRoutes));
1✔
101
        requireWritten(
4✔
102
                "AppHttpRoutingData",
103
                getHttpDataFileGenerator()
6✔
104
                        .generateFile(
1✔
105
                                dataDir,
106
                                "AppHttpRoutingData",
107
                                dataNamespace,
108
                                httpRoutes,
109
                                httpRouteData));
110
        requireWritten(
4✔
111
                "AppGrpcRoutingData",
112
                getGrpcDataFileGenerator()
5✔
113
                        .generateFile(dataDir, "AppGrpcRoutingData", dataNamespace, grpcRoutes));
1✔
114
    }
1✔
115

116
    /**
117
     * Fail loudly when a data file could not be written.
118
     *
119
     * <p>A discarded {@link GenerateStatus#FAILURE} leaves the previous generation's data class on
120
     * disk (or none at all) while the build reports success — the application then boots against a
121
     * stale route map. Surface it instead.
122
     *
123
     * @param dataClassName the data class being written, for the error message
124
     * @param status the generator's result
125
     */
126
    private void requireWritten(String dataClassName, GenerateStatus status) {
127
        if (status == GenerateStatus.FAILURE) {
3!
NEW
128
            throw new RuntimeException("Failed to write " + dataClassName + ".");
×
129
        }
130
    }
1✔
131

132
    private ComponentProviderResult collectProviderData(ConfigResult config) {
133
        ComponentProviderResult combined = new ComponentProviderResult();
4✔
134

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

141
        while (!queue.isEmpty()) {
3✔
142
            String providerFqn = queue.poll();
4✔
143
            if (!visited.add(providerFqn)) {
4✔
144
                continue;
1✔
145
            }
146

147
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
148
            if (filePath.isEmpty()) {
3✔
149
                continue;
1✔
150
            }
151

152
            ComponentProviderResult data = componentProviderReader.readFile(filePath);
5✔
153
            combined = combined.merge(data);
4✔
154
            queue.addAll(data.componentProviders());
5✔
155
        }
1✔
156

157
        return combined;
2✔
158
    }
159

160
    private Map<String, String[]> collectPublishers(
161
            java.util.List<String> serviceProviders, ConfigResult config) {
162
        Map<String, String[]> publishers = new LinkedHashMap<>();
4✔
163
        for (String providerFqn : serviceProviders) {
10✔
164
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
165
            if (filePath.isEmpty()) {
3✔
166
                continue;
1✔
167
            }
168
            publishers.putAll(serviceProviderReader.readFile(filePath).publishers());
7✔
169
        }
1✔
170
        return publishers;
2✔
171
    }
172

173
    private Map<String, String> collectListeners(
174
            java.util.List<String> listenerProviders, ConfigResult config) {
175
        Map<String, String> listeners = new LinkedHashMap<>();
4✔
176
        for (String providerFqn : listenerProviders) {
10✔
177
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
178
            if (filePath.isEmpty()) {
3✔
179
                continue;
1✔
180
            }
181
            listenerProviderReader
3✔
182
                    .readFile(filePath)
1✔
183
                    .listeners()
3✔
184
                    .forEach(expr -> listeners.put(expr.toString(), expr.toString()));
9✔
185
        }
1✔
186
        return listeners;
2✔
187
    }
188

189
    private Map<String, String> collectCliRoutes(
190
            java.util.List<String> cliRouteProviders, ConfigResult config) {
191
        Map<String, String> routes = new LinkedHashMap<>();
4✔
192
        for (String providerFqn : cliRouteProviders) {
10✔
193
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
194
            if (filePath.isEmpty()) {
3✔
195
                continue;
1✔
196
            }
197
            RouteProviderResult routeProvider = routeProviderReader.readFile(filePath);
5✔
198
            for (String controllerFqn : routeProvider.controllerClasses()) {
11✔
199
                String controllerPath =
3✔
200
                        fqnToFilePath(controllerFqn, config.namespace(), config.dir());
5✔
201
                if (!controllerPath.isEmpty()) {
3✔
202
                    CliRouteAttributeResult result =
3✔
203
                            cliRouteAttributeReader.readFile(controllerPath);
2✔
204
                    result.routes().forEach((name, expr) -> routes.put(name, expr.toString()));
12✔
205
                }
206
            }
1✔
207

208
            // Manually-defined provider routes (getRoutes()) — the CLI Route's name is its first
209
            // constructor argument.
210
            for (Expression routeExpr : routeProvider.routes()) {
11✔
211
                String name = extractRouteArgString(routeExpr, 0);
5✔
212
                if (!name.isEmpty()) {
3✔
213
                    routes.put(name, "() -> " + routeExpr);
7✔
214
                }
215
            }
1✔
216
        }
1✔
217
        return routes;
2✔
218
    }
219

220
    private Map<String, String> collectGrpcRoutes(
221
            java.util.List<String> grpcRouteProviders, ConfigResult config) {
222
        Map<String, String> routes = new LinkedHashMap<>();
4✔
223
        for (String providerFqn : grpcRouteProviders) {
10✔
224
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
225
            if (filePath.isEmpty()) {
3✔
226
                continue;
1✔
227
            }
228
            RouteProviderResult routeProvider = routeProviderReader.readFile(filePath);
5✔
229
            for (String controllerFqn : routeProvider.controllerClasses()) {
11✔
230
                String controllerPath =
3✔
231
                        fqnToFilePath(controllerFqn, config.namespace(), config.dir());
5✔
232
                if (!controllerPath.isEmpty()) {
3✔
233
                    GrpcRouteAttributeResult result =
3✔
234
                            grpcRouteAttributeReader.readFile(controllerPath);
2✔
235
                    result.routes().forEach((method, expr) -> routes.put(method, expr.toString()));
12✔
236
                }
237
            }
1✔
238

239
            // Manually-defined provider routes (getRoutes()) — the gRPC Route's fully-qualified
240
            // method is its first constructor argument.
241
            for (Expression routeExpr : routeProvider.routes()) {
11✔
242
                String method = extractRouteArgString(routeExpr, 0);
5✔
243
                if (!method.isEmpty()) {
3✔
244
                    routes.put(method, "() -> " + routeExpr);
7✔
245
                }
246
            }
1✔
247
        }
1✔
248
        return routes;
2✔
249
    }
250

251
    private void collectHttpRoutes(
252
            java.util.List<String> httpRouteProviders,
253
            ConfigResult config,
254
            Map<String, String> httpRoutes,
255
            Map<String, HttpRouteData> httpRouteData) {
256
        for (String providerFqn : httpRouteProviders) {
10✔
257
            String filePath = fqnToFilePath(providerFqn, config.namespace(), config.dir());
8✔
258
            if (filePath.isEmpty()) {
3✔
259
                continue;
1✔
260
            }
261
            RouteProviderResult routeProvider = routeProviderReader.readFile(filePath);
5✔
262
            for (String controllerFqn : routeProvider.controllerClasses()) {
11✔
263
                String controllerPath =
3✔
264
                        fqnToFilePath(controllerFqn, config.namespace(), config.dir());
5✔
265
                if (!controllerPath.isEmpty()) {
3✔
266
                    HttpRouteAttributeResult result =
3✔
267
                            httpRouteAttributeReader.readFile(controllerPath);
2✔
268
                    result.routes().forEach((name, expr) -> httpRoutes.put(name, expr.toString()));
12✔
269
                    httpRouteData.putAll(result.routeData());
4✔
270
                }
271
            }
1✔
272

273
            // Manually-defined provider routes (getRoutes()) are inlined too, so the cached data
274
            // holds every route and the runtime never has to iterate providers.
275
            for (Expression routeExpr : routeProvider.routes()) {
11✔
276
                String name = extractRouteArgString(routeExpr, 1);
5✔
277
                if (name.isEmpty()) {
3✔
278
                    continue;
1✔
279
                }
280
                httpRoutes.put(name, "() -> " + routeExpr);
7✔
281
                httpRouteData.put(name, buildHttpRouteDataFromExpr(routeExpr, name));
8✔
282
            }
1✔
283
        }
1✔
284
    }
1✔
285

286
    private String extractRouteArgString(Expression routeExpr, int index) {
287
        // Callers only pass object-creation route expressions (RouteProviderReader filters them).
288
        var arguments = routeExpr.asObjectCreationExpr().getArguments();
4✔
289
        if (index < arguments.size() && arguments.get(index).isStringLiteralExpr()) {
10✔
290
            return arguments.get(index).asStringLiteralExpr().getValue();
7✔
291
        }
292
        return "";
2✔
293
    }
294

295
    private HttpRouteData buildHttpRouteDataFromExpr(Expression routeExpr, String name) {
296
        String path = extractRouteArgString(routeExpr, 0);
5✔
297
        boolean isDynamic = path.contains("{");
4✔
298
        String regex = isDynamic ? extractRouteArgString(routeExpr, 2) : "";
9✔
299

300
        return new HttpRouteData(
8✔
301
                path,
302
                name,
303
                null,
304
                extractRequestMethodsFromExpr(routeExpr),
1✔
305
                java.util.List.of(),
1✔
306
                java.util.List.of(),
1✔
307
                java.util.List.of(),
1✔
308
                java.util.List.of(),
1✔
309
                java.util.List.of(),
4✔
310
                null,
311
                null,
312
                isDynamic,
313
                java.util.List.of(),
3✔
314
                regex);
315
    }
316

317
    /** Pull RequestMethod constants out of a route expression, defaulting to HEAD + GET. */
318
    private java.util.List<String> extractRequestMethodsFromExpr(Expression routeExpr) {
319
        java.util.List<String> methods = new java.util.ArrayList<>();
4✔
320
        for (FieldAccessExpr fieldAccess : routeExpr.findAll(FieldAccessExpr.class)) {
12✔
321
            if (fieldAccess.getScope().toString().endsWith("RequestMethod")) {
6✔
322
                methods.add(fieldAccess.getNameAsString());
5✔
323
            }
324
        }
1✔
325
        return methods.isEmpty() ? java.util.List.of("HEAD", "GET") : methods;
9✔
326
    }
327

328
    private String fqnToFilePath(String fqn, String namespace, String srcDir) {
329
        String namespacePkg = namespace.toLowerCase(java.util.Locale.ROOT);
4✔
330
        if (fqn.startsWith(namespacePkg + ".")) {
5✔
331
            String relative = fqn.substring(namespacePkg.length() + 1).replace('.', '/');
10✔
332
            return srcDir + "/" + relative + ".java";
4✔
333
        }
334

335
        // Framework/vendor classes live outside the app source tree. Resolve their .java from the
336
        // sources jar on the classpath (the portable equivalent of PHP's
337
        // ReflectionClass::getFileName()) so their publishers()/providers can be scanned too.
338
        return resolveSourceFromClasspath(fqn);
4✔
339
    }
340

341
    /**
342
     * Resolve a class's {@code .java} source from a sources jar on the classpath and stage it as a
343
     * temp file (the AST readers accept only file paths). Returns {@code ""} when not found.
344
     */
345
    protected String resolveSourceFromClasspath(String fqn) {
346
        String cached = classpathSourceCache.get(fqn);
6✔
347
        if (cached != null) {
2✔
348
            return cached;
2✔
349
        }
350

351
        String resource = fqn.replace('.', '/') + ".java";
6✔
352
        java.io.InputStream in = getClass().getClassLoader().getResourceAsStream(resource);
6✔
353
        if (in == null) {
2✔
354
            classpathSourceCache.put(fqn, "");
6✔
355
            return "";
2✔
356
        }
357

358
        // A plain try/catch (rather than try-with-resources) keeps both outcomes fully reachable;
359
        // stageSource owns closing the stream.
360
        try {
361
            String path = stageSource(in);
4✔
362
            classpathSourceCache.put(fqn, path);
6✔
363

364
            return path;
2✔
365
        } catch (java.io.IOException e) {
1✔
366
            classpathSourceCache.put(fqn, "");
6✔
367
            return "";
2✔
368
        }
369
    }
370

371
    /** Copy a resolved source stream to a temp file, closing the stream, and return its path. */
372
    protected String stageSource(java.io.InputStream in) throws java.io.IOException {
373
        java.nio.file.Path temp = java.nio.file.Files.createTempFile("sindri-src-", ".java");
6✔
374
        temp.toFile().deleteOnExit();
3✔
375
        java.nio.file.Files.copy(in, temp, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
10✔
376
        in.close();
2✔
377

378
        return temp.toString();
3✔
379
    }
380
}
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