Skip to content

Commit 8c05d7f

Browse files
michaelsonnleitnerMichael Sonnleitner
andauthored
#1357 Load openapi specifications from jar maven dependency (GAV) of project (#1376)
* Load openapi specifications from jar maven dependency (GAV) of project #1357 - Added support for zipped (jar/zip) OpenAPI specifications. - Introduced `ZippedSpecInputModel` and `JarOrZipGAVCoordinateOpenApiSpecInputProvider`. - Updated `SpecItemConfig` and `CodegenConfig` to allow specification of multiple GAV-based files. - Reworked tests to include parameterized testing for multiple GAV sources. - Simplified YAML-based GAV provider to inherit from `AbstractGAVCoordinateOpenApiSpecInputProvider`. * added include-gavs option & docu * formating * review * review * doc added * review * review#2 --------- Co-authored-by: Michael Sonnleitner <[email protected]>
1 parent 4ab3873 commit 8c05d7f

25 files changed

+646
-155
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
* text eol=lf
2+
*.jar -text -eol -working-tree-encoding -merge -diff

client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodegenConfig.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public interface CodegenConfig extends GlobalCodegenConfig {
2727
// package visibility for unit tests
2828
String BUILD_TIME_GLOBAL_PREFIX_FORMAT = "quarkus." + CODEGEN_TIME_CONFIG_PREFIX + ".%s";
2929
String BUILD_TIME_SPEC_PREFIX_FORMAT = "quarkus." + CODEGEN_TIME_CONFIG_PREFIX + ".spec.%s";
30+
String BUILD_TIME_GAV_PREFIX_FORMAT = "quarkus." + CODEGEN_TIME_CONFIG_PREFIX + ".gav.%s";
3031

3132
List<String> SUPPORTED_CONFIGURATIONS = Arrays.stream(ConfigName.values()).map(cn -> cn.name)
3233
.toList();
@@ -40,6 +41,7 @@ enum ConfigName {
4041
INCLUDE("include"),
4142
EXCLUDE("exclude"),
4243
ARTIFACT_ID_FILTER("artifact-id-filter"),
44+
INCLUDE_GAVS("include-gavs"),
4345
EXCLUDE_GAVS("exclude-gavs"),
4446
VALIDATE_SPEC("validateSpec"),
4547
DEFAULT_SECURITY_SCHEME("default-security-scheme"),
@@ -50,6 +52,9 @@ enum ConfigName {
5052
MODEL_NAME_SUFFIX("model-name-suffix"),
5153
MODEL_NAME_PREFIX("model-name-prefix"),
5254

55+
//gav configs only
56+
SPEC_FILES("spec-files"),
57+
5358
//global & spec configs
5459
SKIP_FORM_MODEL("skip-form-model"),
5560
MUTINY("mutiny"),
@@ -97,6 +102,12 @@ enum ConfigName {
97102
@WithName("spec")
98103
Map<String, SpecItemConfig> specItem();
99104

105+
/**
106+
* OpenAPI GAV details for codegen configuration.
107+
*/
108+
@WithName("gav")
109+
Map<String, GavItemConfig> gavItem();
110+
100111
static String resolveApiPackage(final String basePackage) {
101112
return String.format("%s%s", basePackage, API_PKG_SUFFIX);
102113
}
@@ -119,6 +130,13 @@ static String getSpecConfigName(ConfigName configName, final Path openApiFilePat
119130
return String.format("%s.%s", getBuildTimeSpecPropertyPrefix(openApiFilePath), configName.name);
120131
}
121132

133+
/**
134+
* Return gav config name openapi-generator.codegen.gav.%s.config-name
135+
*/
136+
static String getGavConfigName(ConfigName configName, final Path openApiFilePath) {
137+
return String.format("%s.%s", getBuildTimeGavPropertyPrefix(openApiFilePath), configName.name);
138+
}
139+
122140
/**
123141
* Return spec config name by config-key (<b>openapi-generator.codegen.spec.%s.config-key</b>) property.
124142
* For example, given a configuration <code>quarkus.openapi.generator.codegen.spec.spec_yaml.config-key=petstore</code>, the
@@ -140,6 +158,10 @@ static String getBuildTimeSpecPropertyPrefix(final Path openApiFilePath) {
140158
return String.format(BUILD_TIME_SPEC_PREFIX_FORMAT, getSanitizedFileName(openApiFilePath));
141159
}
142160

161+
static String getBuildTimeGavPropertyPrefix(final Path openApiFilePath) {
162+
return String.format(BUILD_TIME_GAV_PREFIX_FORMAT, getSanitizedFileName(openApiFilePath));
163+
}
164+
143165
static String getSanitizedFileName(final Path openApiFilePath) {
144166
return StringUtil
145167
.replaceNonAlphanumericByUnderscores(OpenApiGeneratorOutputPaths.getRelativePath(openApiFilePath).toString());
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.quarkiverse.openapi.generator.deployment;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
6+
import io.smallrye.config.WithDefault;
7+
import io.smallrye.config.WithName;
8+
9+
/*
10+
* Model for the configuration of this extension.
11+
* It's used for documentation purposes only.
12+
* The configuration is consumed in the codegen phase, before build time.
13+
* Not meant to be used outside this scope.
14+
* Config items can be applied only on gav
15+
*/
16+
public interface GavItemConfig {
17+
/**
18+
* List of OpenAPI spec files in GAV to be generated
19+
*/
20+
@WithName("spec-files")
21+
@WithDefault("openapi.yaml")
22+
Optional<List<String>> gavSpecFiles();
23+
}

client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GlobalCodegenConfig.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ public interface GlobalCodegenConfig extends CommonItemConfig {
7474
@WithName("exclude-gavs")
7575
Optional<List<String>> excludeGavs();
7676

77+
/**
78+
* Option to specify GAVs for which generation should be executed only.
79+
*
80+
* Depending on the GAV Provider default behavior differs:
81+
* <ul>
82+
* <li>for {@link io.quarkiverse.openapi.generator.deployment.codegen.YamlOrJsonGAVCoordinateOpenApiSpecInputProvider}, all
83+
* suitable GAVs will be considered for generation if config value is not given</li>
84+
* <li>for {@link io.quarkiverse.openapi.generator.deployment.codegen.JarOrZipGAVCoordinateOpenApiSpecInputProvider}, only
85+
* specified GAVs will be considered for generation if config value is available</li>
86+
* </ul>
87+
*/
88+
@WithName("include-gavs")
89+
Optional<List<String>> includeGavs();
90+
7791
/**
7892
* Create security for the referenced security scheme
7993
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package io.quarkiverse.openapi.generator.deployment.codegen;
2+
3+
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.*;
4+
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.*;
5+
6+
import java.nio.file.Path;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import java.util.Set;
10+
import java.util.function.Predicate;
11+
12+
import org.jboss.logging.Logger;
13+
14+
import io.quarkus.bootstrap.prebuild.CodeGenException;
15+
import io.quarkus.deployment.CodeGenContext;
16+
import io.quarkus.maven.dependency.ResolvedDependency;
17+
import io.smallrye.config.common.utils.StringUtil;
18+
19+
abstract class AbstractGAVCoordinateOpenApiSpecInputProvider implements OpenApiSpecInputProvider {
20+
private static final Logger LOG = Logger.getLogger(AbstractGAVCoordinateOpenApiSpecInputProvider.class);
21+
22+
@Override
23+
public List<SpecInputModel> read(CodeGenContext context) throws CodeGenException {
24+
if (!context.config().getOptionalValue(getGlobalConfigName(GAV_SCANNING), Boolean.class)
25+
.orElse(true)) {
26+
LOG.debug("GAV scanning is disabled.");
27+
return List.of();
28+
}
29+
30+
List<String> gavsToExclude = context.config().getOptionalValues(getGlobalConfigName(EXCLUDE_GAVS), String.class)
31+
.orElse(List.of());
32+
String artifactIdFilter = context.config().getOptionalValue(getGlobalConfigName(ARTIFACT_ID_FILTER), String.class)
33+
.filter(Predicate.not(String::isBlank))
34+
.orElse(".*openapi.*");
35+
36+
List<ResolvedDependency> dependencies = context.applicationModel().getDependencies().stream()
37+
.filter(rd -> getSupportedExtensions().contains(rd.getType().toLowerCase()))
38+
.filter(rd -> rd.getArtifactId().matches(artifactIdFilter))
39+
.filter(rd -> !gavsToExclude.contains(rd.getKey().toGacString()))
40+
.filter(rd -> specificGAVSpecInputProviderFilter(context, rd.getKey().toGacString()))
41+
.toList();
42+
43+
if (dependencies.isEmpty()) {
44+
LOG.debug("No suitable GAV dependencies found. ArtifactIdFilter was %s and gavsToExclude were %s."
45+
.formatted(artifactIdFilter, gavsToExclude));
46+
return List.of();
47+
}
48+
49+
var inputModels = new ArrayList<SpecInputModel>();
50+
for (ResolvedDependency dependency : dependencies) {
51+
var gacString = StringUtil.replaceNonAlphanumericByUnderscores(dependency.getKey().toGacString());
52+
var path = dependency.getResolvedPaths().stream().findFirst()
53+
.orElseThrow(() -> new CodeGenException("Could not find maven path of %s.".formatted(gacString)));
54+
addInputModels(context, gacString, path, inputModels);
55+
}
56+
return inputModels;
57+
}
58+
59+
protected abstract Set<String> getSupportedExtensions();
60+
61+
/**
62+
* Adds input models to the provided list based on the given context, GAC string, and path.
63+
* This method is implemented by subclasses to generate or retrieve the appropriate
64+
* {@code SpecInputModel} instances that will be processed during code generation.
65+
*
66+
* @param context the code generation context, providing access to configuration and utilities
67+
* @param gacString the GAC (Group, Artifact, Classifier) string representing the dependency identifier
68+
* @param path the path to the file or directory containing the input specification(s)
69+
* @param inputModels the list to which the generated {@code SpecInputModel} instances are added
70+
* @throws CodeGenException if an error occurs while processing the input specifications
71+
*/
72+
protected abstract void addInputModels(CodeGenContext context,
73+
String gacString,
74+
Path path,
75+
List<SpecInputModel> inputModels) throws CodeGenException;
76+
77+
/**
78+
* Filters dependencies based on specific criteria defined in the implementing class.
79+
* This method is invoked as part of the dependency resolution process to determine
80+
* whether a dependency identified by its GAC string should be included for further processing.
81+
*
82+
* @param context the code generation context, providing access to configuration and other utilities
83+
* @param gacString the GAC (Group, Artifact, Classifier) string representing the dependency identifier
84+
* @return true if the dependency matches the filter criteria and should be included; false otherwise
85+
*/
86+
protected abstract boolean specificGAVSpecInputProviderFilter(CodeGenContext context, String gacString);
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package io.quarkiverse.openapi.generator.deployment.codegen;
2+
3+
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.*;
4+
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.*;
5+
6+
import java.io.IOException;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.nio.file.Paths;
10+
import java.util.HashSet;
11+
import java.util.List;
12+
import java.util.Set;
13+
14+
import io.quarkus.bootstrap.prebuild.CodeGenException;
15+
import io.quarkus.deployment.CodeGenContext;
16+
17+
/**
18+
* Provides OpenAPI specification input from Maven GAV (GroupId:ArtifactId:Version) dependencies
19+
* packaged as JAR or ZIP files.
20+
* <p>
21+
* This provider extends the {@link AbstractGAVCoordinateOpenApiSpecInputProvider} and is responsible for
22+
* scanning application dependencies to identify JAR or ZIP files that contain OpenAPI specifications
23+
* (e.g., `openapi.yaml`).
24+
* </p>
25+
*
26+
* <h2>Supported File Types</h2>
27+
* <p>
28+
* The provider specifically supports dependencies packaged as:
29+
* </p>
30+
* <ul>
31+
* <li>JAR files</li>
32+
* <li>ZIP files</li>
33+
* </ul>
34+
*
35+
* <h2>Scanning Behavior</h2>
36+
* <p>
37+
* The provider performs the following steps:
38+
* </p>
39+
* <ol>
40+
* <li>Checks if GAV scanning is enabled via configuration (enabled by default)</li>
41+
* <li>Filters dependencies by artifact type (jar/zip)</li>
42+
* <li>Applies artifact ID filtering using a regex pattern</li>
43+
* <li>Excludes specific GAVs based on configuration</li>
44+
* <li>Includes specific GAVs based on configuration if not available no GAVs are used</li>
45+
* <li>Creates {@link ZippedSpecInputModel} instances for each matching dependency and openAPI specification file</li>
46+
* </ol>
47+
*
48+
* <h2>Configuration</h2>
49+
* <p>
50+
* The provider respects the following configuration properties:
51+
* </p>
52+
* <ul>
53+
* <li>{@code quarkus.openapi-generator.codegen.gav-scanning} - Enable/disable GAV scanning</li>
54+
* <li>{@code quarkus.openapi-generator.codegen.artifact-id-filter} - Regex pattern for artifact ID filtering</li>
55+
* <li>{@code quarkus.openapi-generator.codegen.exclude-gavs} - List of GAV coordinates to exclude
56+
* (format: groupId:artifactId:classifier)</li>
57+
* <li>{@code quarkus.openapi-generator.codegen.gav.com_sample_customer_service_openapi.spec-files} - List of
58+
* openAPI specification files in com.sample:customer-service-openapi:jar</li>
59+
* </ul>
60+
*
61+
* <h2>Example Usage</h2>
62+
*
63+
* <pre>
64+
* # application.properties
65+
* quarkus.openapi-generator.codegen.gav-scanning=true
66+
* quarkus.openapi-generator.codegen.artifact-id-filter=.*api.*
67+
* quarkus.openapi-generator.codegen.exclude-gavs=com.example:old-api
68+
* quarkus.openapi-generator.codegen.gav.com_sample_customer_service_api.spec-files=customer.yaml,another.yaml
69+
* </pre>
70+
*
71+
* @see AbstractGAVCoordinateOpenApiSpecInputProvider
72+
* @see ZippedSpecInputModel
73+
* @see CodeGenContext
74+
*/
75+
public class JarOrZipGAVCoordinateOpenApiSpecInputProvider extends AbstractGAVCoordinateOpenApiSpecInputProvider {
76+
private static final Set<String> SUPPORTED_EXTENSIONS = Set.of("jar", "zip");
77+
78+
@Override
79+
protected void addInputModels(CodeGenContext context,
80+
String gacString,
81+
Path path,
82+
List<SpecInputModel> inputModels) throws CodeGenException {
83+
List<String> rootFilesOfSpecOfDependency = context.config()
84+
.getOptionalValues(getGavConfigName(SPEC_FILES, Paths.get(gacString)), String.class)
85+
.orElse(List.of("openapi.yaml"));
86+
for (String rootFileOfSpecForDependency : rootFilesOfSpecOfDependency) {
87+
try {
88+
inputModels.add(new ZippedSpecInputModel(
89+
gacString,
90+
rootFileOfSpecForDependency,
91+
Files.newInputStream(path)));
92+
} catch (IOException e) {
93+
throw new CodeGenException(
94+
"Could not open input stream of %s from %s.".formatted(gacString, path.toString()),
95+
e);
96+
}
97+
}
98+
}
99+
100+
@Override
101+
protected Set<String> getSupportedExtensions() {
102+
return SUPPORTED_EXTENSIONS;
103+
}
104+
105+
@Override
106+
protected boolean specificGAVSpecInputProviderFilter(final CodeGenContext context, final String gacString) {
107+
return new HashSet<>(context.config().getOptionalValues(getGlobalConfigName(INCLUDE_GAVS), String.class)
108+
.orElse(List.of())) // default to empty list to disable all if not specified
109+
.contains(gacString);
110+
}
111+
}

client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiConfigValidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public final class OpenApiConfigValidator {
1818
private static final Logger LOGGER = LoggerFactory.getLogger(OpenApiConfigValidator.class);
1919

2020
static final Pattern CONFIG_PATTERN = Pattern.compile(
21-
"quarkus\\.openapi-generator\\.codegen\\.(spec.(?<specId>[\\w\\-]*)\\.)?(?<configName>[A-Za-z0-9_\\-]*)\\.?(?<configMap>.+)?");
21+
"quarkus\\.openapi-generator\\.codegen\\.((spec|gav).(?<specId>[\\w\\-]*)\\.)?(?<configName>[A-Za-z0-9_\\-]*)\\.?(?<configMap>.+)?");
2222

2323
private OpenApiConfigValidator() {
2424
}

0 commit comments

Comments
 (0)