Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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 {
/**
* 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