Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ enum ConfigName {
EXCLUDE_GAVS("exclude-gavs"),
VALIDATE_SPEC("validateSpec"),
DEFAULT_SECURITY_SCHEME("default-security-scheme"),
SKIP_IF_UNCHANGED("skip-if-unchanged"),

//spec configs only
BASE_PACKAGE("base-package"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,10 @@ public interface GlobalCodegenConfig extends CommonItemConfig {
@WithName("default-security-scheme")
Optional<String> defaultSecuritySchema();

/**
* Whether to skip code generation if spec or configuration has not changed
*/
@WithName("skip-if-unchanged")
@WithDefault("false")
Optional<Boolean> skipIfUnchanged();
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.VALIDATE_SPEC;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -205,10 +208,33 @@ private static String determineRestClientReactiveJacksonCapabilityId() {
return Capability.REST_CLIENT_REACTIVE_JACKSON;
}

// TODO: do not generate if the output dir has generated files and the openapi file has the same checksum of the previous run
protected void generate(OpenApiGeneratorOptions options) {
Config config = options.config();
Path openApiFilePath = options.openApiFilePath();

boolean skipIfUnchanged = getValues(
config,
openApiFilePath,
CodegenConfig.ConfigName.SKIP_IF_UNCHANGED,
Boolean.class).orElse(false);

if (!skipIfUnchanged) {
doGenerate(options);
return;
}

String fingerprint = computeFingerprint(options);
if (shouldSkipGeneration(options, fingerprint)) {
return;
}

doGenerate(options);
persistFingerprint(options, fingerprint);
}

protected void doGenerate(OpenApiGeneratorOptions options) {
Config config = options.config();
Path openApiFilePath = options.openApiFilePath();
Path outDir = options.outDir();
boolean isRestEasyReactive = options.isRestEasyReactive();

Expand Down Expand Up @@ -503,4 +529,101 @@ private <K, V> Optional<Map<K, V>> getConfigKeyValues(final SmallRyeConfig confi
return possibleConfigKey.flatMap(s -> getValuesByConfigKey(config, configName, kClass, vClass, s));

}

private Path resolveChecksumFile(Path outDir, Path openApiFilePath) {
return outDir.resolve(".quarkus-openapi-generator")
.resolve(getSanitizedFileName(openApiFilePath) + ".sha256");
}

private String computeFingerprint(OpenApiGeneratorOptions options) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");

updateDigestWithFile(digest, options.openApiFilePath());
updateDigestWithString(digest, "reactive=" + options.isRestEasyReactive());
updateDigestWithString(digest, "generator=" + getClass().getName());
updateDigestWithString(digest, "quarkusVersion=" + Version.getVersion());

addRelevantConfig(digest, options.config());

Path templateDir = options.templateDir();
if (templateDir != null && Files.isDirectory(templateDir)) {
try (Stream<Path> paths = Files.walk(templateDir).sorted()) {
for (Path path : paths.filter(Files::isRegularFile).toList()) {
updateDigestWithString(digest, "template:" + templateDir.relativize(path));
updateDigestWithFile(digest, path);
}
}
}

return HexFormat.of().formatHex(digest.digest());
} catch (Exception e) {
throw new RuntimeException("Unable to compute fingerprint for " + options.openApiFilePath(), e);
}
}

private void updateDigestWithString(MessageDigest digest, String value) {
digest.update(value.getBytes(StandardCharsets.UTF_8));
}

private void updateDigestWithFile(MessageDigest digest, Path file) throws IOException {
digest.update(Files.readAllBytes(file));
}

// Includes configuration in the fingerprint so changes force regeneration
private void addRelevantConfig(MessageDigest digest, Config config) {
String prefix = "quarkus." + CodegenConfig.CODEGEN_TIME_CONFIG_PREFIX + ".";

StreamSupport.stream(config.getPropertyNames().spliterator(), false)
.filter(propertyName -> propertyName.startsWith(prefix))
.sorted()
.forEach(propertyName -> config.getOptionalValue(propertyName, String.class)
.ifPresent(value -> updateDigestWithString(digest, propertyName + "=" + value)));
}

private boolean shouldSkipGeneration(OpenApiGeneratorOptions options, String fingerprint) {
Path checksumFile = resolveChecksumFile(options.outDir(), options.openApiFilePath());

if (!Files.exists(checksumFile)) {
return false;
}

if (!hasGeneratedFiles(options)) {
return false;
}

try {
String previous = Files.readString(checksumFile);
return previous.equals(fingerprint);
} catch (IOException e) {
return false;
}
}

// Store a checksum based on spec and configuration,
// in order to avoid regenerating code when the latter hasn't changed
private void persistFingerprint(OpenApiGeneratorOptions options, String fingerprint) {
Path checksumFile = resolveChecksumFile(options.outDir(), options.openApiFilePath());

try {
Files.createDirectories(checksumFile.getParent());
Files.writeString(checksumFile, fingerprint);
} catch (IOException e) {
String message = "Unable to persist OpenAPI generation fingerprint for " + options.openApiFilePath();
throw new RuntimeException(message, e);
}
}

private boolean hasGeneratedFiles(OpenApiGeneratorOptions options) {
Path outDir = options.outDir();
if (!Files.isDirectory(outDir)) {
return false;
}

try (Stream<Path> paths = Files.walk(outDir)) {
return paths.anyMatch(path -> Files.isRegularFile(path) && path.toString().endsWith(".java"));
} catch (IOException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package io.quarkiverse.openapi.generator.deployment;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.microprofile.config.Config;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import io.quarkiverse.openapi.generator.deployment.codegen.OpenApiGeneratorCodeGenBase;
import io.smallrye.config.SmallRyeConfig;
import io.smallrye.config.SmallRyeConfigBuilder;

class OpenApiGeneratorCodeGenSkipIfUnchangedTest {

private static final String SPEC_YAML = "petstore.yaml";
private static final String CONFIG_PROPERTY = "quarkus.openapi-generator.codegen.skip-if-unchanged";

private static final String SPEC = """
openapi: 3.0.1
info:
title: Test
version: 1.0.0
paths: {}""";

private static final String ANOTHER_SPEC = """
openapi: 3.0.1
info:
title: Another Test
version: 1.0.0
paths: {}""";

@TempDir
Path tempDir;

@Test
void shouldSkipGenerationWhenEnabledAndUnchanged() throws Exception {
Path spec = tempDir.resolve(SPEC_YAML);
Files.writeString(spec, SPEC);

Path outDir = tempDir.resolve("generated");
Files.createDirectories(outDir);

TestCodegen codegen = new TestCodegen();
Config config = config(Map.of(CONFIG_PROPERTY, "true"));

OpenApiGeneratorOptions options = new OpenApiGeneratorOptions(
config,
spec,
outDir,
tempDir.resolve("templates"),
false);

codegen.generate(options);
codegen.generate(options);

assertThat(codegen.generateCount).hasValue(1);
}

@Test
void shouldGenerateAgainWhenOptionDisabled() throws Exception {
Path spec = tempDir.resolve(SPEC_YAML);
Files.writeString(spec, SPEC);

Path outDir = tempDir.resolve("generated");
Files.createDirectories(outDir);

TestCodegen codegen = new TestCodegen();
Config config = config(Map.of(CONFIG_PROPERTY, "false"));

OpenApiGeneratorOptions options = new OpenApiGeneratorOptions(
config,
spec,
outDir,
tempDir.resolve("templates"),
false);

codegen.generate(options);
codegen.generate(options);

assertThat(codegen.generateCount).hasValue(2);
}

@Test
void shouldGenerateAgainWhenFingerprintChanges() throws Exception {
Path spec = tempDir.resolve(SPEC_YAML);
Files.writeString(spec, SPEC);

Path outDir = tempDir.resolve("generated");
Files.createDirectories(outDir);

TestCodegen codegen = new TestCodegen();
Config config = config(Map.of(CONFIG_PROPERTY, "true"));

OpenApiGeneratorOptions options = new OpenApiGeneratorOptions(
config,
spec,
outDir,
tempDir.resolve("templates"),
false);
codegen.generate(options);

// Change the spec, to trigger a new generation
Files.writeString(spec, ANOTHER_SPEC);
codegen.generate(options);

assertThat(codegen.generateCount).hasValue(2);
}

@Test
void shouldGenerateAgainWhenConfigKeyChanges() throws Exception {
Path spec = tempDir.resolve(SPEC_YAML);
Files.writeString(spec, SPEC);

Path outDir = tempDir.resolve("generated");
Files.createDirectories(outDir);

TestCodegen codegen = new TestCodegen();

Config firstConfig = config(Map.of(
CONFIG_PROPERTY, "true",
"quarkus.openapi-generator.codegen.spec.petstore_yaml.config-key", "petstore",
"quarkus.openapi-generator.codegen.spec.petstore.additional-api-type-annotations", "@org.test.Foo"));

OpenApiGeneratorOptions firstOptions = new OpenApiGeneratorOptions(
firstConfig,
spec,
outDir,
tempDir.resolve("templates"),
false);

codegen.generate(firstOptions);

Config secondConfig = config(Map.of(
CONFIG_PROPERTY, "true",
"quarkus.openapi-generator.codegen.spec.petstore_yaml.config-key", "another",
"quarkus.openapi-generator.codegen.spec.another.additional-api-type-annotations", "@org.test.Foo"));

OpenApiGeneratorOptions secondOptions = new OpenApiGeneratorOptions(
secondConfig,
spec,
outDir,
tempDir.resolve("templates"),
false);

codegen.generate(secondOptions);

assertThat(codegen.generateCount).hasValue(2);
}

private static SmallRyeConfig config(Map<String, String> values) {
SmallRyeConfigBuilder builder = new SmallRyeConfigBuilder();
values.forEach(builder::withDefaultValue);
return builder.build();
}

// Test code generator that generates dummy content
static final class TestCodegen extends OpenApiGeneratorCodeGenBase {
final AtomicInteger generateCount = new AtomicInteger();

@Override
protected void doGenerate(OpenApiGeneratorOptions options) {
generateCount.incrementAndGet();
try {
Path javaFile = options.outDir().resolve("org/example/PetApi.java");
Files.createDirectories(javaFile.getParent());
Files.writeString(javaFile, "package org.example; class PetApi {}");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
protected void generate(final OpenApiGeneratorOptions options) {
super.generate(options);
}

@Override
public String providerId() {
return "openapi-test";
}

@Override
public String[] inputExtensions() {
return new String[] { ".yaml" };
}
}
}
Loading
Loading