Skip to content

Commit f2f1cb8

Browse files
authored
Merge pull request #37 from quarkiverse/kogito-6752
Add support to generate stubs from any InputStream
2 parents f17cb56 + cb9e61a commit f2f1cb8

File tree

14 files changed

+250
-29
lines changed

14 files changed

+250
-29
lines changed

README.md

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,17 @@ See the [integration-tests](integration-tests) module for more information of ho
100100

101101
## Authentication Support
102102

103-
If your OpenAPI specification file has `securitySchemes` [definitions](https://spec.openapis.org/oas/v3.1.0#security-scheme-object), the inner generator will [register `ClientRequestFilter`s providers](https://download.eclipse.org/microprofile/microprofile-rest-client-2.0/microprofile-rest-client-spec-2.0.html#_provider_declaration) for you to implement the given authentication mechanism.
103+
If your OpenAPI specification file has `securitySchemes` [definitions](https://spec.openapis.org/oas/v3.1.0#security-scheme-object), the inner generator
104+
will [register `ClientRequestFilter`s providers](https://download.eclipse.org/microprofile/microprofile-rest-client-2.0/microprofile-rest-client-spec-2.0.html#_provider_declaration) for you to
105+
implement the given authentication mechanism.
104106

105-
To provide the credentials for your application, you can use the [Quarkus configuration support](https://quarkus.io/guides/config). The configuration key is composed using this pattern: `[base_package].security.auth.[security_scheme_name]/[auth_property_name]`. Where:
107+
To provide the credentials for your application, you can use the [Quarkus configuration support](https://quarkus.io/guides/config). The configuration key is composed using this
108+
pattern: `[base_package].security.auth.[security_scheme_name]/[auth_property_name]`. Where:
106109

107110
- `base_package` is the package name you gave when configuring the code generation using `quarkus.openapi-generator.codegen.spec.[open_api_file].base-package` property.
108-
- `security_scheme_name` is the name of the [security scheme object definition](https://spec.openapis.org/oas/v3.1.0#security-scheme-object) in the OpenAPI file. Given the following excerpt, we have `api_key` and `basic_auth` security schemes:
111+
- `security_scheme_name` is the name of the [security scheme object definition](https://spec.openapis.org/oas/v3.1.0#security-scheme-object) in the OpenAPI file. Given the following excerpt, we
112+
have `api_key` and `basic_auth` security schemes:
113+
109114
```json
110115
{
111116
"securitySchemes": {
@@ -121,6 +126,7 @@ To provide the credentials for your application, you can use the [Quarkus config
121126
}
122127
}
123128
```
129+
124130
- `auth_property_name` varies depending on the authentication provider. For example, for Basic Authentication we have `username` and `password`. See the following sections for more details.
125131

126132
> Tip: on production environments you will likely to use [HashiCorp Vault](https://quarkiverse.github.io/quarkiverse-docs/quarkus-vault/dev/index.html) or [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) to provide this information for your application.
@@ -150,14 +156,15 @@ Similarly to bearer token, the API Key Authentication also has the token entry k
150156
| -------------| --------------------------------------------------------------| --------------------------------------------------- |
151157
| API Key | `[base_package].security.auth.[security_scheme_name]/api-key` | `org.acme.openapi.security.auth.apikey/api-key` |
152158

153-
The API Key scheme has an additional property that requires where to add the API key in the request token: header, cookie or query. The inner provider takes care of that for you.
159+
The API Key scheme has an additional property that requires where to add the API key in the request token: header, cookie or query. The inner provider takes care of that for you.
154160

155161
## Circuit Breaker
156162

157163
You can define the [CircuitBreaker annotation from MicroProfile Fault Tolerance](https://microprofile.io/project/eclipse/microprofile-fault-tolerance/spec/src/main/asciidoc/circuitbreaker.asciidoc)
158164
in your generated classes by setting the desired configuration in `application.properties`.
159165

160166
Let's say you have the following OpenAPI definition:
167+
161168
````json
162169
{
163170
"openapi": "3.0.3",
@@ -207,17 +214,17 @@ And you want to configure Circuit Breaker for the `/bye` endpoint, you can do it
207214
Add the [SmallRye Fault Tolerance extension](https://quarkus.io/guides/smallrye-fault-tolerance) to your project's `pom.xml` file:
208215

209216
````xml
217+
210218
<dependency>
211-
<groupId>io.quarkus</groupId>
212-
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
219+
<groupId>io.quarkus</groupId>
220+
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
213221
</dependency>
214222
````
215223

216224
Assuming your Open API spec file is in `src/main/openapi/simple-openapi.json`, add the following configuration to your `application.properties` file:
217225

218226
````properties
219227
quarkus.openapi-generator.codegen.spec."simple-openapi.json".base-package=org.acme.openapi.simple
220-
221228
# Enables the CircuitBreaker extension for the byeGet method from the DefaultApi class
222229
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/enabled=true
223230
````
@@ -233,6 +240,7 @@ import java.io.InputStream;
233240
import java.io.OutputStream;
234241
import java.util.List;
235242
import java.util.Map;
243+
236244
import javax.ws.rs.*;
237245
import javax.ws.rs.core.Response;
238246
import javax.ws.rs.core.MediaType;
@@ -243,30 +251,38 @@ public interface DefaultApi {
243251

244252
@GET
245253
@Path("/bye")
246-
@Produces({"text/plain"})
254+
@Produces({ "text/plain" })
247255
@org.eclipse.microprofile.faulttolerance.CircuitBreaker
248256
public String byeGet();
249257

250258
@GET
251259
@Path("/hello")
252-
@Produces({"text/plain"})
260+
@Produces({ "text/plain" })
253261
public String helloGet();
254262

255263
}
256264
````
257265

258-
You can also override the default Circuit Breaker configuration by setting the properties in `application.properties` [just as you would for a traditional MicroProfile application](https://quarkus.io/guides/smallrye-fault-tolerance#runtime-configuration):
266+
You can also override the default Circuit Breaker configuration by setting the properties
267+
in `application.properties` [just as you would for a traditional MicroProfile application](https://quarkus.io/guides/smallrye-fault-tolerance#runtime-configuration):
259268

260269
````properties
261-
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/failOn = java.lang.IllegalArgumentException,java.lang.NullPointerException
262-
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/skipOn = java.lang.NumberFormatException
263-
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/delay = 33
264-
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/delayUnit = MILLIS
265-
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/requestVolumeThreshold = 42
266-
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/failureRatio = 3.14
267-
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/successThreshold = 22
270+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/failOn=java.lang.IllegalArgumentException,java.lang.NullPointerException
271+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/skipOn=java.lang.NumberFormatException
272+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/delay=33
273+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/delayUnit=MILLIS
274+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/requestVolumeThreshold=42
275+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/failureRatio=3.14
276+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/successThreshold=22
268277
````
269278

279+
## Generating files via InputStream
280+
281+
Having the files in the `src/main/openapi` directory will generate the REST stubs by default. Alternatively, you can implement the `io.quarkiverse.openapi.generator.codegen.OpenApiSpecInputProvider`
282+
interface to provide a list of `InputStream`s of OpenAPI specification files. This is useful in scenarios where you want to dynamically generate the client code without having the target spec file
283+
saved locally in your project.
284+
285+
See the example implementation [here](test-utils/src/main/java/io/quarkiverse/openapi/generator/testutils/codegen/ClassPathPetstoreOpenApiSpecInputProvider.java)
270286

271287
## Known Limitations
272288

deployment/pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@
100100
<dependency>
101101
<groupId>io.quarkiverse.openapi.generator</groupId>
102102
<artifactId>quarkus-openapi-generator-test-utils</artifactId>
103-
<version>${project.version}</version>
104103
<scope>test</scope>
105104
</dependency>
106105

deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,7 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
4242
.map(Path::toString)
4343
.filter(s -> s.endsWith(this.inputExtension()))
4444
.map(Path::of).forEach(openApiFilePath -> {
45-
final OpenApiClientGeneratorWrapper generator = new OpenApiClientGeneratorWrapper(
46-
openApiFilePath.normalize(), outDir)
47-
.withCircuitBreakerConfiguration(CircuitBreakerConfigurationParser.parse(
48-
context.config()));
49-
50-
context.config()
51-
.getOptionalValue(getResolvedBasePackageProperty(openApiFilePath), String.class)
52-
.ifPresent(generator::withBasePackage);
53-
generator.generate();
45+
this.generate(context, openApiFilePath, outDir);
5446
});
5547
} catch (IOException e) {
5648
throw new CodeGenException("Failed to generate java files from OpenApi files in " + openApiDir.toAbsolutePath(),
@@ -60,4 +52,16 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
6052
}
6153
return false;
6254
}
55+
56+
protected void generate(CodeGenContext context, final Path openApiFilePath, final Path outDir) {
57+
// TODO: do not generate with the output dir has generated files and the openapi file has the same checksum of the previous runz
58+
final OpenApiClientGeneratorWrapper generator = new OpenApiClientGeneratorWrapper(
59+
openApiFilePath.normalize(), outDir)
60+
.withCircuitBreakerConfiguration(CircuitBreakerConfigurationParser.parse(
61+
context.config()));
62+
context.config()
63+
.getOptionalValue(getResolvedBasePackageProperty(openApiFilePath), String.class)
64+
.ifPresent(generator::withBasePackage);
65+
generator.generate();
66+
}
6367
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package io.quarkiverse.openapi.generator.deployment.codegen;
2+
3+
import java.io.FileOutputStream;
4+
import java.io.IOException;
5+
import java.io.UncheckedIOException;
6+
import java.nio.channels.Channels;
7+
import java.nio.channels.ReadableByteChannel;
8+
import java.nio.file.Path;
9+
import java.nio.file.Paths;
10+
import java.util.List;
11+
import java.util.ServiceLoader;
12+
import java.util.stream.Collectors;
13+
14+
import org.eclipse.microprofile.config.Config;
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
18+
import io.quarkiverse.openapi.generator.codegen.OpenApiSpecInputProvider;
19+
import io.quarkiverse.openapi.generator.codegen.SpecInputModel;
20+
import io.quarkus.bootstrap.prebuild.CodeGenException;
21+
import io.quarkus.deployment.CodeGenContext;
22+
23+
public class OpenApiGeneratorStreamCodeGen extends OpenApiGeneratorCodeGenBase {
24+
25+
private static final Logger LOGGER = LoggerFactory.getLogger(OpenApiGeneratorStreamCodeGen.class);
26+
27+
private List<OpenApiSpecInputProvider> providers;
28+
private final ServiceLoader<OpenApiSpecInputProvider> loader;
29+
30+
public OpenApiGeneratorStreamCodeGen() {
31+
loader = ServiceLoader.load(OpenApiSpecInputProvider.class);
32+
}
33+
34+
private void loadServices() {
35+
loader.reload();
36+
providers = loader.stream().map(ServiceLoader.Provider::get).collect(Collectors.toList());
37+
LOGGER.debug("Loaded {} OpenApiSpecInputProviders", providers);
38+
}
39+
40+
@Override
41+
public String providerId() {
42+
return "open-api-stream";
43+
}
44+
45+
// unused by this CodeGenProvider since we rely on the input coming from ServiceLoaders
46+
@Override
47+
public String inputExtension() {
48+
return ".yaml";
49+
}
50+
51+
// unused by this CodeGenProvider since we rely on the input coming from ServiceLoaders
52+
@Override
53+
public String inputDirectory() {
54+
return "openapi";
55+
}
56+
57+
@Override
58+
public boolean trigger(CodeGenContext context) throws CodeGenException {
59+
final Path outDir = context.outDir();
60+
boolean generated = false;
61+
62+
for (final OpenApiSpecInputProvider provider : this.providers) {
63+
for (SpecInputModel inputModel : provider.read()) {
64+
LOGGER.debug("Processing OpenAPI spec input model {}", inputModel);
65+
if (inputModel == null) {
66+
throw new CodeGenException("SpecInputModel from provider " + provider + " is null");
67+
}
68+
final Path openApiFilePath = Paths.get(outDir.toString(), inputModel.getFileName());
69+
try (ReadableByteChannel channel = Channels.newChannel(inputModel.getInputStream());
70+
FileOutputStream output = new FileOutputStream(openApiFilePath.toString())) {
71+
output.getChannel().transferFrom(channel, 0, Integer.MAX_VALUE);
72+
LOGGER.debug("Saved OpenAPI spec input model in {}", openApiFilePath);
73+
this.generate(context, openApiFilePath, outDir);
74+
generated = true;
75+
} catch (IOException e) {
76+
throw new UncheckedIOException("Failed to save InputStream from provider " + provider + " into location ",
77+
e);
78+
}
79+
}
80+
}
81+
return generated;
82+
}
83+
84+
@Override
85+
public boolean shouldRun(Path sourceDir, Config config) {
86+
this.loadServices();
87+
return !this.providers.isEmpty();
88+
}
89+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
io.quarkiverse.openapi.generator.deployment.codegen.OpenApiGeneratorJsonCodeGen
22
io.quarkiverse.openapi.generator.deployment.codegen.OpenApiGeneratorYamlCodeGen
33
io.quarkiverse.openapi.generator.deployment.codegen.OpenApiGeneratorYmlCodeGen
4+
io.quarkiverse.openapi.generator.deployment.codegen.OpenApiGeneratorStreamCodeGen

integration-tests/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@
3636
<artifactId>microprofile-fault-tolerance-api</artifactId>
3737
</dependency>
3838

39+
<!-- exceptionally here, we are using the test utils with default scope because of the ClassPathOpenApiSpecInputProvider -->
3940
<dependency>
4041
<groupId>io.quarkiverse.openapi.generator</groupId>
4142
<artifactId>quarkus-openapi-generator-test-utils</artifactId>
42-
<version>${project.version}</version>
43-
<scope>test</scope>
43+
<scope>compile</scope>
4444
</dependency>
4545
<dependency>
4646
<groupId>io.quarkus</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.quarkiverse.openapi.generator.testutils.codegen.ClassPathPetstoreOpenApiSpecInputProvider

pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
<artifactId>microprofile-fault-tolerance-api</artifactId>
4242
<version>${version.org.eclipse.microprofile.fault-tolerance}</version>
4343
</dependency>
44+
<dependency>
45+
<groupId>io.quarkiverse.openapi.generator</groupId>
46+
<artifactId>quarkus-openapi-generator</artifactId>
47+
<version>${project.version}</version>
48+
</dependency>
4449
<!-- Test -->
4550
<dependency>
4651
<groupId>com.github.javaparser</groupId>
@@ -54,6 +59,12 @@
5459
<version>${version.org.assertj}</version>
5560
<scope>test</scope>
5661
</dependency>
62+
<dependency>
63+
<groupId>io.quarkiverse.openapi.generator</groupId>
64+
<artifactId>quarkus-openapi-generator-test-utils</artifactId>
65+
<version>${project.version}</version>
66+
<scope>test</scope>
67+
</dependency>
5768
</dependencies>
5869
</dependencyManagement>
5970
<build>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.quarkiverse.openapi.generator.codegen;
2+
3+
import java.io.InputStream;
4+
import java.util.List;
5+
6+
/**
7+
* Provider interface for clients to dynamically provide their own OpenAPI specification files.
8+
*/
9+
public interface OpenApiSpecInputProvider {
10+
11+
/**
12+
* Fetch OpenAPI specification files from a given source.
13+
*
14+
* @return a list of spec files in {@link InputStream} format.
15+
*/
16+
List<SpecInputModel> read();
17+
18+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.quarkiverse.openapi.generator.codegen;
2+
3+
import static java.util.Objects.requireNonNull;
4+
5+
import java.io.InputStream;
6+
import java.util.Objects;
7+
8+
public class SpecInputModel {
9+
10+
private final InputStream inputStream;
11+
private final String filename;
12+
13+
public SpecInputModel(final String filename, final InputStream inputStream) {
14+
requireNonNull(inputStream, "InputStream can't be null");
15+
requireNonNull(filename, "File name can't be null");
16+
this.inputStream = inputStream;
17+
this.filename = filename;
18+
}
19+
20+
public String getFileName() {
21+
return filename;
22+
}
23+
24+
public InputStream getInputStream() {
25+
return inputStream;
26+
}
27+
28+
@Override
29+
public String toString() {
30+
return "SpecInputModel{" +
31+
"name='" + filename + '\'' +
32+
'}';
33+
}
34+
35+
@Override
36+
public boolean equals(Object o) {
37+
if (this == o) {
38+
return true;
39+
}
40+
if (o == null || getClass() != o.getClass()) {
41+
return false;
42+
}
43+
SpecInputModel that = (SpecInputModel) o;
44+
return inputStream.equals(that.inputStream) && filename.equals(that.filename);
45+
}
46+
47+
@Override
48+
public int hashCode() {
49+
return Objects.hash(inputStream, filename);
50+
}
51+
}

0 commit comments

Comments
 (0)