Skip to content
Merged
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 @@ -32,6 +32,7 @@
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.config.MergedSpecBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -57,6 +58,13 @@ public class Generate extends OpenApiGeneratorCommand {
description = "location of the OpenAPI spec, as URL or file (required if not loaded via config using -c)")
private String spec;

@Option(name = "--input-spec-root-directory", title = "Folder with spec(s)",
description = "Local root folder with spec file(s)")
private String inputSpecRootDirectory;

@Option(name = "--merged-spec-filename", title = "Name of resulted merged specs file (used along with --input-spec-root-directory option)")
private String mergedFileName;

@Option(name = {"-t", "--template-dir"}, title = "template directory",
description = "folder containing the template files")
private String templateDir;
Expand Down Expand Up @@ -283,6 +291,12 @@ public class Generate extends OpenApiGeneratorCommand {

@Override
public void execute() {
if (StringUtils.isNotBlank(inputSpecRootDirectory)) {
spec = new MergedSpecBuilder(inputSpecRootDirectory, StringUtils.isBlank(mergedFileName) ? "_merged_spec" : mergedFileName)
.buildMergedSpec();
System.out.println("Merge input spec would be used - " + spec);
}

if (logToStderr != null) {
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
Stream.of(Logger.ROOT_LOGGER_NAME, "io.swagger", "org.openapitools")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import org.openapitools.codegen.CodegenConstants
import org.openapitools.codegen.DefaultGenerator
import org.openapitools.codegen.config.CodegenConfigurator
import org.openapitools.codegen.config.GlobalSettings
import org.openapitools.codegen.config.MergedSpecBuilder

/**
* A task which generates the desired code.
Expand Down Expand Up @@ -96,6 +97,21 @@ open class GenerateTask : DefaultTask() {
@PathSensitive(PathSensitivity.RELATIVE)
val inputSpec = project.objects.property<String>()

/**
* Local root folder with spec files
*/
@Optional
@get:InputFile
@PathSensitive(PathSensitivity.RELATIVE)
val inputSpecRootDirectory = project.objects.property<String>();

/**
* Name of the file that will contains all merged specs
*/
@Input
@Optional
val mergedFileName = project.objects.property<String>();

/**
* The remote Open API 2.0/3.x specification URL location.
*/
Expand Down Expand Up @@ -527,6 +543,11 @@ open class GenerateTask : DefaultTask() {
@Suppress("unused")
@TaskAction
fun doWork() {
inputSpecRootDirectory.ifNotEmpty { inputSpecRootDirectoryValue -> {
inputSpec.set(MergedSpecBuilder(inputSpecRootDirectoryValue, mergedFileName.get()).buildMergedSpec())
logger.info("Merge input spec would be used - {}", inputSpec.get())
}}

cleanupOutput.ifNotEmpty { cleanup ->
if (cleanup) {
project.delete(outputDir)
Expand Down
2 changes: 0 additions & 2 deletions modules/openapi-generator-maven-plugin/examples/spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,6 @@

<properties>
<swagger-annotations-version>1.5.8</swagger-annotations-version>

<spring-boot-starter-web.version>2.2.1.RELEASE</spring-boot-starter-web.version>
<springfox-version>2.8.0</springfox-version>
</properties>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import org.openapitools.codegen.auth.AuthParser;
import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.config.MergedSpecBuilder;
import org.sonatype.plexus.build.incremental.BuildContext;
import org.sonatype.plexus.build.incremental.DefaultBuildContext;
import org.slf4j.Logger;
Expand Down Expand Up @@ -104,6 +105,18 @@ public class CodeGenMojo extends AbstractMojo {
@Parameter(name = "inputSpec", property = "openapi.generator.maven.plugin.inputSpec", required = true)
private String inputSpec;

/**
* Local root folder with spec files
*/
@Parameter(name = "inputSpecRootDirectory", property = "openapi.generator.maven.plugin.inputSpecRootDirectory")
private String inputSpecRootDirectory;

/**
* Name of the file that will contains all merged specs
*/
@Parameter(name = "mergedFileName", property = "openapi.generator.maven.plugin.mergedFileName", defaultValue = "_merged_spec")
private String mergedFileName;

/**
* Git host, e.g. gitlab.com.
*/
Expand Down Expand Up @@ -468,6 +481,12 @@ public void setBuildContext(BuildContext buildContext) {

@Override
public void execute() throws MojoExecutionException {
if (StringUtils.isNotBlank(inputSpecRootDirectory)) {
inputSpec = new MergedSpecBuilder(inputSpecRootDirectory, mergedFileName)
.buildMergedSpec();
LOGGER.info("Merge input spec would be used - {}", inputSpec);
}

File inputSpecFile = new File(inputSpec);

if (output == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package org.openapitools.codegen.config;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.google.common.collect.ImmutableMap;

import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.parser.core.models.ParseOptions;

public class MergedSpecBuilder {

private static final Logger LOGGER = LoggerFactory.getLogger(MergedSpecBuilder.class);

private final String inputSpecRootDirectory;
private final String mergeFileName;

public MergedSpecBuilder(final String rootDirectory, final String mergeFileName) {
this.inputSpecRootDirectory = rootDirectory;
this.mergeFileName = mergeFileName;
}

public String buildMergedSpec() {
deleteMergedFileFromPreviousRun();
List<String> specRelatedPaths = getAllSpecFilesInDirectory();
if (specRelatedPaths.isEmpty()) {
throw new RuntimeException("Spec directory doesn't contains any specification");
}
LOGGER.info("In spec root directory {} found specs {}", inputSpecRootDirectory, specRelatedPaths);

String openapiVersion = null;
boolean isJson = false;
ParseOptions options = new ParseOptions();
options.setResolve(true);
List<SpecWithPaths> allPaths = new ArrayList<>();

for (String specRelatedPath : specRelatedPaths) {
String specPath = inputSpecRootDirectory + File.separator + specRelatedPath;
try {
LOGGER.info("Reading spec: {}", specPath);

OpenAPI result = new OpenAPIParser()
.readLocation(specPath, new ArrayList<>(), options)
.getOpenAPI();

if (openapiVersion == null) {
openapiVersion = result.getOpenapi();
if (specRelatedPath.toLowerCase(Locale.ROOT).endsWith(".json")) {
isJson = true;
}
}
allPaths.add(new SpecWithPaths(specRelatedPath, result.getPaths().keySet()));
} catch (Exception e) {
LOGGER.error("Failed to read file: {}. It would be ignored", specPath);
}
}

Map<String, Object> mergedSpec = generatedMergedSpec(openapiVersion, allPaths);
String mergedFilename = this.mergeFileName + (isJson ? ".json" : ".yaml");
Path mergedFilePath = Paths.get(inputSpecRootDirectory, mergedFilename);

try {
ObjectMapper objectMapper = isJson ? new ObjectMapper() : new ObjectMapper(new YAMLFactory());
Files.write(mergedFilePath, objectMapper.writeValueAsBytes(mergedSpec), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
} catch (IOException e) {
throw new RuntimeException(e);
}

return mergedFilePath.toString();
}

private static Map<String, Object> generatedMergedSpec(String openapiVersion, List<SpecWithPaths> allPaths) {
Map<String, Object> spec = generateHeader(openapiVersion);
Map<String, Object> paths = new HashMap<>();
spec.put("paths", paths);

for(SpecWithPaths specWithPaths : allPaths) {
for (String path : specWithPaths.paths) {
String specRelatedPath = "./" + specWithPaths.specRelatedPath + "#/paths/" + path.replace("/", "~1");
paths.put(path, ImmutableMap.of(
"$ref", specRelatedPath
));
}
}

return spec;
}

private static Map<String, Object> generateHeader(String openapiVersion) {
Map<String, Object> map = new HashMap<>();
map.put("openapi", openapiVersion);
map.put("info", ImmutableMap.of(
"title", "merged spec",
"description", "merged spec",
"version", "1.0.0"
));
map.put("servers", Collections.singleton(
ImmutableMap.of("url", "http://localhost:8080")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, why not include the servers array from one or more of the input files?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have abandoned this plugin and use redocly

tasks.register<Exec>("mergeOpenApiSpecs") {
    val specs = projectDir.resolve("src/main/resources/openapi").listFiles()!!.filter { it.name != "merged.yml" }
        .map { it.name }.toTypedArray()
    commandLine(
        "docker",
        "run",
        "--rm",
        "-v",
        "${projectDir.resolve("src/main/resources/openapi").absolutePath}:/spec",
        "redocly/cli:1.12.2",
        "join",
        "--without-x-tag-groups",
        *specs,
        "-o",
        "merged.yml"
    )
}

Copy link
Copy Markdown

@gd-scalemem gd-scalemem Aug 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing the merge explicitly is not that hard and offers more flexibility, but I guess it's useful to have a built-in one that does the job for many use cases. @borsch

));
return map;
}

private List<String> getAllSpecFilesInDirectory() {
Path rootDirectory = new File(inputSpecRootDirectory).toPath();
try {
return Files.walk(rootDirectory)
.filter(path -> !Files.isDirectory(path))
.map(path -> rootDirectory.relativize(path).toString())
.collect(Collectors.toList());
} catch (IOException e) {
throw new RuntimeException("Exception while listing files in spec root directory: " + inputSpecRootDirectory, e);
}
}

private void deleteMergedFileFromPreviousRun() {
try {
Files.deleteIfExists(Paths.get(inputSpecRootDirectory + File.separator + mergeFileName + ".json"));
} catch (IOException e) { }
try {
Files.deleteIfExists(Paths.get(inputSpecRootDirectory + File.separator + mergeFileName + ".yaml"));
} catch (IOException e) { }
}

private static class SpecWithPaths {
private final String specRelatedPath;
private final Set<String> paths;

private SpecWithPaths(final String specRelatedPath, final Set<String> paths) {
this.specRelatedPath = specRelatedPath;
this.paths = paths;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.openapitools.codegen.config;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.openapitools.codegen.ClientOptInput;
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.java.assertions.JavaFileAssert;
import org.openapitools.codegen.languages.SpringCodegen;
import org.testng.annotations.Test;

import com.google.common.collect.ImmutableMap;

import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.parser.core.models.ParseOptions;

public class MergedSpecBuilderTest {

@Test
public void shouldMergeYamlSpecs() throws IOException {
mergeSpecs("yaml");
}

@Test
public void shouldMergeJsonSpecs() throws IOException {
mergeSpecs("json");
}

private void mergeSpecs(String fileExt) throws IOException {
File output = Files.createTempDirectory("spec-directory").toFile().getCanonicalFile();
output.deleteOnExit();

Files.copy(Paths.get("src/test/resources/bugs/mergerTest/spec1." + fileExt), output.toPath().resolve("spec1." + fileExt));
Files.copy(Paths.get("src/test/resources/bugs/mergerTest/spec2." + fileExt), output.toPath().resolve("spec2." + fileExt));

String outputPath = output.getAbsolutePath().replace('\\', '/');

String mergedSpec = new MergedSpecBuilder(outputPath, "_merged_file")
.buildMergedSpec();

assertFilesFromMergedSpec(mergedSpec);
}

private void assertFilesFromMergedSpec(String mergedSpec) throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

ParseOptions parseOptions = new ParseOptions();
parseOptions.setResolve(true);
OpenAPI openAPI = new OpenAPIParser()
.readLocation(mergedSpec, null, parseOptions).getOpenAPI();

SpringCodegen codegen = new SpringCodegen();
codegen.setOutputDir(output.getAbsolutePath());

ClientOptInput input = new ClientOptInput();
input.openAPI(openAPI);
input.config(codegen);

DefaultGenerator generator = new DefaultGenerator();
Map<String, File> files = generator.opts(input).generate().stream()
.collect(Collectors.toMap(File::getName, Function.identity()));

JavaFileAssert.assertThat(files.get("Spec1Api.java"))
.assertMethod("spec1Operation").hasReturnType("ResponseEntity<Spec1Model>")

.toFileAssert()

.assertMethod("spec1OperationComplex")
.hasReturnType("ResponseEntity<Spec1Model>")
.assertMethodAnnotations()
.containsWithNameAndAttributes("RequestMapping", ImmutableMap.of("value", "\"/spec1/complex/{param1}/path\""))
.toMethod()
.hasParameter("param1")
.withType("String")
.assertParameterAnnotations()
.containsWithNameAndAttributes("PathVariable", ImmutableMap.of("value", "\"param1\""));

JavaFileAssert.assertThat(files.get("Spec2Api.java"))
.assertMethod("spec2Operation").hasReturnType("ResponseEntity<Spec2Model>");

JavaFileAssert.assertThat(files.get("Spec1Model.java"))
.assertMethod("getSpec1Field").hasReturnType("String");

JavaFileAssert.assertThat(files.get("Spec2Model.java"))
.assertMethod("getSpec2Field").hasReturnType("BigDecimal");
}

}
Loading