diff --git a/codegen-cli/src/main/resources/config/service.yml b/codegen-cli/src/main/resources/config/service.yml index b70bc2fd1..286553c47 100644 --- a/codegen-cli/src/main/resources/config/service.yml +++ b/codegen-cli/src/main/resources/config/service.yml @@ -9,3 +9,4 @@ singletons: - com.networknt.codegen.graphql.GraphqlGenerator - com.networknt.codegen.eventuate.EventuateOpenApiGenerator - com.networknt.codegen.rest.OpenApiKotlinGenerator + - com.networknt.codegen.rest.OpenApiSpecGenerator diff --git a/light-rest-4j/pom.xml b/light-rest-4j/pom.xml index acef130fa..1dac6e50c 100644 --- a/light-rest-4j/pom.xml +++ b/light-rest-4j/pom.xml @@ -75,7 +75,15 @@ org.yaml snakeyaml - + + + io.github.classgraph + classgraph + + + io.swagger.core.v3 + swagger-core + ch.qos.logback logback-classic diff --git a/light-rest-4j/src/main/java/com/networknt/codegen/rest/OpenApiSpecGenerator.java b/light-rest-4j/src/main/java/com/networknt/codegen/rest/OpenApiSpecGenerator.java new file mode 100644 index 000000000..7e90f9eef --- /dev/null +++ b/light-rest-4j/src/main/java/com/networknt/codegen/rest/OpenApiSpecGenerator.java @@ -0,0 +1,184 @@ +package com.networknt.codegen.rest; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.jsoniter.any.Any; +import com.networknt.codegen.Generator; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; + +public class OpenApiSpecGenerator implements Generator { + private static final Logger logger = LoggerFactory.getLogger(OpenApiSpecGenerator.class); + + private static final String FRAMEWORK="openapi-spec"; + + /** -- configuration items begin -- */ + private static final String CONFIG_SPECGENERATION ="specGeneration"; + // comma delimited package names + private static final String CONFIG_MODELPACKAGES ="modelPackages"; + // absolute path of an existing spec file. If this is specified, the generated models (a.k.a. components) will be added to this file and override existing schemas, if any. + private static final String CONFIG_MERGETO ="mergeTo"; + // comma delimited formats, currently support json, yml, or yaml + private static final String CONFIG_OUTPUTFORMAT="outputFormat"; + // the output file name without extension + private static final String CONFIG_OUTPUTFILENAME="outputFilename"; + /** -- configuration items end -- */ + + private static final String DOT = "."; + private static final String COMMA_SPACE = "\\s*,\\s*"; + private static final String JSON="json"; + private static final String YAML="yaml"; + private static final String YML="yml"; + private static final String DEFAULT_OUTPUT_NAME="openapi_generated"; + + + @Override + public String getFramework() { + return FRAMEWORK; + } + + @SuppressWarnings("rawtypes") + @Override + public void generate(String targetPath, Object model, Any config) throws IOException { + if (StringUtils.isBlank(targetPath)) { + logger.error("Output location is not specified."); + return; + } + + if (!config.keys().contains(CONFIG_SPECGENERATION)) { + logger.error("Missing config: cannot find {} in the specified config file", CONFIG_SPECGENERATION); + return; + } + + Map genConfig = config.get(CONFIG_SPECGENERATION).asMap(); + String modelPackages = StringUtils.trimToEmpty(genConfig.get(CONFIG_MODELPACKAGES).toString()); + String mergeTo = StringUtils.trimToEmpty(genConfig.get(CONFIG_MERGETO).toString()); + String outputFormat = StringUtils.trimToEmpty(genConfig.get(CONFIG_OUTPUTFORMAT).toString()); + String outputFilename = StringUtils.trimToEmpty(genConfig.get(CONFIG_OUTPUTFILENAME).toString()); + + File output_dir = new File(targetPath); + + if (!output_dir.exists() || !output_dir.isDirectory()) { + output_dir.mkdirs(); + } + + String[] basePackageArray = modelPackages.split(COMMA_SPACE); + + Map schemas = new HashMap<>(); + + for (String packageName: basePackageArray) { + try (ScanResult scanResult = + new ClassGraph() + .enableClassInfo() + .whitelistPackages(packageName) + .scan()) { + + List> classes = scanResult.getAllClasses().loadClasses(); + + for (Class cls: classes) { + schemas.putAll(ModelConverters.getInstance().read(cls)); + } + } + } + + OpenAPI openApi = new OpenAPI(); + + openApi.setComponents(new Components().schemas(schemas)); + + openApi = merge(openApi, mergeTo); + + String[] formats = outputFormat.split(COMMA_SPACE); + + String filename = StringUtils.isBlank(outputFilename)?DEFAULT_OUTPUT_NAME:outputFilename; + + for (String format: formats) { + dump(openApi, format, new File(output_dir, filename + DOT + format)); + } + } + + private void dump(OpenAPI openApi, String format, File outputFile) throws IOException { + String specStr=StringUtils.EMPTY; + + if (StringUtils.equalsIgnoreCase(format, JSON)) { + specStr = Json.pretty(openApi); + }else if (StringUtils.equalsIgnoreCase(format, YML) || StringUtils.equalsIgnoreCase(format, YAML)){ + specStr = Yaml.pretty(openApi); + }else { + throw new UnsupportedOperationException("Unknow output format " + format); + } + + Files.write(Paths.get(outputFile.toURI()), specStr.getBytes()); + } + + @SuppressWarnings("rawtypes") + private OpenAPI merge(OpenAPI generatedSpec, String mergeTo) { + if (StringUtils.isNotBlank(mergeTo)) { + File destFile = new File(mergeTo); + + if (destFile.isFile()) { + try { + OpenAPI openAPI=null; + String ext = getFileExtension(destFile); + if (StringUtils.equalsIgnoreCase(ext, JSON)) { + openAPI = Json.mapper().readValue(destFile, OpenAPI.class); + }else if (StringUtils.equalsIgnoreCase(ext, YML)||StringUtils.equalsIgnoreCase(ext, YAML)) { + openAPI = Yaml.mapper().readValue(destFile, OpenAPI.class); + }else { + throw new UnsupportedOperationException("Unknow file format " + ext); + } + + if (null!=openAPI) { + Components components = openAPI.getComponents(); + + if (null == components) { + components = new Components(); + } + + Map schemas = new HashMap<>(); + + schemas.putAll(components.getSchemas()); + + schemas.putAll(generatedSpec.getComponents().getSchemas()); + + components.setSchemas(schemas); + + openAPI.setComponents(components); + + return openAPI; + } + + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + } + + return generatedSpec; + } + + private String getFileExtension(File file) { + String name = file.getName(); + int lastIndexOf = name.lastIndexOf(DOT); + if (lastIndexOf < 0) { + return StringUtils.EMPTY; + } + return name.substring(lastIndexOf+1); + } +} diff --git a/light-rest-4j/src/test/java/com/networknt/codegen/rest/OpenApiSpecGeneratorTest.java b/light-rest-4j/src/test/java/com/networknt/codegen/rest/OpenApiSpecGeneratorTest.java new file mode 100644 index 000000000..f6a1bc231 --- /dev/null +++ b/light-rest-4j/src/test/java/com/networknt/codegen/rest/OpenApiSpecGeneratorTest.java @@ -0,0 +1,25 @@ +package com.networknt.codegen.rest; + +import java.io.IOException; + +import org.junit.Ignore; +import org.junit.Test; + +import com.jsoniter.JsonIterator; +import com.jsoniter.any.Any; +import com.networknt.codegen.OpenApiGeneratorTest; + +@Ignore +public class OpenApiSpecGeneratorTest { + private static final String configName = "/config.json"; + private static final String outputDir = "/tmp/codegen/"; + + @Test + public void test() throws IOException { + Any anyConfig = JsonIterator.parse(OpenApiGeneratorTest.class.getResourceAsStream(configName), 1024).readAny(); + + OpenApiSpecGenerator generator = new OpenApiSpecGenerator(); + + generator.generate(outputDir, null, anyConfig); + } +} diff --git a/light-rest-4j/src/test/resources/config.json b/light-rest-4j/src/test/resources/config.json index 72df874d4..72636e4fd 100644 --- a/light-rest-4j/src/test/resources/config.json +++ b/light-rest-4j/src/test/resources/config.json @@ -33,5 +33,11 @@ "handerl.yml", "values.yml" ] + }, + "specGeneration": { + "modelPackages": "com.networknt.petstore.model", + "mergeTo": "/tmp/codegen/openapi.json", + "outputFormat": "yaml, json", + "outputFilename": "openapi_gen_test" } } diff --git a/pom.xml b/pom.xml index 0e021b506..5853a991d 100644 --- a/pom.xml +++ b/pom.xml @@ -90,16 +90,18 @@ 3.5.3-beta 0.3 4.0.0 - 1.20 + 1.24 1.2.1 1.72 2.18.1 8.0 2.0-M9 2.4 + 4.8.22 + 2.0.7 -Xmx512m -XX:MaxPermSize=256m - + codegen-core light-rest-4j @@ -297,6 +299,16 @@ qdox ${version.qdox} + + io.github.classgraph + classgraph + ${versions.classgraph} + + + io.swagger.core.v3 + swagger-core + ${version.swagger} +