Skip to content

Commit 2bc963f

Browse files
authored
Generate merge spec (#14387)
1 parent 0816008 commit 2bc963f

File tree

10 files changed

+490
-2
lines changed

10 files changed

+490
-2
lines changed

modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.commons.lang3.StringUtils;
3333
import org.openapitools.codegen.*;
3434
import org.openapitools.codegen.config.CodegenConfigurator;
35+
import org.openapitools.codegen.config.MergedSpecBuilder;
3536
import org.slf4j.Logger;
3637
import org.slf4j.LoggerFactory;
3738

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

61+
@Option(name = "--input-spec-root-directory", title = "Folder with spec(s)",
62+
description = "Local root folder with spec file(s)")
63+
private String inputSpecRootDirectory;
64+
65+
@Option(name = "--merged-spec-filename", title = "Name of resulted merged specs file (used along with --input-spec-root-directory option)")
66+
private String mergedFileName;
67+
6068
@Option(name = {"-t", "--template-dir"}, title = "template directory",
6169
description = "folder containing the template files")
6270
private String templateDir;
@@ -283,6 +291,12 @@ public class Generate extends OpenApiGeneratorCommand {
283291

284292
@Override
285293
public void execute() {
294+
if (StringUtils.isNotBlank(inputSpecRootDirectory)) {
295+
spec = new MergedSpecBuilder(inputSpecRootDirectory, StringUtils.isBlank(mergedFileName) ? "_merged_spec" : mergedFileName)
296+
.buildMergedSpec();
297+
System.out.println("Merge input spec would be used - " + spec);
298+
}
299+
286300
if (logToStderr != null) {
287301
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
288302
Stream.of(Logger.ROOT_LOGGER_NAME, "io.swagger", "org.openapitools")

modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import org.openapitools.codegen.CodegenConstants
3939
import org.openapitools.codegen.DefaultGenerator
4040
import org.openapitools.codegen.config.CodegenConfigurator
4141
import org.openapitools.codegen.config.GlobalSettings
42+
import org.openapitools.codegen.config.MergedSpecBuilder
4243

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

100+
/**
101+
* Local root folder with spec files
102+
*/
103+
@Optional
104+
@get:InputFile
105+
@PathSensitive(PathSensitivity.RELATIVE)
106+
val inputSpecRootDirectory = project.objects.property<String>();
107+
108+
/**
109+
* Name of the file that will contains all merged specs
110+
*/
111+
@Input
112+
@Optional
113+
val mergedFileName = project.objects.property<String>();
114+
99115
/**
100116
* The remote Open API 2.0/3.x specification URL location.
101117
*/
@@ -527,6 +543,11 @@ open class GenerateTask : DefaultTask() {
527543
@Suppress("unused")
528544
@TaskAction
529545
fun doWork() {
546+
inputSpecRootDirectory.ifNotEmpty { inputSpecRootDirectoryValue -> {
547+
inputSpec.set(MergedSpecBuilder(inputSpecRootDirectoryValue, mergedFileName.get()).buildMergedSpec())
548+
logger.info("Merge input spec would be used - {}", inputSpec.get())
549+
}}
550+
530551
cleanupOutput.ifNotEmpty { cleanup ->
531552
if (cleanup) {
532553
project.delete(outputDir)

modules/openapi-generator-maven-plugin/examples/spring.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,6 @@
125125

126126
<properties>
127127
<swagger-annotations-version>1.5.8</swagger-annotations-version>
128-
129-
<spring-boot-starter-web.version>2.2.1.RELEASE</spring-boot-starter-web.version>
130128
<springfox-version>2.8.0</springfox-version>
131129
</properties>
132130
</project>

modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.openapitools.codegen.auth.AuthParser;
6060
import org.openapitools.codegen.config.CodegenConfigurator;
6161
import org.openapitools.codegen.config.GlobalSettings;
62+
import org.openapitools.codegen.config.MergedSpecBuilder;
6263
import org.sonatype.plexus.build.incremental.BuildContext;
6364
import org.sonatype.plexus.build.incremental.DefaultBuildContext;
6465
import org.slf4j.Logger;
@@ -104,6 +105,18 @@ public class CodeGenMojo extends AbstractMojo {
104105
@Parameter(name = "inputSpec", property = "openapi.generator.maven.plugin.inputSpec", required = true)
105106
private String inputSpec;
106107

108+
/**
109+
* Local root folder with spec files
110+
*/
111+
@Parameter(name = "inputSpecRootDirectory", property = "openapi.generator.maven.plugin.inputSpecRootDirectory")
112+
private String inputSpecRootDirectory;
113+
114+
/**
115+
* Name of the file that will contains all merged specs
116+
*/
117+
@Parameter(name = "mergedFileName", property = "openapi.generator.maven.plugin.mergedFileName", defaultValue = "_merged_spec")
118+
private String mergedFileName;
119+
107120
/**
108121
* Git host, e.g. gitlab.com.
109122
*/
@@ -468,6 +481,12 @@ public void setBuildContext(BuildContext buildContext) {
468481

469482
@Override
470483
public void execute() throws MojoExecutionException {
484+
if (StringUtils.isNotBlank(inputSpecRootDirectory)) {
485+
inputSpec = new MergedSpecBuilder(inputSpecRootDirectory, mergedFileName)
486+
.buildMergedSpec();
487+
LOGGER.info("Merge input spec would be used - {}", inputSpec);
488+
}
489+
471490
File inputSpecFile = new File(inputSpec);
472491

473492
if (output == null) {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package org.openapitools.codegen.config;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.nio.file.Paths;
8+
import java.nio.file.StandardOpenOption;
9+
import java.util.ArrayList;
10+
import java.util.Collections;
11+
import java.util.HashMap;
12+
import java.util.List;
13+
import java.util.Locale;
14+
import java.util.Map;
15+
import java.util.Set;
16+
import java.util.stream.Collectors;
17+
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
23+
import com.google.common.collect.ImmutableMap;
24+
25+
import io.swagger.parser.OpenAPIParser;
26+
import io.swagger.v3.oas.models.OpenAPI;
27+
import io.swagger.v3.parser.core.models.ParseOptions;
28+
29+
public class MergedSpecBuilder {
30+
31+
private static final Logger LOGGER = LoggerFactory.getLogger(MergedSpecBuilder.class);
32+
33+
private final String inputSpecRootDirectory;
34+
private final String mergeFileName;
35+
36+
public MergedSpecBuilder(final String rootDirectory, final String mergeFileName) {
37+
this.inputSpecRootDirectory = rootDirectory;
38+
this.mergeFileName = mergeFileName;
39+
}
40+
41+
public String buildMergedSpec() {
42+
deleteMergedFileFromPreviousRun();
43+
List<String> specRelatedPaths = getAllSpecFilesInDirectory();
44+
if (specRelatedPaths.isEmpty()) {
45+
throw new RuntimeException("Spec directory doesn't contains any specification");
46+
}
47+
LOGGER.info("In spec root directory {} found specs {}", inputSpecRootDirectory, specRelatedPaths);
48+
49+
String openapiVersion = null;
50+
boolean isJson = false;
51+
ParseOptions options = new ParseOptions();
52+
options.setResolve(true);
53+
List<SpecWithPaths> allPaths = new ArrayList<>();
54+
55+
for (String specRelatedPath : specRelatedPaths) {
56+
String specPath = inputSpecRootDirectory + File.separator + specRelatedPath;
57+
try {
58+
LOGGER.info("Reading spec: {}", specPath);
59+
60+
OpenAPI result = new OpenAPIParser()
61+
.readLocation(specPath, new ArrayList<>(), options)
62+
.getOpenAPI();
63+
64+
if (openapiVersion == null) {
65+
openapiVersion = result.getOpenapi();
66+
if (specRelatedPath.toLowerCase(Locale.ROOT).endsWith(".json")) {
67+
isJson = true;
68+
}
69+
}
70+
allPaths.add(new SpecWithPaths(specRelatedPath, result.getPaths().keySet()));
71+
} catch (Exception e) {
72+
LOGGER.error("Failed to read file: {}. It would be ignored", specPath);
73+
}
74+
}
75+
76+
Map<String, Object> mergedSpec = generatedMergedSpec(openapiVersion, allPaths);
77+
String mergedFilename = this.mergeFileName + (isJson ? ".json" : ".yaml");
78+
Path mergedFilePath = Paths.get(inputSpecRootDirectory, mergedFilename);
79+
80+
try {
81+
ObjectMapper objectMapper = isJson ? new ObjectMapper() : new ObjectMapper(new YAMLFactory());
82+
Files.write(mergedFilePath, objectMapper.writeValueAsBytes(mergedSpec), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
83+
} catch (IOException e) {
84+
throw new RuntimeException(e);
85+
}
86+
87+
return mergedFilePath.toString();
88+
}
89+
90+
private static Map<String, Object> generatedMergedSpec(String openapiVersion, List<SpecWithPaths> allPaths) {
91+
Map<String, Object> spec = generateHeader(openapiVersion);
92+
Map<String, Object> paths = new HashMap<>();
93+
spec.put("paths", paths);
94+
95+
for(SpecWithPaths specWithPaths : allPaths) {
96+
for (String path : specWithPaths.paths) {
97+
String specRelatedPath = "./" + specWithPaths.specRelatedPath + "#/paths/" + path.replace("/", "~1");
98+
paths.put(path, ImmutableMap.of(
99+
"$ref", specRelatedPath
100+
));
101+
}
102+
}
103+
104+
return spec;
105+
}
106+
107+
private static Map<String, Object> generateHeader(String openapiVersion) {
108+
Map<String, Object> map = new HashMap<>();
109+
map.put("openapi", openapiVersion);
110+
map.put("info", ImmutableMap.of(
111+
"title", "merged spec",
112+
"description", "merged spec",
113+
"version", "1.0.0"
114+
));
115+
map.put("servers", Collections.singleton(
116+
ImmutableMap.of("url", "http://localhost:8080")
117+
));
118+
return map;
119+
}
120+
121+
private List<String> getAllSpecFilesInDirectory() {
122+
Path rootDirectory = new File(inputSpecRootDirectory).toPath();
123+
try {
124+
return Files.walk(rootDirectory)
125+
.filter(path -> !Files.isDirectory(path))
126+
.map(path -> rootDirectory.relativize(path).toString())
127+
.collect(Collectors.toList());
128+
} catch (IOException e) {
129+
throw new RuntimeException("Exception while listing files in spec root directory: " + inputSpecRootDirectory, e);
130+
}
131+
}
132+
133+
private void deleteMergedFileFromPreviousRun() {
134+
try {
135+
Files.deleteIfExists(Paths.get(inputSpecRootDirectory + File.separator + mergeFileName + ".json"));
136+
} catch (IOException e) { }
137+
try {
138+
Files.deleteIfExists(Paths.get(inputSpecRootDirectory + File.separator + mergeFileName + ".yaml"));
139+
} catch (IOException e) { }
140+
}
141+
142+
private static class SpecWithPaths {
143+
private final String specRelatedPath;
144+
private final Set<String> paths;
145+
146+
private SpecWithPaths(final String specRelatedPath, final Set<String> paths) {
147+
this.specRelatedPath = specRelatedPath;
148+
this.paths = paths;
149+
}
150+
}
151+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package org.openapitools.codegen.config;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.nio.file.Files;
6+
import java.nio.file.Paths;
7+
import java.util.Map;
8+
import java.util.function.Function;
9+
import java.util.stream.Collectors;
10+
11+
import org.openapitools.codegen.ClientOptInput;
12+
import org.openapitools.codegen.DefaultGenerator;
13+
import org.openapitools.codegen.java.assertions.JavaFileAssert;
14+
import org.openapitools.codegen.languages.SpringCodegen;
15+
import org.testng.annotations.Test;
16+
17+
import com.google.common.collect.ImmutableMap;
18+
19+
import io.swagger.parser.OpenAPIParser;
20+
import io.swagger.v3.oas.models.OpenAPI;
21+
import io.swagger.v3.parser.core.models.ParseOptions;
22+
23+
public class MergedSpecBuilderTest {
24+
25+
@Test
26+
public void shouldMergeYamlSpecs() throws IOException {
27+
mergeSpecs("yaml");
28+
}
29+
30+
@Test
31+
public void shouldMergeJsonSpecs() throws IOException {
32+
mergeSpecs("json");
33+
}
34+
35+
private void mergeSpecs(String fileExt) throws IOException {
36+
File output = Files.createTempDirectory("spec-directory").toFile().getCanonicalFile();
37+
output.deleteOnExit();
38+
39+
Files.copy(Paths.get("src/test/resources/bugs/mergerTest/spec1." + fileExt), output.toPath().resolve("spec1." + fileExt));
40+
Files.copy(Paths.get("src/test/resources/bugs/mergerTest/spec2." + fileExt), output.toPath().resolve("spec2." + fileExt));
41+
42+
String outputPath = output.getAbsolutePath().replace('\\', '/');
43+
44+
String mergedSpec = new MergedSpecBuilder(outputPath, "_merged_file")
45+
.buildMergedSpec();
46+
47+
assertFilesFromMergedSpec(mergedSpec);
48+
}
49+
50+
private void assertFilesFromMergedSpec(String mergedSpec) throws IOException {
51+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
52+
output.deleteOnExit();
53+
54+
ParseOptions parseOptions = new ParseOptions();
55+
parseOptions.setResolve(true);
56+
OpenAPI openAPI = new OpenAPIParser()
57+
.readLocation(mergedSpec, null, parseOptions).getOpenAPI();
58+
59+
SpringCodegen codegen = new SpringCodegen();
60+
codegen.setOutputDir(output.getAbsolutePath());
61+
62+
ClientOptInput input = new ClientOptInput();
63+
input.openAPI(openAPI);
64+
input.config(codegen);
65+
66+
DefaultGenerator generator = new DefaultGenerator();
67+
Map<String, File> files = generator.opts(input).generate().stream()
68+
.collect(Collectors.toMap(File::getName, Function.identity()));
69+
70+
JavaFileAssert.assertThat(files.get("Spec1Api.java"))
71+
.assertMethod("spec1Operation").hasReturnType("ResponseEntity<Spec1Model>")
72+
73+
.toFileAssert()
74+
75+
.assertMethod("spec1OperationComplex")
76+
.hasReturnType("ResponseEntity<Spec1Model>")
77+
.assertMethodAnnotations()
78+
.containsWithNameAndAttributes("RequestMapping", ImmutableMap.of("value", "\"/spec1/complex/{param1}/path\""))
79+
.toMethod()
80+
.hasParameter("param1")
81+
.withType("String")
82+
.assertParameterAnnotations()
83+
.containsWithNameAndAttributes("PathVariable", ImmutableMap.of("value", "\"param1\""));
84+
85+
JavaFileAssert.assertThat(files.get("Spec2Api.java"))
86+
.assertMethod("spec2Operation").hasReturnType("ResponseEntity<Spec2Model>");
87+
88+
JavaFileAssert.assertThat(files.get("Spec1Model.java"))
89+
.assertMethod("getSpec1Field").hasReturnType("String");
90+
91+
JavaFileAssert.assertThat(files.get("Spec2Model.java"))
92+
.assertMethod("getSpec2Field").hasReturnType("BigDecimal");
93+
}
94+
95+
}

0 commit comments

Comments
 (0)