forked from loafle/openapi-generator-original
Generate merge spec (#14387)
This commit is contained in:
parent
0816008f1e
commit
2bc963f00c
@ -32,6 +32,7 @@ import java.util.stream.Stream;
|
||||
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;
|
||||
|
||||
@ -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;
|
||||
@ -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")
|
||||
|
@ -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.
|
||||
@ -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.
|
||||
*/
|
||||
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -59,6 +59,7 @@ import org.openapitools.codegen.DefaultGenerator;
|
||||
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;
|
||||
@ -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.
|
||||
*/
|
||||
@ -468,6 +481,12 @@ public class CodeGenMojo extends AbstractMojo {
|
||||
|
||||
@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) {
|
||||
|
@ -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")
|
||||
));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Specification to reproduce nullable issue with Array",
|
||||
"title": "ArrayNullableTest Api"
|
||||
},
|
||||
"paths": {
|
||||
"/spec1": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"spec1"
|
||||
],
|
||||
"summary": "dummy",
|
||||
"operationId": "spec1Operation",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Spec1Model"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/spec1/complex/{param1}/path": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"spec1"
|
||||
],
|
||||
"summary": "dummy",
|
||||
"operationId": "spec1OperationComplex",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "param1",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Spec1Model"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Spec1Model": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spec1Field": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
version: 1.0.0
|
||||
description: Specification to reproduce nullable issue with Array
|
||||
title: ArrayNullableTest Api
|
||||
paths:
|
||||
/spec1:
|
||||
get:
|
||||
tags:
|
||||
- spec1
|
||||
summary: dummy
|
||||
operationId: spec1Operation
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Spec1Model'
|
||||
/spec1/complex/{param1}/path:
|
||||
get:
|
||||
tags:
|
||||
- spec1
|
||||
summary: dummy
|
||||
operationId: spec1OperationComplex
|
||||
parameters:
|
||||
- in: path
|
||||
name: param1
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Spec1Model'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Spec1Model:
|
||||
type: object
|
||||
properties:
|
||||
spec1Field:
|
||||
type: string
|
@ -0,0 +1,43 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Specification to reproduce nullable issue with Array",
|
||||
"title": "ArrayNullableTest Api"
|
||||
},
|
||||
"paths": {
|
||||
"/spec2": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"spec2"
|
||||
],
|
||||
"summary": "dummy",
|
||||
"operationId": "spec2Operation",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Spec2Model"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Spec2Model": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spec2Field": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
version: 1.0.0
|
||||
description: Specification to reproduce nullable issue with Array
|
||||
title: ArrayNullableTest Api
|
||||
paths:
|
||||
/spec2:
|
||||
get:
|
||||
tags:
|
||||
- spec2
|
||||
summary: dummy
|
||||
operationId: spec2Operation
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Spec2Model'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Spec2Model:
|
||||
type: object
|
||||
properties:
|
||||
spec2Field:
|
||||
type: number
|
Loading…
x
Reference in New Issue
Block a user