diff --git a/.travis.yml b/.travis.yml index ddf43cf484c..359a3b0688b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -148,8 +148,8 @@ script: - /bin/bash ./bin/utils/detect_tab_in_java_class.sh # run integration tests defined in maven pom.xml # WARN: Travis will timeout after 10 minutes of no stdout/stderr activity, which is problematic with mvn --quiet. - - mvn --no-snapshot-updates --quiet --batch-mode --show-version clean install -Dorg.slf4j.simpleLogger.defaultLogLevel=error - - mvn --no-snapshot-updates --quiet --batch-mode --show-version verify -Psamples -Dorg.slf4j.simpleLogger.defaultLogLevel=error + - mvn -e --no-snapshot-updates --quiet --batch-mode --show-version clean install -Dorg.slf4j.simpleLogger.defaultLogLevel=error + - mvn -e --no-snapshot-updates --quiet --batch-mode --show-version verify -Psamples -Dorg.slf4j.simpleLogger.defaultLogLevel=error after_success: # push to maven repo - if [ $SONATYPE_USERNAME ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then diff --git a/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/WorkflowSettings.java b/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/WorkflowSettings.java index 68d8b2e9b29..8d24d3b0a50 100644 --- a/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/WorkflowSettings.java +++ b/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/WorkflowSettings.java @@ -29,6 +29,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; /** * Represents those settings applied to a generation workflow. @@ -444,10 +445,20 @@ public class WorkflowSettings { uri = f.toURI(); this.templateDir = Paths.get(uri).toAbsolutePath().normalize().toString(); } else { - URL url = this.getClass().getClassLoader().getResource(templateDir); + String cpDir; + // HACK: this duplicates TemplateManager.getCPResourcePath a bit. We should probably move that function to core. + if (!"/".equals(File.separator)) { + // Windows users may pass path specific to OS, but classpath must be "/" separators + cpDir = templateDir.replaceAll(Pattern.quote(File.separator), "/"); + } else { + cpDir = templateDir; + } + + URL url = this.getClass().getClassLoader().getResource(cpDir); if (url != null) { try { uri = url.toURI(); + // we can freely set to templateDir here and allow templating to manage template lookups this.templateDir = templateDir; } catch (URISyntaxException e) { LOGGER.warn("The requested template was found on the classpath, but resulted in a syntax error."); diff --git a/modules/openapi-generator-maven-plugin/examples/java-client.xml b/modules/openapi-generator-maven-plugin/examples/java-client.xml index c966e5c63ee..caa28302a7f 100644 --- a/modules/openapi-generator-maven-plugin/examples/java-client.xml +++ b/modules/openapi-generator-maven-plugin/examples/java-client.xml @@ -53,8 +53,7 @@ java - + ${project.basedir}/templates diff --git a/modules/openapi-generator-maven-plugin/examples/templates/README.mustache b/modules/openapi-generator-maven-plugin/examples/templates/README.mustache new file mode 100644 index 00000000000..cee4f752e72 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/examples/templates/README.mustache @@ -0,0 +1,21 @@ +# TEST TEST TEST + +# {{artifactId}} + +{{appName}} + +- API version: {{appVersion}} +{{^hideGenerationTimestamp}} + +- Build date: {{generatedDate}} +{{/hideGenerationTimestamp}} + +{{#appDescriptionWithNewLines}}{{{appDescriptionWithNewLines}}}{{/appDescriptionWithNewLines}} + +{{#infoUrl}} + For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) +{{/infoUrl}} + +*Automatically generated by the [OpenAPI Generator](https://openapi-generator.tech)* + +… etc. diff --git a/modules/openapi-generator-maven-plugin/pom.xml b/modules/openapi-generator-maven-plugin/pom.xml index d0eb7ad5792..af8a424ee7b 100644 --- a/modules/openapi-generator-maven-plugin/pom.xml +++ b/modules/openapi-generator-maven-plugin/pom.xml @@ -16,6 +16,8 @@ UTF-8 **/src/main/java/org/openapitools/codegen/plugin/**/* + + 3.0.5 @@ -60,6 +62,24 @@ test + + org.apache.maven.shared + maven-verifier + 1.7.2 + test + + + org.apache.maven.plugin-testing + maven-plugin-testing-harness + 3.3.0 + test + + + org.codehaus.plexus + plexus-utils + 3.3.0 + test + @@ -108,7 +128,41 @@ - + + integration + + + + org.apache.maven.plugins + maven-invoker-plugin + 3.2.1 + + verify + true + true + false + true + + + + org.codehaus.groovy + groovy + ${groovy.version} + runtime + + + + + integration-test + + run + + + + + + + static-analysis diff --git a/modules/openapi-generator-maven-plugin/src/it/custom-template-resource/invoker.properties b/modules/openapi-generator-maven-plugin/src/it/custom-template-resource/invoker.properties new file mode 100644 index 00000000000..1f2cc145e63 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/custom-template-resource/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals = -nsu generate-sources +invoker.name = Test Custom Templates via Resource diff --git a/modules/openapi-generator-maven-plugin/src/it/custom-template-resource/pom.xml b/modules/openapi-generator-maven-plugin/src/it/custom-template-resource/pom.xml new file mode 100644 index 00000000000..b8d127dadd7 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/custom-template-resource/pom.xml @@ -0,0 +1,57 @@ + + + + + 4.0.0 + + org.openapitools.maven.its + custom-template-resource + 1.0-SNAPSHOT + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + https://raw.githubusercontent.com/OpenAPITools/openapi-generator/master/modules/openapi-generator/src/test/resources/2_0/petstore.yaml + kotlin + ${basedir}/out + + bash + + true + + + + + default + generate-sources + + generate + + + + + + + diff --git a/modules/openapi-generator-maven-plugin/src/it/custom-template-resource/verify.groovy b/modules/openapi-generator-maven-plugin/src/it/custom-template-resource/verify.groovy new file mode 100644 index 00000000000..0af466d2635 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/custom-template-resource/verify.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2020 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +File readme = new File(basedir, "out/README.md") + +assert readme.isFile() +if (File.separator == "/") { + // For whatever reason, resource path templates fail in this test in Windows + assert readme.text.contains("# OpenAPI Petstore Bash client") +} + +File gradle = new File(basedir, "out/build.gradle") +assert gradle.isFile() + +File api = new File(basedir, "out/src/main/kotlin/org/openapitools/client/apis/PetApi.kt") +assert api.isFile() + +File model = new File(basedir, "out/src/main/kotlin/org/openapitools/client/models/Pet.kt") +assert model.isFile() + +// note that in Java 11+, this anything matching this condition could fail due to +// Illegal reflective access by org.codehaus.groovy.reflection.CachedClass +// and cause tests to fail. This is more to document for engineers. +if (GroovySystem.version.tokenize('.')[0].toInteger() < 3) { + throw new IllegalStateException("Found:" + GroovySystem.version + ", need Groovy 3.x or higher for Java 11+, so we require it for all versions") +} diff --git a/modules/openapi-generator-maven-plugin/src/it/custom-template/invoker.properties b/modules/openapi-generator-maven-plugin/src/it/custom-template/invoker.properties new file mode 100644 index 00000000000..65393cde95a --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/custom-template/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals = -nsu generate-sources +invoker.name = Test Custom Templates diff --git a/modules/openapi-generator-maven-plugin/src/it/custom-template/pom.xml b/modules/openapi-generator-maven-plugin/src/it/custom-template/pom.xml new file mode 100644 index 00000000000..52313b33d2f --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/custom-template/pom.xml @@ -0,0 +1,53 @@ + + + + + 4.0.0 + + org.openapitools.maven.its + custom-template + 1.0-SNAPSHOT + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + https://raw.githubusercontent.com/OpenAPITools/openapi-generator/master/modules/openapi-generator/src/test/resources/2_0/petstore.yaml + kotlin + ${basedir}/out + ${project.basedir}/templates + + true + + + + + remote + generate-sources + + generate + + + + + + + diff --git a/modules/openapi-generator-maven-plugin/src/it/custom-template/templates/README.mustache b/modules/openapi-generator-maven-plugin/src/it/custom-template/templates/README.mustache new file mode 100644 index 00000000000..cee4f752e72 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/custom-template/templates/README.mustache @@ -0,0 +1,21 @@ +# TEST TEST TEST + +# {{artifactId}} + +{{appName}} + +- API version: {{appVersion}} +{{^hideGenerationTimestamp}} + +- Build date: {{generatedDate}} +{{/hideGenerationTimestamp}} + +{{#appDescriptionWithNewLines}}{{{appDescriptionWithNewLines}}}{{/appDescriptionWithNewLines}} + +{{#infoUrl}} + For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) +{{/infoUrl}} + +*Automatically generated by the [OpenAPI Generator](https://openapi-generator.tech)* + +… etc. diff --git a/modules/openapi-generator-maven-plugin/src/it/custom-template/verify.groovy b/modules/openapi-generator-maven-plugin/src/it/custom-template/verify.groovy new file mode 100644 index 00000000000..02698915258 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/custom-template/verify.groovy @@ -0,0 +1,38 @@ +/* + * Copyright 2020 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +File readme = new File(basedir, "out/README.md") + +assert readme.isFile() +assert readme.text.contains("# TEST TEST TEST") +assert readme.text.contains("# kotlin-client") +assert readme.text.contains("OpenAPI Petstore") + +File gradle = new File(basedir, "out/build.gradle") +assert gradle.isFile() + +File api = new File(basedir, "out/src/main/kotlin/org/openapitools/client/apis/PetApi.kt") +assert api.isFile() + +File model = new File(basedir, "out/src/main/kotlin/org/openapitools/client/models/Pet.kt") +assert model.isFile() + +// note that in Java 11+, this anything matching this condition could fail due to +// Illegal reflective access by org.codehaus.groovy.reflection.CachedClass +// and cause tests to fail. This is more to document for engineers. +if (GroovySystem.version.tokenize('.')[0].toInteger() < 3) { + throw new IllegalStateException("Found:" + GroovySystem.version + ", need Groovy 3.x or higher for Java 11+, so we require it for all versions") +} diff --git a/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/BaseTestCase.java b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/BaseTestCase.java new file mode 100644 index 00000000000..ba69d021af3 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/BaseTestCase.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.plugin; + +import org.apache.maven.plugin.testing.AbstractMojoTestCase; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * A base test class where we can add helper methods and whatnot + */ +public abstract class BaseTestCase extends AbstractMojoTestCase { + protected Path getUnitTestDir() { + return Paths.get(getBasedir(), "src", "test", "resources", "unit"); + } +} diff --git a/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/CodeGenMojoTest.java b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/CodeGenMojoTest.java new file mode 100644 index 00000000000..7b29b522409 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/CodeGenMojoTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.plugin; + +import org.junit.Test; +import org.openapitools.codegen.plugin.stubs.StubUtility; + +import java.io.File; +import java.util.Map; + +public class CodeGenMojoTest extends BaseTestCase { + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @SuppressWarnings("unchecked") + public void testCommonConfiguration() throws Exception { + File testPom = StubUtility.basedPath(getUnitTestDir().toFile(), "common-maven", "common-maven.xml").toFile(); + final CodeGenMojo mojo = (CodeGenMojo) lookupMojo("generate", testPom); + mojo.execute(); + assertEquals("java", getVariableValueFromObject(mojo, "generatorName")); + assertEquals("jersey2", getVariableValueFromObject(mojo, "library")); + assertEquals("remote.org.openapitools.client.api", getVariableValueFromObject(mojo, "apiPackage")); + assertEquals("remote.org.openapitools.client.model", getVariableValueFromObject(mojo, "modelPackage")); + assertEquals("remote.org.openapitools.client", getVariableValueFromObject(mojo, "invokerPackage")); + + Map configOptions = (Map) getVariableValueFromObject(mojo, "configOptions"); + assertNotNull(configOptions); + assertEquals("joda", configOptions.get("dateLibrary")); + } +} \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/stubs/CommonMavenProjectStub.java b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/stubs/CommonMavenProjectStub.java new file mode 100644 index 00000000000..9577061f0a8 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/stubs/CommonMavenProjectStub.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.plugin.stubs; + +import org.apache.maven.plugin.testing.stubs.MavenProjectStub; + +public class CommonMavenProjectStub extends MavenProjectStub { + public CommonMavenProjectStub() { + StubUtility.configureStub(this,"common-maven", "common-maven.xml"); + } +} diff --git a/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/stubs/StubUtility.java b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/stubs/StubUtility.java new file mode 100644 index 00000000000..c7ccd1a2a9e --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/stubs/StubUtility.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.plugin.stubs; + +import org.apache.maven.model.Build; +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.apache.maven.plugin.testing.stubs.ArtifactStub; +import org.apache.maven.plugin.testing.stubs.DefaultArtifactHandlerStub; +import org.apache.maven.plugin.testing.stubs.MavenProjectStub; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +public abstract class StubUtility { + private StubUtility() { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Configures a stub to conventional directories based on test name and pom file name. + *

