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

valkyrjaio / sindri-java / 30068029645

24 Jul 2026 04:55AM UTC coverage: 99.716%. First build
30068029645

Pull #38

github

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

439 of 441 branches covered (99.55%)

Branch coverage included in aggregate %.

181 of 184 new or added lines in 13 files covered. (98.37%)

1314 of 1317 relevant lines covered (99.77%)

4.31 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 io.sindri.generator.throwable.exception.DataFileWriteException;
36
import java.util.LinkedHashMap;
37
import java.util.Map;
38

39
public abstract class GenerateDataFromAst {
2✔
40

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

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

55
    protected abstract ContainerDataFileGeneratorContract getContainerDataFileGenerator();
56

57
    protected abstract EventDataFileGeneratorContract getEventDataFileGenerator();
58

59
    protected abstract CliDataFileGeneratorContract getCliDataFileGenerator();
60

61
    protected abstract HttpDataFileGeneratorContract getHttpDataFileGenerator();
62

63
    protected abstract GrpcDataFileGeneratorContract getGrpcDataFileGenerator();
64

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

158
        return combined;
2✔
159
    }
160

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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