Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* text eol=lf
*.jar -text -eol -working-tree-encoding -merge -diff
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public interface CodegenConfig extends GlobalCodegenConfig {
// package visibility for unit tests
String BUILD_TIME_GLOBAL_PREFIX_FORMAT = "quarkus." + CODEGEN_TIME_CONFIG_PREFIX + ".%s";
String BUILD_TIME_SPEC_PREFIX_FORMAT = "quarkus." + CODEGEN_TIME_CONFIG_PREFIX + ".spec.%s";
String BUILD_TIME_GAV_PREFIX_FORMAT = "quarkus." + CODEGEN_TIME_CONFIG_PREFIX + ".gav.%s";

List<String> SUPPORTED_CONFIGURATIONS = Arrays.stream(ConfigName.values()).map(cn -> cn.name)
.toList();
Expand All @@ -40,6 +41,7 @@ enum ConfigName {
INCLUDE("include"),
EXCLUDE("exclude"),
ARTIFACT_ID_FILTER("artifact-id-filter"),
INCLUDE_GAVS("include-gavs"),
EXCLUDE_GAVS("exclude-gavs"),
VALIDATE_SPEC("validateSpec"),
DEFAULT_SECURITY_SCHEME("default-security-scheme"),
Expand All @@ -50,6 +52,9 @@ enum ConfigName {
MODEL_NAME_SUFFIX("model-name-suffix"),
MODEL_NAME_PREFIX("model-name-prefix"),

//gav configs only
SPEC_FILES("spec-files"),

//global & spec configs
SKIP_FORM_MODEL("skip-form-model"),
MUTINY("mutiny"),
Expand Down Expand Up @@ -97,6 +102,12 @@ enum ConfigName {
@WithName("spec")
Map<String, SpecItemConfig> specItem();

/**
* OpenAPI GAV details for codegen configuration.
*/
@WithName("gav")
Map<String, GavItemConfig> gavItem();

static String resolveApiPackage(final String basePackage) {
return String.format("%s%s", basePackage, API_PKG_SUFFIX);
}
Expand All @@ -119,6 +130,13 @@ static String getSpecConfigName(ConfigName configName, final Path openApiFilePat
return String.format("%s.%s", getBuildTimeSpecPropertyPrefix(openApiFilePath), configName.name);
}

/**
* Return gav config name openapi-generator.codegen.gav.%s.config-name
*/
static String getGavConfigName(ConfigName configName, final Path openApiFilePath) {
return String.format("%s.%s", getBuildTimeGavPropertyPrefix(openApiFilePath), configName.name);
}

/**
* Return spec config name by config-key (<b>openapi-generator.codegen.spec.%s.config-key</b>) property.
* For example, given a configuration <code>quarkus.openapi.generator.codegen.spec.spec_yaml.config-key=petstore</code>, the
Expand All @@ -140,6 +158,10 @@ static String getBuildTimeSpecPropertyPrefix(final Path openApiFilePath) {
return String.format(BUILD_TIME_SPEC_PREFIX_FORMAT, getSanitizedFileName(openApiFilePath));
}

static String getBuildTimeGavPropertyPrefix(final Path openApiFilePath) {
return String.format(BUILD_TIME_GAV_PREFIX_FORMAT, getSanitizedFileName(openApiFilePath));
}

static String getSanitizedFileName(final Path openApiFilePath) {
return StringUtil
.replaceNonAlphanumericByUnderscores(OpenApiGeneratorOutputPaths.getRelativePath(openApiFilePath).toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkiverse.openapi.generator.deployment;

import java.util.List;
import java.util.Optional;

import io.smallrye.config.WithDefault;
import io.smallrye.config.WithName;

/*
* Model for the configuration of this extension.
* It's used for documentation purposes only.
* The configuration is consumed in the codegen phase, before build time.
* Not meant to be used outside this scope.
* Config items can be applied only on gav
*/
public interface GavItemConfig extends CommonItemConfig {
/**
* List of OpenAPI spec files in GAV to be generated
*/
@WithName("spec-files")
@WithDefault("openapi.yaml")
Optional<List<String>> gavSpecFiles();
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ public interface GlobalCodegenConfig extends CommonItemConfig {
@WithName("exclude-gavs")
Optional<List<String>> excludeGavs();

/**
* Option to specify GAVs for which generation should be executed only.
*
* Depending on the GAV Provider default behavior differs:
* <ul>
* <li>for {@link io.quarkiverse.openapi.generator.deployment.codegen.YamlOrJsonGAVCoordinateOpenApiSpecInputProvider}, all
* suitable GAVs will be considered for generation if config value is not given</li>
* <li>for {@link io.quarkiverse.openapi.generator.deployment.codegen.JarOrZipGAVCoordinateOpenApiSpecInputProvider}, only
* specified GAVs will be considered for generation if config value is available</li>
* </ul>
*/
@WithName("include-gavs")
Optional<List<String>> includeGavs();

/**
* Create security for the referenced security scheme
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package io.quarkiverse.openapi.generator.deployment.codegen;

import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.*;
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.*;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;

import org.jboss.logging.Logger;

import io.quarkus.bootstrap.prebuild.CodeGenException;
import io.quarkus.deployment.CodeGenContext;
import io.quarkus.maven.dependency.ResolvedDependency;
import io.smallrye.config.common.utils.StringUtil;

abstract class AbstractGAVCoordinateOpenApiSpecInputProvider implements OpenApiSpecInputProvider {
private static final Logger LOG = Logger.getLogger(AbstractGAVCoordinateOpenApiSpecInputProvider.class);

@Override
public List<SpecInputModel> read(CodeGenContext context) throws CodeGenException {
if (!context.config().getOptionalValue(getGlobalConfigName(GAV_SCANNING), Boolean.class)
.orElse(true)) {
LOG.debug("GAV scanning is disabled.");
return List.of();
}

List<String> gavsToExclude = context.config().getOptionalValues(getGlobalConfigName(EXCLUDE_GAVS), String.class)
.orElse(List.of());
String artifactIdFilter = context.config().getOptionalValue(getGlobalConfigName(ARTIFACT_ID_FILTER), String.class)
.filter(Predicate.not(String::isBlank))
.orElse(".*openapi.*");

List<ResolvedDependency> dependencies = context.applicationModel().getDependencies().stream()
.filter(rd -> getSupportedExtensions().contains(rd.getType().toLowerCase()))
.filter(rd -> rd.getArtifactId().matches(artifactIdFilter))
.filter(rd -> !gavsToExclude.contains(rd.getKey().toGacString()))
.filter(rd -> specificGAVSpecInputProviderFilter(context, rd.getKey().toGacString()))
.toList();

if (dependencies.isEmpty()) {
LOG.debug("No suitable GAV dependencies found. ArtifactIdFilter was %s and gavsToExclude were %s."
.formatted(artifactIdFilter, gavsToExclude));
return List.of();
}

var inputModels = new ArrayList<SpecInputModel>();
for (ResolvedDependency dependency : dependencies) {
var gacString = StringUtil.replaceNonAlphanumericByUnderscores(dependency.getKey().toGacString());
var path = dependency.getResolvedPaths().stream().findFirst()
.orElseThrow(() -> new CodeGenException("Could not find maven path of %s.".formatted(gacString)));
addInputModels(context, gacString, path, inputModels);
}
return inputModels;
}

protected abstract Set<String> getSupportedExtensions();

/**
* Adds input models to the provided list based on the given context, GAC string, and path.
* This method is implemented by subclasses to generate or retrieve the appropriate
* {@code SpecInputModel} instances that will be processed during code generation.
*
* @param context the code generation context, providing access to configuration and utilities
* @param gacString the GAC (Group, Artifact, Classifier) string representing the dependency identifier
* @param path the path to the file or directory containing the input specification(s)
* @param inputModels the list to which the generated {@code SpecInputModel} instances are added
* @throws CodeGenException if an error occurs while processing the input specifications
*/
protected abstract void addInputModels(CodeGenContext context,
String gacString,
Path path,
List<SpecInputModel> inputModels) throws CodeGenException;

/**
* Filters dependencies based on specific criteria defined in the implementing class.
* This method is invoked as part of the dependency resolution process to determine
* whether a dependency identified by its GAC string should be included for further processing.
*
* @param context the code generation context, providing access to configuration and other utilities
* @param gacString the GAC (Group, Artifact, Classifier) string representing the dependency identifier
* @return true if the dependency matches the filter criteria and should be included; false otherwise
*/
protected abstract boolean specificGAVSpecInputProviderFilter(CodeGenContext context, String gacString);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package io.quarkiverse.openapi.generator.deployment.codegen;

import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.*;
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.*;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import io.quarkus.bootstrap.prebuild.CodeGenException;
import io.quarkus.deployment.CodeGenContext;

/**
* Provides OpenAPI specification input from Maven GAV (GroupId:ArtifactId:Version) dependencies
* packaged as JAR or ZIP files.
* <p>
* This provider extends the {@link AbstractGAVCoordinateOpenApiSpecInputProvider} and is responsible for
* scanning application dependencies to identify JAR or ZIP files that contain OpenAPI specifications
* (e.g., `openapi.yaml`).
* </p>
*
* <h2>Supported File Types</h2>
* <p>
* The provider specifically supports dependencies packaged as:
* </p>
* <ul>
* <li>JAR files</li>
* <li>ZIP files</li>
* </ul>
*
* <h2>Scanning Behavior</h2>
* <p>
* The provider performs the following steps:
* </p>
* <ol>
* <li>Checks if GAV scanning is enabled via configuration (enabled by default)</li>
* <li>Filters dependencies by artifact type (jar/zip)</li>
* <li>Applies artifact ID filtering using a regex pattern</li>
* <li>Excludes specific GAVs based on configuration</li>
* <li>Includes specific GAVs based on configuration if not available no GAVs are used</li>
* <li>Creates {@link ZippedSpecInputModel} instances for each matching dependency and openAPI specification file</li>
* </ol>
*
* <h2>Configuration</h2>
* <p>
* The provider respects the following configuration properties:
* </p>
* <ul>
* <li>{@code quarkus.openapi-generator.codegen.gav-scanning} - Enable/disable GAV scanning</li>
* <li>{@code quarkus.openapi-generator.codegen.artifact-id-filter} - Regex pattern for artifact ID filtering</li>
* <li>{@code quarkus.openapi-generator.codegen.exclude-gavs} - List of GAV coordinates to exclude
* (format: groupId:artifactId:classifier)</li>
* <li>{@code quarkus.openapi-generator.codegen.gav.com_sample_customer_service_openapi.spec-files} - List of
* openAPI specification files in com.sample:customer-service-openapi:jar</li>
* </ul>
*
* <h2>Example Usage</h2>
*
* <pre>
* # application.properties
* quarkus.openapi-generator.codegen.gav-scanning=true
* quarkus.openapi-generator.codegen.artifact-id-filter=.*api.*
* quarkus.openapi-generator.codegen.exclude-gavs=com.example:old-api
* quarkus.openapi-generator.codegen.gav.com_sample_customer_service_api.spec-files=customer.yaml,another.yaml
* </pre>
*
* @see AbstractGAVCoordinateOpenApiSpecInputProvider
* @see ZippedSpecInputModel
* @see CodeGenContext
*/
public class JarOrZipGAVCoordinateOpenApiSpecInputProvider extends AbstractGAVCoordinateOpenApiSpecInputProvider {
private static final Set<String> SUPPORTED_EXTENSIONS = Set.of("jar", "zip");

@Override
protected void addInputModels(CodeGenContext context,
String gacString,
Path path,
List<SpecInputModel> inputModels) throws CodeGenException {
List<String> rootFilesOfSpecOfDependency = context.config()
.getOptionalValues(getGavConfigName(SPEC_FILES, Paths.get(gacString)), String.class)
.orElse(List.of("openapi.yaml"));
for (String rootFileOfSpecForDependency : rootFilesOfSpecOfDependency) {
try {
inputModels.add(new ZippedSpecInputModel(
gacString,
rootFileOfSpecForDependency,
Files.newInputStream(path)));
} catch (IOException e) {
throw new CodeGenException(
"Could not open input stream of %s from %s.".formatted(gacString, path.toString()),
e);
}
}
}

@Override
protected Set<String> getSupportedExtensions() {
return SUPPORTED_EXTENSIONS;
}

@Override
protected boolean specificGAVSpecInputProviderFilter(final CodeGenContext context, final String gacString) {
return new HashSet<>(context.config().getOptionalValues(getGlobalConfigName(INCLUDE_GAVS), String.class)
.orElse(List.of())) // default to empty list to disable all if not specified
.contains(gacString);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public final class OpenApiConfigValidator {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenApiConfigValidator.class);

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

private OpenApiConfigValidator() {
}
Expand Down
Loading