+ * Taken largely from PMD plugin: + * https://github.com/apache/maven-pmd-plugin/blob/d766fdb0c93a6630ad7a788f260746b05c804a71/src/test/java/org/apache/maven/plugins/pmd/stubs/CustomConfigurationMavenProjectStub.java + * License: Apache 2.0 + * + * @param configure The stub to configure + * @param testName A name used to identify this stub, used in resource lookup + * @param pomFileName The filename and extension, e.g. "my-test-pom.xml" + */ + public static void configureStub(MavenProjectStub configure, String testName, String pomFileName) { + MavenXpp3Reader pomReader = new MavenXpp3Reader(); + Model model = null; + try { + File pomFile = basedPath( + configure.getBasedir(), "src", "test", "resources", "unit", testName, pomFileName + ).toFile(); + model = pomReader.read(new InputStreamReader(new FileInputStream(pomFile), StandardCharsets.UTF_8)); + configure.setModel(model); + } catch (Exception ignored) { + + } + + configure.setGroupId(model.getGroupId()); + configure.setArtifactId(model.getArtifactId()); + configure.setVersion(model.getVersion()); + configure.setName(model.getName()); + configure.setUrl(model.getUrl()); + configure.setPackaging(model.getPackaging()); + + Build build = new Build(); + build.setFinalName(model.getBuild().getFinalName()); + build.setDirectory(basedPath(configure.getBasedir(), "target", "test", "unit", testName, "target").toString()); + build.setSourceDirectory(basedPath(configure.getBasedir(), "src", "test", "resources", "unit", testName).toString()); + configure.setBuild(build); + + List compileSourceRoots = new ArrayList<>(); + compileSourceRoots.add(basedPath(configure.getBasedir(), "src", "test", "resources", "unit", testName, "src").toString()); + configure.setCompileSourceRoots(compileSourceRoots); + + ArtifactStub artifactStub = new ArtifactStub(); + artifactStub.setGroupId(configure.getGroupId()); + artifactStub.setArtifactId(configure.getArtifactId()); + artifactStub.setVersion(configure.getVersion()); + artifactStub.setArtifactHandler(new DefaultArtifactHandlerStub("jar")); + configure.setArtifact(artifactStub); + + configure.setFile(configure.getBasedir().toPath().resolve(pomFileName).toFile()); + } + + public static Path basedPath(File baseDir, String first, String... more) { + return baseDir.toPath().resolve(Paths.get(first, more)).normalize().toAbsolutePath(); + } +} diff --git a/modules/openapi-generator-maven-plugin/src/test/resources/unit/common-maven/common-maven.xml b/modules/openapi-generator-maven-plugin/src/test/resources/unit/common-maven/common-maven.xml new file mode 100644 index 00000000000..767caeab2dd --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/resources/unit/common-maven/common-maven.xml @@ -0,0 +1,56 @@ + + + + 4.0.0 + common.maven + common-maven + jar + 1.0.0-SNAPSHOT + OpenAPI Generator Configuration Test + https://openapi-generator.tech/ + + common-maven + + + org.openapitools + openapi-generator-maven-plugin + + + ${basedir}/src/test/resources/unit/common-maven/petstore.yaml + java + + joda + + jersey2 + ${basedir}/target/generated-sources/common-maven/remote-openapi + remote.org.openapitools.client.api + remote.org.openapitools.client.model + remote.org.openapitools.client + + + + default + generate-sources + + generate + + + + + + + \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/test/resources/unit/common-maven/petstore.yaml b/modules/openapi-generator-maven-plugin/src/test/resources/unit/common-maven/petstore.yaml new file mode 100644 index 00000000000..f5e98eec38d --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/resources/unit/common-maven/petstore.yaml @@ -0,0 +1,736 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: OpenAPI Petstore + license: + name: Apache-2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + explode: false + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + explode: false + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'read:pets' + deprecated: true + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value <= 5 or > 10. Other values + will generated exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 5 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with value < 1000. Anything above + 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + Set-Cookie: + description: >- + Cookie authentication key for use with the `api_key` + apiKey authentication. + schema: + type: string + example: AUTH_KEY=abcde12345; Path=/; HttpOnly + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when toekn expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + responses: + default: + description: successful operation + security: + - api_key: [] + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing. + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + security: + - api_key: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found + security: + - api_key: [] +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Order: + title: Pet Order + description: An order for a pets from the pet store + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + title: Pet category + description: A category for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + xml: + name: Category + User: + title: a User + description: A User who is purchasing from the pet store + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + title: Pet Tag + description: A tag for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + title: a Pet + description: A pet for sale in the pet store + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + title: An uploaded response + description: Describes the result of uploading an image resource + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string diff --git a/modules/openapi-generator-maven-plugin/src/test/resources/unit/common-maven/src/main/java/com/example/Example.java b/modules/openapi-generator-maven-plugin/src/test/resources/unit/common-maven/src/main/java/com/example/Example.java new file mode 100644 index 00000000000..8447f00216d --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/resources/unit/common-maven/src/main/java/com/example/Example.java @@ -0,0 +1,7 @@ +package com.example; + +public class Example { + public static void main(String[] args) { + System.out.println("You did it!"); + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java index 14b71d86538..861b9bc3a3d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java @@ -6,6 +6,7 @@ import org.openapitools.codegen.TemplateManager; import org.openapitools.codegen.api.TemplatePathLocator; import java.io.File; +import java.nio.file.Paths; /** * Locates templates according to {@link CodegenConfig} settings. @@ -23,7 +24,7 @@ public class GeneratorTemplateContentLocator implements TemplatePathLocator { } private String buildLibraryFilePath(String dir, String library, String file) { - return dir + File.separator + "libraries" + File.separator + library + File.separator + file; + return Paths.get(dir, "libraries", library, file).normalize().toString(); } /** @@ -34,6 +35,10 @@ public class GeneratorTemplateContentLocator implements TemplatePathLocator { * @return true if file is an embedded resource, false if it does not exist */ public boolean embeddedTemplateExists(String name) { + return classpathTemplateExists(name); + } + + private boolean classpathTemplateExists(String name) { return this.getClass().getClassLoader().getResource(TemplateManager.getCPResourcePath(name)) != null; } @@ -60,20 +65,26 @@ public class GeneratorTemplateContentLocator implements TemplatePathLocator { if (StringUtils.isNotEmpty(library)) { //look for the file in the library subfolder of the supplied template final String libTemplateFile = buildLibraryFilePath(config.templateDir(), library, relativeTemplateFile); - if (new File(libTemplateFile).exists() || this.getClass().getClassLoader().getResource(libTemplateFile) != null) { + // looks for user-defined file or classpath + // supports template dir which refers to local file system or custom path in classpath as defined by templateDir + if (new File(libTemplateFile).exists() || classpathTemplateExists(libTemplateFile)) { return libTemplateFile; } } - //check the supplied template main folder for the file + // check the supplied template main folder for the file + // File.separator is necessary here as the file load is OS-specific final String template = config.templateDir() + File.separator + relativeTemplateFile; - if (new File(template).exists() || this.getClass().getClassLoader().getResource(template) != null) { + // looks for user-defined file or classpath + // supports template dir which refers to local file system or custom path in classpath as defined by templateDir + if (new File(template).exists() || classpathTemplateExists(template)) { return template; } //try the embedded template library folder next if (StringUtils.isNotEmpty(library)) { final String embeddedLibTemplateFile = buildLibraryFilePath(config.embeddedTemplateDir(), library, relativeTemplateFile); + // *only* looks for those files in classpath as defined by embeddedTemplateDir if (embeddedTemplateExists(embeddedLibTemplateFile)) { // Fall back to the template file embedded/packaged in the JAR file library folder... return embeddedLibTemplateFile; @@ -82,6 +93,7 @@ public class GeneratorTemplateContentLocator implements TemplatePathLocator { // Fall back to the template file for generator root directory embedded/packaged in the JAR file... String loc = config.embeddedTemplateDir() + File.separator + relativeTemplateFile; + // *only* looks for those files in classpath as defined by embeddedTemplateDir if (embeddedTemplateExists(loc)) { return loc; } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java index 1a1806d6f83..1bf5dc70257 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java @@ -433,4 +433,208 @@ public class DefaultGeneratorTest { Assert.assertEquals(((Schema) codegenResponse.schema).getPattern(), expectedPattern); Assert.assertEquals(codegenResponse.pattern, escapedPattern); } + + @Test + public void testBuiltinLibraryTemplates() throws IOException { + Path target = Files.createTempDirectory("test"); + File output = target.toFile(); + try { + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin") + .setLibrary("jvm-okhttp4") + .setInputSpec("src/test/resources/3_0/petstore.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString()); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(false); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.API_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.API_TESTS, "false"); + + List files = generator.opts(clientOptInput).generate(); + + Assert.assertEquals(files.size(), 20); + + // Generator should report a library templated file as a generated file + TestUtils.ensureContainsFile(files, output, "src/main/kotlin/org/openapitools/client/infrastructure/Errors.kt"); + + // Generated file should exist on the filesystem after generation + File generatedFile = new File(output, "src/main/kotlin/org/openapitools/client/infrastructure/Errors.kt"); + Assert.assertTrue(generatedFile.exists()); + + // Generated file should contain some expected text + TestUtils.assertFileContains(generatedFile.toPath(), "package org.openapitools.client.infrastructure", + "open class ClientException", + "open class ServerException"); + } finally { + output.delete(); + } + } + + @Test + public void testBuiltinNonLibraryTemplates() throws IOException { + Path target = Files.createTempDirectory("test"); + File output = target.toFile(); + try { + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin") + .setInputSpec("src/test/resources/3_0/petstore.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString()); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(false); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.API_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.API_TESTS, "false"); + + List files = generator.opts(clientOptInput).generate(); + + Assert.assertEquals(files.size(), 20); + + // Generator should report README.md as a generated file + TestUtils.ensureContainsFile(files, output, "README.md"); + + // Generated file should exist on the filesystem after generation + File readme = new File(output, "README.md"); + Assert.assertTrue(readme.exists()); + + // README.md should contain some expected text + TestUtils.assertFileContains(readme.toPath(), "# org.openapitools.client - Kotlin client library for OpenAPI Petstore", + "## Requires", + "## Build", + "## Features/Implementation Notes"); + } finally { + output.delete(); + } + } + + @Test + public void testCustomLibraryTemplates() throws IOException { + Path target = Files.createTempDirectory("test"); + Path templates = Files.createTempDirectory("templates"); + File output = target.toFile(); + try { + // Create custom template + File customTemplate = new File(templates.toFile(), "libraries/jvm-okhttp/infrastructure/Errors.kt.mustache"); + new File(customTemplate.getParent()).mkdirs(); + StringBuilder sb = new StringBuilder(); + sb.append("// {{someKey}}").append("\n"); + sb.append("@file:Suppress(\"unused\")").append("\n"); + sb.append("package org.openapitools.client.infrastructure").append("\n"); + sb.append("import java.lang.RuntimeException").append("\n"); + sb.append("open class CustomException(").append("\n"); + sb.append(" message: kotlin.String? = null, val statusCode: Int = -1, val response: Response? = null) : RuntimeException(message) {").append("\n"); + sb.append(" companion object {").append("\n"); + sb.append(" private const val serialVersionUID: Long = 789L").append("\n"); + sb.append(" }").append("\n"); + sb.append("}").append("\n"); + Files.write(customTemplate.toPath(), + sb.toString().getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin") + .addAdditionalProperty("someKey", "testCustomLibraryTemplates") + .setTemplateDir(templates.toAbsolutePath().toString()) + .setLibrary("jvm-okhttp4") + .setInputSpec("src/test/resources/3_0/petstore.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString()); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(false); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.API_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.API_TESTS, "false"); + + List files = generator.opts(clientOptInput).generate(); + + Assert.assertEquals(files.size(), 20); + + // Generator should report a library templated file as a generated file + TestUtils.ensureContainsFile(files, output, "src/main/kotlin/org/openapitools/client/infrastructure/Errors.kt"); + + // Generated file should exist on the filesystem after generation + File readme = new File(output, "src/main/kotlin/org/openapitools/client/infrastructure/Errors.kt"); + Assert.assertTrue(readme.exists()); + + // Generated file should contain our custom templated text + TestUtils.assertFileContains(readme.toPath(), "// testCustomLibraryTemplates", + "package org.openapitools.client.infrastructure", + "open class CustomException(", + "private const val serialVersionUID: Long = 789L"); + } finally { + output.delete(); + templates.toFile().delete(); + } + } + + @Test + public void testCustomNonLibraryTemplates() throws IOException { + Path target = Files.createTempDirectory("test"); + Path templates = Files.createTempDirectory("templates"); + File output = target.toFile(); + try { + // Create custom template + File customTemplate = new File(templates.toFile(), "README.mustache"); + new File(customTemplate.getParent()).mkdirs(); + Files.write(customTemplate.toPath(), + "# {{someKey}}".getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin") + .addAdditionalProperty("someKey", "testCustomNonLibraryTemplates") + .setTemplateDir(templates.toAbsolutePath().toString()) + .setInputSpec("src/test/resources/3_0/petstore.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString()); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(false); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.API_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.API_TESTS, "false"); + + List files = generator.opts(clientOptInput).generate(); + + Assert.assertEquals(files.size(), 20); + + // Generator should report README.md as a generated file + TestUtils.ensureContainsFile(files, output, "README.md"); + + // Generated file should exist on the filesystem after generation + File readme = new File(output, "README.md"); + Assert.assertTrue(readme.exists()); + + // README.md should contain our custom templated text + TestUtils.assertFileContains(readme.toPath(), "# testCustomNonLibraryTemplates"); + } finally { + output.delete(); + templates.toFile().delete(); + } + } } +