diff --git a/bin/kotlin-server-petstore.sh b/bin/kotlin-server-petstore.sh new file mode 100755 index 00000000000..597a5d648d6 --- /dev/null +++ b/bin/kotlin-server-petstore.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +SCRIPT="$0" + +while [ -h "$SCRIPT" ] ; do + ls=$(ls -ld "$SCRIPT") + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' > /dev/null; then + SCRIPT="$link" + else + SCRIPT=$(dirname "$SCRIPT")/"$link" + fi +done + +if [ ! -d "${APP_DIR}" ]; then + APP_DIR=$(dirname "$SCRIPT")/.. + APP_DIR=$(cd "${APP_DIR}"; pwd) +fi + +executable="./modules/swagger-codegen-cli/target/swagger-codegen-cli.jar" + +if [ ! -f "$executable" ] +then + mvn clean package +fi + +# if you've executed sbt assembly previously it will use that instead. +export JAVA_OPTS="${JAVA_OPTS} -XX:MaxPermSize=256M -Xmx1024M -DloggerPath=conf/log4j.properties -DdebugSupportingFiles=true" +ags="generate -i modules/swagger-codegen/src/test/resources/2_0/petstore.yaml -t modules/swagger-codegen/src/main/resources/kotlin-server -l kotlin-server --library=ktor -o samples/server/petstore/kotlin-server/ktor $@" + +java ${JAVA_OPTS} -jar ${executable} ${ags} diff --git a/bin/windows/kotlin-server-petstore.bat b/bin/windows/kotlin-server-petstore.bat new file mode 100644 index 00000000000..51892671c15 --- /dev/null +++ b/bin/windows/kotlin-server-petstore.bat @@ -0,0 +1,10 @@ +set executable=.\modules\swagger-codegen-cli\target\swagger-codegen-cli.jar + +If Not Exist %executable% ( + mvn clean package +) + +REM set JAVA_OPTS=%JAVA_OPTS% -Xmx1024M -DloggerPath=conf/log4j.properties +set ags=generate --artifact-id "kotlin-petstore-server" -i modules\swagger-codegen\src\test\resources\2_0\petstore.yaml -l kotlin-server --library=ktor -o samples\server\petstore\kotlin + +java %JAVA_OPTS% -jar %executable% %ags% diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenConstants.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenConstants.java index e33ad82516c..c4534e62132 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenConstants.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenConstants.java @@ -194,12 +194,15 @@ public class CodegenConstants { public static final String EXCLUDE_TESTS_DESC = "Specifies that no tests are to be generated."; // Not user-configurable. System provided for use in templates. + + public static final String GENERATE_APIS = "generateApis"; public static final String GENERATE_API_DOCS = "generateApiDocs"; public static final String GENERATE_API_TESTS = "generateApiTests"; public static final String GENERATE_API_TESTS_DESC = "Specifies that api tests are to be generated."; // Not user-configurable. System provided for use in templates. + public static final String GENERATE_MODELS = "generateModels"; public static final String GENERATE_MODEL_DOCS = "generateModelDocs"; public static final String GENERATE_MODEL_TESTS = "generateModelTests"; diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultGenerator.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultGenerator.java index 1c249003cdd..6ebeb01ac0d 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultGenerator.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultGenerator.java @@ -157,6 +157,9 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { config.additionalProperties().put(CodegenConstants.GENERATE_API_DOCS, generateApiDocumentation); config.additionalProperties().put(CodegenConstants.GENERATE_MODEL_DOCS, generateModelDocumentation); + config.additionalProperties().put(CodegenConstants.GENERATE_APIS, generateApis); + config.additionalProperties().put(CodegenConstants.GENERATE_MODELS, generateModels); + if (!generateApiTests && !generateModelTests) { config.additionalProperties().put(CodegenConstants.EXCLUDE_TESTS, true); } diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/InlineModelResolver.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/InlineModelResolver.java index 143fa9568f6..cf97844d24c 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/InlineModelResolver.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/InlineModelResolver.java @@ -434,7 +434,7 @@ public class InlineModelResolver { * * @param ref new property name * @param property Property - * @return + * @return {@link Property} A constructed Swagger property */ public Property makeRefProperty(String ref, Property property) { RefProperty newProperty = new RefProperty(ref); diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/AbstractKotlinCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/AbstractKotlinCodegen.java new file mode 100644 index 00000000000..2599006c1d0 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/AbstractKotlinCodegen.java @@ -0,0 +1,548 @@ +package io.swagger.codegen.languages; + +import io.swagger.codegen.CliOption; +import io.swagger.codegen.CodegenConfig; +import io.swagger.codegen.CodegenConstants; +import io.swagger.codegen.DefaultCodegen; +import io.swagger.models.properties.ArrayProperty; +import io.swagger.models.properties.MapProperty; +import io.swagger.models.properties.Property; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +public abstract class AbstractKotlinCodegen extends DefaultCodegen implements CodegenConfig { + static Logger LOGGER = LoggerFactory.getLogger(AbstractKotlinCodegen.class); + + protected String artifactId; + protected String artifactVersion = "1.0.0"; + protected String groupId = "io.swagger"; + protected String packageName; + + protected String sourceFolder = "src/main/kotlin"; + + protected String apiDocPath = "docs/"; + protected String modelDocPath = "docs/"; + + protected CodegenConstants.ENUM_PROPERTY_NAMING_TYPE enumPropertyNaming = CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.camelCase; + + public AbstractKotlinCodegen() { + super(); + supportsInheritance = true; + + languageSpecificPrimitives = new HashSet(Arrays.asList( + "kotlin.Byte", + "kotlin.Short", + "kotlin.Int", + "kotlin.Long", + "kotlin.Float", + "kotlin.Double", + "kotlin.Boolean", + "kotlin.Char", + "kotlin.String", + "kotlin.Array", + "kotlin.collections.List", + "kotlin.collections.Map", + "kotlin.collections.Set" + )); + + // this includes hard reserved words defined by https://github.com/JetBrains/kotlin/blob/master/core/descriptors/src/org/jetbrains/kotlin/renderer/KeywordStringsGenerated.java + // as well as keywords from https://kotlinlang.org/docs/reference/keyword-reference.html + reservedWords = new HashSet(Arrays.asList( + "abstract", + "annotation", + "as", + "break", + "case", + "catch", + "class", + "companion", + "const", + "constructor", + "continue", + "crossinline", + "data", + "delegate", + "do", + "else", + "enum", + "external", + "false", + "final", + "finally", + "for", + "fun", + "if", + "in", + "infix", + "init", + "inline", + "inner", + "interface", + "internal", + "is", + "it", + "lateinit", + "lazy", + "noinline", + "null", + "object", + "open", + "operator", + "out", + "override", + "package", + "private", + "protected", + "public", + "reified", + "return", + "sealed", + "super", + "suspend", + "tailrec", + "this", + "throw", + "true", + "try", + "typealias", + "typeof", + "val", + "var", + "vararg", + "when", + "while" + )); + + defaultIncludes = new HashSet(Arrays.asList( + "kotlin.Byte", + "kotlin.Short", + "kotlin.Int", + "kotlin.Long", + "kotlin.Float", + "kotlin.Double", + "kotlin.Boolean", + "kotlin.Char", + "kotlin.Array", + "kotlin.collections.List", + "kotlin.collections.Set", + "kotlin.collections.Map" + )); + + typeMapping = new HashMap(); + typeMapping.put("string", "kotlin.String"); + typeMapping.put("boolean", "kotlin.Boolean"); + typeMapping.put("integer", "kotlin.Int"); + typeMapping.put("float", "kotlin.Float"); + typeMapping.put("long", "kotlin.Long"); + typeMapping.put("double", "kotlin.Double"); + typeMapping.put("number", "java.math.BigDecimal"); + typeMapping.put("date-time", "java.time.LocalDateTime"); + typeMapping.put("date", "java.time.LocalDateTime"); + typeMapping.put("file", "java.io.File"); + typeMapping.put("array", "kotlin.Array"); + typeMapping.put("list", "kotlin.Array"); + typeMapping.put("map", "kotlin.collections.Map"); + typeMapping.put("object", "kotlin.Any"); + typeMapping.put("binary", "kotlin.Array"); + typeMapping.put("Date", "java.time.LocalDateTime"); + typeMapping.put("DateTime", "java.time.LocalDateTime"); + + instantiationTypes.put("array", "arrayOf"); + instantiationTypes.put("list", "arrayOf"); + instantiationTypes.put("map", "mapOf"); + + importMapping = new HashMap(); + importMapping.put("BigDecimal", "java.math.BigDecimal"); + importMapping.put("UUID", "java.util.UUID"); + importMapping.put("File", "java.io.File"); + importMapping.put("Date", "java.util.Date"); + importMapping.put("Timestamp", "java.sql.Timestamp"); + importMapping.put("DateTime", "java.time.LocalDateTime"); + importMapping.put("LocalDateTime", "java.time.LocalDateTime"); + importMapping.put("LocalDate", "java.time.LocalDate"); + importMapping.put("LocalTime", "java.time.LocalTime"); + + specialCharReplacements.put(";", "Semicolon"); + + cliOptions.clear(); + addOption(CodegenConstants.SOURCE_FOLDER, CodegenConstants.SOURCE_FOLDER_DESC, sourceFolder); + addOption(CodegenConstants.PACKAGE_NAME, "Generated artifact package name (e.g. io.swagger).", packageName); + addOption(CodegenConstants.GROUP_ID, "Generated artifact package's organization (i.e. maven groupId).", groupId); + addOption(CodegenConstants.ARTIFACT_ID, "Generated artifact id (name of jar).", artifactId); + addOption(CodegenConstants.ARTIFACT_VERSION, "Generated artifact's package version.", artifactVersion); + + CliOption enumPropertyNamingOpt = new CliOption(CodegenConstants.ENUM_PROPERTY_NAMING, CodegenConstants.ENUM_PROPERTY_NAMING_DESC); + cliOptions.add(enumPropertyNamingOpt.defaultValue(enumPropertyNaming.name())); + } + + protected void addOption(String key, String description) { + addOption(key, description, null); + } + + protected void addOption(String key, String description, String defaultValue) { + CliOption option = new CliOption(key, description); + if (defaultValue != null) option.defaultValue(defaultValue); + cliOptions.add(option); + } + + protected void addSwitch(String key, String description, Boolean defaultValue) { + CliOption option = CliOption.newBoolean(key, description); + if (defaultValue != null) option.defaultValue(defaultValue.toString()); + cliOptions.add(option); + } + + @Override + public String apiDocFileFolder() { + return (outputFolder + "/" + apiDocPath).replace('/', File.separatorChar); + } + + @Override + public String apiFileFolder() { + return outputFolder + File.separator + sourceFolder + File.separator + apiPackage().replace('.', File.separatorChar); + } + + @Override + public String escapeQuotationMark(String input) { + // remove " to avoid code injection + return input.replace("\"", ""); + } + + @Override + public String escapeReservedWord(String name) { + // TODO: Allow enum escaping as an option (e.g. backticks vs append/prepend underscore vs match model property escaping). + return String.format("`%s`", name); + } + + @Override + public String escapeUnsafeCharacters(String input) { + return input.replace("*/", "*_/").replace("/*", "/_*"); + } + + public CodegenConstants.ENUM_PROPERTY_NAMING_TYPE getEnumPropertyNaming() { + return this.enumPropertyNaming; + } + + /** + * Sets the naming convention for Kotlin enum properties + * + * @param enumPropertyNamingType The string representation of the naming convention, as defined by {@link CodegenConstants.ENUM_PROPERTY_NAMING_TYPE} + */ + public void setEnumPropertyNaming(final String enumPropertyNamingType) { + try { + this.enumPropertyNaming = CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.valueOf(enumPropertyNamingType); + } catch (IllegalArgumentException ex) { + StringBuilder sb = new StringBuilder(enumPropertyNamingType + " is an invalid enum property naming option. Please choose from:"); + for (CodegenConstants.ENUM_PROPERTY_NAMING_TYPE t : CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.values()) { + sb.append("\n ").append(t.name()); + } + throw new RuntimeException(sb.toString()); + } + } + + /** + * returns the swagger type for the property + * + * @param p Swagger property object + * @return string presentation of the type + **/ + @Override + public String getSwaggerType(Property p) { + String swaggerType = super.getSwaggerType(p); + String type; + // This maps, for example, long -> kotlin.Long based on hashes in this type's constructor + if (typeMapping.containsKey(swaggerType)) { + type = typeMapping.get(swaggerType); + if (languageSpecificPrimitives.contains(type)) { + return toModelName(type); + } + } else { + type = swaggerType; + } + return toModelName(type); + } + + /** + * Output the type declaration of the property + * + * @param p Swagger Property object + * @return a string presentation of the property type + */ + @Override + public String getTypeDeclaration(Property p) { + if (p instanceof ArrayProperty) { + return getArrayTypeDeclaration((ArrayProperty) p); + } else if (p instanceof MapProperty) { + MapProperty mp = (MapProperty) p; + Property inner = mp.getAdditionalProperties(); + + // Maps will be keyed only by primitive Kotlin string + return getSwaggerType(p) + ""; + } + return super.getTypeDeclaration(p); + } + + @Override + public String modelDocFileFolder() { + return (outputFolder + "/" + modelDocPath).replace('/', File.separatorChar); + } + + @Override + public String modelFileFolder() { + return outputFolder + File.separator + sourceFolder + File.separator + modelPackage().replace('.', File.separatorChar); + } + + @Override + public Map postProcessModels(Map objs) { + return postProcessModelsEnum(super.postProcessModels(objs)); + } + + @Override + public void processOpts() { + super.processOpts(); + + if (additionalProperties.containsKey(CodegenConstants.ENUM_PROPERTY_NAMING)) { + setEnumPropertyNaming((String) additionalProperties.get(CodegenConstants.ENUM_PROPERTY_NAMING)); + } + + if (additionalProperties.containsKey(CodegenConstants.SOURCE_FOLDER)) { + this.setSourceFolder((String) additionalProperties.get(CodegenConstants.SOURCE_FOLDER)); + } else { + additionalProperties.put(CodegenConstants.SOURCE_FOLDER, sourceFolder); + } + + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) { + this.setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME)); + if (!additionalProperties.containsKey(CodegenConstants.MODEL_PACKAGE)) + this.setModelPackage(packageName + ".models"); + if (!additionalProperties.containsKey(CodegenConstants.API_PACKAGE)) + this.setApiPackage(packageName + ".apis"); + } else { + additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName); + } + + if (additionalProperties.containsKey(CodegenConstants.ARTIFACT_ID)) { + this.setArtifactId((String) additionalProperties.get(CodegenConstants.ARTIFACT_ID)); + } else { + additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId); + } + + if (additionalProperties.containsKey(CodegenConstants.GROUP_ID)) { + this.setGroupId((String) additionalProperties.get(CodegenConstants.GROUP_ID)); + } else { + additionalProperties.put(CodegenConstants.GROUP_ID, groupId); + } + + if (additionalProperties.containsKey(CodegenConstants.ARTIFACT_VERSION)) { + this.setArtifactVersion((String) additionalProperties.get(CodegenConstants.ARTIFACT_VERSION)); + } else { + additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion); + } + + if (additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) { + LOGGER.warn(CodegenConstants.INVOKER_PACKAGE + " with " + this.getName() + " generator is ignored. Use " + CodegenConstants.PACKAGE_NAME + "."); + } + + additionalProperties.put(CodegenConstants.API_PACKAGE, apiPackage()); + additionalProperties.put(CodegenConstants.MODEL_PACKAGE, modelPackage()); + + additionalProperties.put("apiDocPath", apiDocPath); + additionalProperties.put("modelDocPath", modelDocPath); + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public void setArtifactVersion(String artifactVersion) { + this.artifactVersion = artifactVersion; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public void setSourceFolder(String sourceFolder) { + this.sourceFolder = sourceFolder; + } + + /** + * Return the sanitized variable name for enum + * + * @param value enum variable name + * @param datatype data type + * @return the sanitized variable name for enum + */ + @Override + public String toEnumVarName(String value, String datatype) { + String modified; + if (value.length() == 0) { + modified = "EMPTY"; + } else { + modified = value; + modified = sanitizeKotlinSpecificNames(modified); + } + + switch (getEnumPropertyNaming()) { + case original: + // NOTE: This is provided as a last-case allowance, but will still result in reserved words being escaped. + modified = value; + break; + case camelCase: + // NOTE: Removes hyphens and underscores + modified = camelize(modified, true); + break; + case PascalCase: + // NOTE: Removes hyphens and underscores + String result = camelize(modified); + modified = titleCase(result); + break; + case snake_case: + // NOTE: Removes hyphens + modified = underscore(modified); + break; + case UPPERCASE: + modified = modified.toUpperCase(); + break; + } + + if (reservedWords.contains(modified)) { + return escapeReservedWord(modified); + } + + return modified; + } + + @Override + public String toInstantiationType(Property p) { + if (p instanceof ArrayProperty) { + return getArrayTypeDeclaration((ArrayProperty) p); + } + return super.toInstantiationType(p); + } + + /** + * Return the fully-qualified "Model" name for import + * + * @param name the name of the "Model" + * @return the fully-qualified "Model" name for import + */ + @Override + public String toModelImport(String name) { + // toModelImport is called while processing operations, but DefaultCodegen doesn't + // define imports correctly with fully qualified primitives and models as defined in this generator. + if (needToImport(name)) { + return super.toModelImport(name); + } + + return name; + } + + /** + * Output the proper model name (capitalized). + * In case the name belongs to the TypeSystem it won't be renamed. + * + * @param name the name of the model + * @return capitalized model name + */ + @Override + public String toModelName(final String name) { + // Allow for explicitly configured kotlin.* and java.* types + if (name.startsWith("kotlin.") || name.startsWith("java.")) { + return name; + } + + // If importMapping contains name, assume this is a legitimate model name. + if (importMapping.containsKey(name)) { + return importMapping.get(name); + } + + String modifiedName = name.replaceAll("\\.", ""); + modifiedName = sanitizeKotlinSpecificNames(modifiedName); + + if (reservedWords.contains(modifiedName)) { + modifiedName = escapeReservedWord(modifiedName); + } + + return titleCase(modifiedName); + } + + /** + * Provides a strongly typed declaration for simple arrays of some type and arrays of arrays of some type. + * + * @param arr + * @return + */ + private String getArrayTypeDeclaration(ArrayProperty arr) { + // TODO: collection type here should be fully qualified namespace to avoid model conflicts + // This supports arrays of arrays. + String arrayType = typeMapping.get("array"); + StringBuilder instantiationType = new StringBuilder(arrayType); + Property items = arr.getItems(); + String nestedType = getTypeDeclaration(items); + // TODO: We may want to differentiate here between generics and primitive arrays. + instantiationType.append("<").append(nestedType).append(">"); + return instantiationType.toString(); + } + + /** + * Sanitize against Kotlin specific naming conventions, which may differ from those required by {@link DefaultCodegen#sanitizeName}. + * + * @param name string to be sanitize + * @return sanitized string + */ + private String sanitizeKotlinSpecificNames(final String name) { + String word = name; + for (Map.Entry specialCharacters : specialCharReplacements.entrySet()) { + // Underscore is the only special character we'll allow + if (!specialCharacters.getKey().equals("_")) { + word = word.replaceAll("\\Q" + specialCharacters.getKey() + "\\E", specialCharacters.getValue()); + } + } + + // Fallback, replace unknowns with underscore. + word = word.replaceAll("\\W+", "_"); + if (word.matches("\\d.*")) { + word = "_" + word; + } + + // _, __, and ___ are reserved in Kotlin. Treat all names with only underscores consistently, regardless of count. + if (word.matches("^_*$")) { + word = word.replaceAll("\\Q_\\E", "Underscore"); + } + + return word; + } + + private String titleCase(final String input) { + return input.substring(0, 1).toUpperCase() + input.substring(1); + } + + @Override + protected boolean isReservedWord(String word) { + // We want case-sensitive escaping, to avoid unnecessary backtick-escaping. + return reservedWords.contains(word); + } + + /** + * Check the type to see if it needs import the library/module/package + * + * @param type name of the type + * @return true if the library/module/package of the corresponding type needs to be imported + */ + @Override + protected boolean needToImport(String type) { + // provides extra protection against improperly trying to import language primitives and java types + boolean imports = !type.startsWith("kotlin.") && !type.startsWith("java.") && !defaultIncludes.contains(type) && !languageSpecificPrimitives.contains(type); + return imports; + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/KotlinClientCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/KotlinClientCodegen.java index e014f8a2504..9310cd467f0 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/KotlinClientCodegen.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/KotlinClientCodegen.java @@ -1,36 +1,25 @@ package io.swagger.codegen.languages; -import io.swagger.codegen.*; -import io.swagger.models.properties.ArrayProperty; -import io.swagger.models.properties.MapProperty; -import io.swagger.models.properties.Property; +import io.swagger.codegen.CodegenType; +import io.swagger.codegen.SupportingFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -public class KotlinClientCodegen extends DefaultCodegen implements CodegenConfig { +public class KotlinClientCodegen extends AbstractKotlinCodegen { + static Logger LOGGER = LoggerFactory.getLogger(KotlinClientCodegen.class); - protected String groupId = "io.swagger"; - protected String artifactId = "kotlin-client"; - protected String artifactVersion = "1.0.0"; - protected String sourceFolder = "src/main/kotlin"; - protected String packageName = "io.swagger.client"; - protected String apiDocPath = "docs/"; - protected String modelDocPath = "docs/"; - protected CodegenConstants.ENUM_PROPERTY_NAMING_TYPE enumPropertyNaming = CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.camelCase; - /** * Constructs an instance of `KotlinClientCodegen`. */ public KotlinClientCodegen() { super(); + artifactId = "kotlin-client"; + packageName = "io.swagger.client"; + outputFolder = "generated-code" + File.separator + "kotlin-client"; modelTemplateFiles.put("model.mustache", ".kt"); apiTemplateFiles.put("api.mustache", ".kt"); @@ -39,151 +28,6 @@ public class KotlinClientCodegen extends DefaultCodegen implements CodegenConfig embeddedTemplateDir = templateDir = "kotlin-client"; apiPackage = packageName + ".apis"; modelPackage = packageName + ".models"; - - languageSpecificPrimitives = new HashSet(Arrays.asList( - "kotlin.Byte", - "kotlin.Short", - "kotlin.Int", - "kotlin.Long", - "kotlin.Float", - "kotlin.Double", - "kotlin.Boolean", - "kotlin.Char", - "kotlin.String", - "kotlin.Array", - "kotlin.collections.List", - "kotlin.collections.Map", - "kotlin.collections.Set" - )); - - // this includes hard reserved words defined by https://github.com/JetBrains/kotlin/blob/master/core/descriptors/src/org/jetbrains/kotlin/renderer/KeywordStringsGenerated.java - // as well as keywords from https://kotlinlang.org/docs/reference/keyword-reference.html - reservedWords = new HashSet(Arrays.asList( - "abstract", - "annotation", - "as", - "break", - "case", - "catch", - "class", - "companion", - "const", - "constructor", - "continue", - "crossinline", - "data", - "delegate", - "do", - "else", - "enum", - "external", - "false", - "final", - "finally", - "for", - "fun", - "if", - "in", - "infix", - "init", - "inline", - "inner", - "interface", - "internal", - "is", - "it", - "lateinit", - "lazy", - "noinline", - "null", - "object", - "open", - "operator", - "out", - "override", - "package", - "private", - "protected", - "public", - "reified", - "return", - "sealed", - "super", - "suspend", - "tailrec", - "this", - "throw", - "true", - "try", - "typealias", - "typeof", - "val", - "var", - "vararg", - "when", - "while" - )); - - defaultIncludes = new HashSet(Arrays.asList( - "kotlin.Byte", - "kotlin.Short", - "kotlin.Int", - "kotlin.Long", - "kotlin.Float", - "kotlin.Double", - "kotlin.Boolean", - "kotlin.Char", - "kotlin.Array", - "kotlin.collections.List", - "kotlin.collections.Set", - "kotlin.collections.Map" - )); - - typeMapping = new HashMap(); - typeMapping.put("string", "kotlin.String"); - typeMapping.put("boolean", "kotlin.Boolean"); - typeMapping.put("integer", "kotlin.Int"); - typeMapping.put("float", "kotlin.Float"); - typeMapping.put("long", "kotlin.Long"); - typeMapping.put("double", "kotlin.Double"); - typeMapping.put("number", "java.math.BigDecimal"); - typeMapping.put("date-time", "java.time.LocalDateTime"); - typeMapping.put("date", "java.time.LocalDateTime"); - typeMapping.put("file", "java.io.File"); - typeMapping.put("array", "kotlin.Array"); - typeMapping.put("list", "kotlin.Array"); - typeMapping.put("map", "kotlin.collections.Map"); - typeMapping.put("object", "kotlin.Any"); - typeMapping.put("binary", "kotlin.Array"); - typeMapping.put("Date", "java.time.LocalDateTime"); - typeMapping.put("DateTime", "java.time.LocalDateTime"); - - instantiationTypes.put("array", "arrayOf"); - instantiationTypes.put("list", "arrayOf"); - instantiationTypes.put("map", "mapOf"); - - importMapping = new HashMap(); - importMapping.put("BigDecimal", "java.math.BigDecimal"); - importMapping.put("UUID", "java.util.UUID"); - importMapping.put("File", "java.io.File"); - importMapping.put("Date", "java.util.Date"); - importMapping.put("Timestamp", "java.sql.Timestamp"); - importMapping.put("DateTime", "java.time.LocalDateTime"); - importMapping.put("LocalDateTime", "java.time.LocalDateTime"); - importMapping.put("LocalDate", "java.time.LocalDate"); - importMapping.put("LocalTime", "java.time.LocalTime"); - - specialCharReplacements.put(";", "Semicolon"); - - cliOptions.clear(); - cliOptions.add(new CliOption(CodegenConstants.SOURCE_FOLDER, CodegenConstants.SOURCE_FOLDER_DESC).defaultValue(sourceFolder)); - cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "Client package name (e.g. io.swagger).").defaultValue(this.packageName)); - cliOptions.add(new CliOption(CodegenConstants.GROUP_ID, "Client package's organization (i.e. maven groupId).").defaultValue(groupId)); - cliOptions.add(new CliOption(CodegenConstants.ARTIFACT_ID, "Client artifact id (name of generated jar).").defaultValue(artifactId)); - cliOptions.add(new CliOption(CodegenConstants.ARTIFACT_VERSION, "Client package version.").defaultValue(artifactVersion)); - - CliOption enumPropertyNamingOpt = new CliOption(CodegenConstants.ENUM_PROPERTY_NAMING, CodegenConstants.ENUM_PROPERTY_NAMING_DESC); - cliOptions.add(enumPropertyNamingOpt.defaultValue(enumPropertyNaming.name())); } public CodegenType getTag() { @@ -198,78 +42,10 @@ public class KotlinClientCodegen extends DefaultCodegen implements CodegenConfig return "Generates a kotlin client."; } - public void setArtifactId(String artifactId) { - this.artifactId = artifactId; - } - - public void setArtifactVersion(String artifactVersion) { - this.artifactVersion = artifactVersion; - } - - public void setGroupId(String groupId) { - this.groupId = groupId; - } - - public void setPackageName(String packageName) { - this.packageName = packageName; - } - - public void setSourceFolder(String sourceFolder) { - this.sourceFolder = sourceFolder; - } - @Override public void processOpts() { super.processOpts(); - if (additionalProperties.containsKey(CodegenConstants.ENUM_PROPERTY_NAMING)) { - setEnumPropertyNaming((String) additionalProperties.get(CodegenConstants.ENUM_PROPERTY_NAMING)); - } - - if (additionalProperties.containsKey(CodegenConstants.SOURCE_FOLDER)) { - this.setSourceFolder((String) additionalProperties.get(CodegenConstants.SOURCE_FOLDER)); - } else { - additionalProperties.put(CodegenConstants.SOURCE_FOLDER, sourceFolder); - } - - if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) { - this.setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME)); - if (!additionalProperties.containsKey(CodegenConstants.MODEL_PACKAGE)) - this.setModelPackage(packageName + ".models"); - if (!additionalProperties.containsKey(CodegenConstants.API_PACKAGE)) - this.setApiPackage(packageName + ".apis"); - } else { - additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName); - } - - if(additionalProperties.containsKey(CodegenConstants.ARTIFACT_ID)) { - this.setArtifactId((String) additionalProperties.get(CodegenConstants.ARTIFACT_ID)); - } else { - additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId); - } - - if(additionalProperties.containsKey(CodegenConstants.GROUP_ID)) { - this.setGroupId((String) additionalProperties.get(CodegenConstants.GROUP_ID)); - } else { - additionalProperties.put(CodegenConstants.GROUP_ID, groupId); - } - - if(additionalProperties.containsKey(CodegenConstants.ARTIFACT_VERSION)) { - this.setArtifactVersion((String) additionalProperties.get(CodegenConstants.ARTIFACT_VERSION)); - } else { - additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion); - } - - if (additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) { - LOGGER.warn(CodegenConstants.INVOKER_PACKAGE + " with " + this.getName() + " generator is ignored. Use " + CodegenConstants.PACKAGE_NAME + "."); - } - - additionalProperties.put(CodegenConstants.API_PACKAGE, apiPackage()); - additionalProperties.put(CodegenConstants.MODEL_PACKAGE, modelPackage()); - - additionalProperties.put("apiDocPath", apiDocPath); - additionalProperties.put("modelDocPath", modelDocPath); - supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); supportingFiles.add(new SupportingFile("build.gradle.mustache", "", "build.gradle")); @@ -287,251 +63,4 @@ public class KotlinClientCodegen extends DefaultCodegen implements CodegenConfig supportingFiles.add(new SupportingFile("infrastructure/Serializer.kt.mustache", infrastructureFolder, "Serializer.kt")); supportingFiles.add(new SupportingFile("infrastructure/Errors.kt.mustache", infrastructureFolder, "Errors.kt")); } - - /** - * Sets the naming convention for Kotlin enum properties - * - * @param enumPropertyNamingType The string representation of the naming convention, as defined by {@link CodegenConstants.ENUM_PROPERTY_NAMING_TYPE} - */ - public void setEnumPropertyNaming(final String enumPropertyNamingType) { - try { - this.enumPropertyNaming = CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.valueOf(enumPropertyNamingType); - } catch (IllegalArgumentException ex) { - StringBuilder sb = new StringBuilder(enumPropertyNamingType + " is an invalid enum property naming option. Please choose from:"); - for (CodegenConstants.ENUM_PROPERTY_NAMING_TYPE t : CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.values()) { - sb.append("\n ").append(t.name()); - } - throw new RuntimeException(sb.toString()); - } - } - - public CodegenConstants.ENUM_PROPERTY_NAMING_TYPE getEnumPropertyNaming() { - return this.enumPropertyNaming; - } - - @Override - public String escapeUnsafeCharacters(String input) { - return input.replace("*/", "*_/").replace("/*", "/_*"); - } - - @Override - public String escapeQuotationMark(String input) { - // remove " to avoid code injection - return input.replace("\"", ""); - } - - @Override - public String apiDocFileFolder() { - return (outputFolder + "/" + apiDocPath).replace('/', File.separatorChar); - } - - @Override - public String apiFileFolder() { - return outputFolder + File.separator + sourceFolder + File.separator + apiPackage().replace('.', File.separatorChar); - } - - @Override - public String modelDocFileFolder() { - return (outputFolder + "/" + modelDocPath).replace('/', File.separatorChar); - } - - - @Override - public String modelFileFolder() { - return outputFolder + File.separator + sourceFolder + File.separator + modelPackage().replace('.', File.separatorChar); - } - - @Override - public String escapeReservedWord(String name) { - // TODO: Allow enum escaping as an option (e.g. backticks vs append/prepend underscore vs match model property escaping). - return String.format("`%s`", name); - } - - /** - * Output the proper model name (capitalized). - * In case the name belongs to the TypeSystem it won't be renamed. - * - * @param name the name of the model - * @return capitalized model name - */ - @Override - public String toModelName(final String name) { - // Allow for explicitly configured kotlin.* and java.* types - if (name.startsWith("kotlin.") || name.startsWith("java.")) { - return name; - } - - // If importMapping contains name, assume this is a legitimate model name. - if (importMapping.containsKey(name)) { - return importMapping.get(name); - } - - String modifiedName = name.replaceAll("\\.", ""); - modifiedName = sanitizeKotlinSpecificNames(modifiedName); - - if (reservedWords.contains(modifiedName)) { - modifiedName = escapeReservedWord(modifiedName); - } - - return titleCase(modifiedName); - } - - /** - * returns the swagger type for the property - * - * @param p Swagger property object - * @return string presentation of the type - **/ - @Override - public String getSwaggerType(Property p) { - String swaggerType = super.getSwaggerType(p); - String type; - // This maps, for example, long -> kotlin.Long based on hashes in this type's constructor - if (typeMapping.containsKey(swaggerType)) { - type = typeMapping.get(swaggerType); - if (languageSpecificPrimitives.contains(type)) { - return toModelName(type); - } - } else { - type = swaggerType; - } - return toModelName(type); - } - - /** - * Output the type declaration of the property - * - * @param p Swagger Property object - * @return a string presentation of the property type - */ - @Override - public String getTypeDeclaration(Property p) { - if (p instanceof ArrayProperty) { - ArrayProperty ap = (ArrayProperty) p; - Property inner = ap.getItems(); - return getSwaggerType(p) + "<" + getTypeDeclaration(inner) + ">"; - } else if (p instanceof MapProperty) { - MapProperty mp = (MapProperty) p; - Property inner = mp.getAdditionalProperties(); - - // Maps will be keyed only by primitive Kotlin string - return getSwaggerType(p) + ""; - } - return super.getTypeDeclaration(p); - } - - /** - * Check the type to see if it needs import the library/module/package - * - * @param type name of the type - * @return true if the library/module/package of the corresponding type needs to be imported - */ - @Override - protected boolean needToImport(String type) { - // provides extra protection against improperly trying to import language primitives and java types - boolean imports = !type.startsWith("kotlin.") && !type.startsWith("java.") && !defaultIncludes.contains(type) && !languageSpecificPrimitives.contains(type); - return imports; - } - - /** - * Return the fully-qualified "Model" name for import - * - * @param name the name of the "Model" - * @return the fully-qualified "Model" name for import - */ - @Override - public String toModelImport(String name) { - // toModelImport is called while processing operations, but DefaultCodegen doesn't - // define imports correctly with fully qualified primitives and models as defined in this generator. - if(needToImport(name)) { - return super.toModelImport(name); - } - - return name; - } - - @Override - public Map postProcessModels(Map objs) { - return postProcessModelsEnum(super.postProcessModels(objs)); - } - - /** - * Return the sanitized variable name for enum - * - * @param value enum variable name - * @param datatype data type - * @return the sanitized variable name for enum - */ - @Override - public String toEnumVarName(String value, String datatype) { - String modified; - if (value.length() == 0) { - modified = "EMPTY"; - } else { - modified = value; - modified = sanitizeKotlinSpecificNames(modified); - } - - switch (getEnumPropertyNaming()) { - case original: - // NOTE: This is provided as a last-case allowance, but will still result in reserved words being escaped. - modified = value; - break; - case camelCase: - // NOTE: Removes hyphens and underscores - modified = camelize(modified, true); - break; - case PascalCase: - // NOTE: Removes hyphens and underscores - String result = camelize(modified); - modified = titleCase(result); - break; - case snake_case: - // NOTE: Removes hyphens - modified = underscore(modified); - break; - case UPPERCASE: - modified = modified.toUpperCase(); - break; - } - - if (reservedWords.contains(modified)) { - return escapeReservedWord(modified); - } - - return modified; - } - - private String titleCase(final String input) { - return input.substring(0, 1).toUpperCase() + input.substring(1); - } - - /** - * Sanitize against Kotlin specific naming conventions, which may differ from those required by {@link DefaultCodegen#sanitizeName}. - * - * @param name string to be sanitize - * @return sanitized string - */ - private String sanitizeKotlinSpecificNames(final String name) { - String word = name; - for (Map.Entry specialCharacters : specialCharReplacements.entrySet()) { - // Underscore is the only special character we'll allow - if (!specialCharacters.getKey().equals("_")) { - word = word.replaceAll("\\Q" + specialCharacters.getKey() + "\\E", specialCharacters.getValue()); - } - } - - // Fallback, replace unknowns with underscore. - word = word.replaceAll("\\W+", "_"); - if (word.matches("\\d.*")) { - word = "_" + word; - } - - // _, __, and ___ are reserved in Kotlin. Treat all names with only underscores consistently, regardless of count. - if (word.matches("^_*$")) { - word = word.replaceAll("\\Q_\\E", "Underscore"); - } - - return word; - } } diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/KotlinServerCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/KotlinServerCodegen.java new file mode 100644 index 00000000000..2e04de7e45e --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/KotlinServerCodegen.java @@ -0,0 +1,226 @@ +package io.swagger.codegen.languages; + +import com.google.common.collect.ImmutableMap; +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; +import io.swagger.codegen.*; + +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.util.*; + +import io.swagger.codegen.mustache.IndentedLambda; +import io.swagger.codegen.mustache.LowercaseLambda; +import io.swagger.codegen.mustache.TitlecaseLambda; +import io.swagger.codegen.mustache.UppercaseLambda; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class KotlinServerCodegen extends AbstractKotlinCodegen { + + public static final String DEFAULT_LIBRARY = Constants.KTOR; + static Logger LOGGER = LoggerFactory.getLogger(KotlinServerCodegen.class); + private Boolean autoHeadFeatureEnabled = true; + private Boolean conditionalHeadersFeatureEnabled = false; + private Boolean hstsFeatureEnabled = true; + private Boolean corsFeatureEnabled = false; + private Boolean compressionFeatureEnabled = true; + + // This is here to potentially warn the user when an option is not supoprted by the target framework. + private Map> optionsSupportedPerFramework = new ImmutableMap.Builder>() + .put(Constants.KTOR, Arrays.asList( + Constants.AUTOMATIC_HEAD_REQUESTS, + Constants.CONDITIONAL_HEADERS, + Constants.HSTS, + Constants.CORS, + Constants.COMPRESSION + )) + .build(); + + /** + * Constructs an instance of `KotlinServerCodegen`. + */ + public KotlinServerCodegen() { + super(); + + artifactId = "kotlin-server"; + packageName = "io.swagger.server"; + outputFolder = "generated-code" + File.separator + "kotlin-server"; + modelTemplateFiles.put("model.mustache", ".kt"); + apiTemplateFiles.put("api.mustache", ".kt"); + embeddedTemplateDir = templateDir = "kotlin-server"; + apiPackage = packageName + ".apis"; + modelPackage = packageName + ".models"; + + supportedLibraries.put("ktor", "ktor framework"); + + // TODO: Configurable server engine. Defaults to netty in build.gradle. + CliOption library = new CliOption(CodegenConstants.LIBRARY, "library template (sub-template) to use"); + library.setDefault(DEFAULT_LIBRARY); + library.setEnum(supportedLibraries); + + cliOptions.add(library); + + addSwitch(Constants.AUTOMATIC_HEAD_REQUESTS, Constants.AUTOMATIC_HEAD_REQUESTS_DESC, getAutoHeadFeatureEnabled()); + addSwitch(Constants.CONDITIONAL_HEADERS, Constants.CONDITIONAL_HEADERS_DESC, getConditionalHeadersFeatureEnabled()); + addSwitch(Constants.HSTS, Constants.HSTS_DESC, getHstsFeatureEnabled()); + addSwitch(Constants.CORS, Constants.CORS_DESC, getCorsFeatureEnabled()); + addSwitch(Constants.COMPRESSION, Constants.COMPRESSION_DESC, getCompressionFeatureEnabled()); + } + + public Boolean getAutoHeadFeatureEnabled() { + return autoHeadFeatureEnabled; + } + + public void setAutoHeadFeatureEnabled(Boolean autoHeadFeatureEnabled) { + this.autoHeadFeatureEnabled = autoHeadFeatureEnabled; + } + + public Boolean getCompressionFeatureEnabled() { + return compressionFeatureEnabled; + } + + public void setCompressionFeatureEnabled(Boolean compressionFeatureEnabled) { + this.compressionFeatureEnabled = compressionFeatureEnabled; + } + + public Boolean getConditionalHeadersFeatureEnabled() { + return conditionalHeadersFeatureEnabled; + } + + public void setConditionalHeadersFeatureEnabled(Boolean conditionalHeadersFeatureEnabled) { + this.conditionalHeadersFeatureEnabled = conditionalHeadersFeatureEnabled; + } + + public Boolean getCorsFeatureEnabled() { + return corsFeatureEnabled; + } + + public void setCorsFeatureEnabled(Boolean corsFeatureEnabled) { + this.corsFeatureEnabled = corsFeatureEnabled; + } + + public String getHelp() { + return "Generates a kotlin server."; + } + + public Boolean getHstsFeatureEnabled() { + return hstsFeatureEnabled; + } + + public void setHstsFeatureEnabled(Boolean hstsFeatureEnabled) { + this.hstsFeatureEnabled = hstsFeatureEnabled; + } + + public String getName() { + return "kotlin-server"; + } + + public CodegenType getTag() { + return CodegenType.SERVER; + } + + @Override + public void processOpts() { + super.processOpts(); + + if (additionalProperties.containsKey(CodegenConstants.LIBRARY)) { + this.setLibrary((String) additionalProperties.get(CodegenConstants.LIBRARY)); + } + + if (additionalProperties.containsKey(Constants.AUTOMATIC_HEAD_REQUESTS)) { + setAutoHeadFeatureEnabled(convertPropertyToBooleanAndWriteBack(Constants.AUTOMATIC_HEAD_REQUESTS)); + } else { + additionalProperties.put(Constants.AUTOMATIC_HEAD_REQUESTS, getAutoHeadFeatureEnabled()); + } + + if (additionalProperties.containsKey(Constants.CONDITIONAL_HEADERS)) { + setConditionalHeadersFeatureEnabled(convertPropertyToBooleanAndWriteBack(Constants.CONDITIONAL_HEADERS)); + } else { + additionalProperties.put(Constants.CONDITIONAL_HEADERS, getConditionalHeadersFeatureEnabled()); + } + + if (additionalProperties.containsKey(Constants.HSTS)) { + setHstsFeatureEnabled(convertPropertyToBooleanAndWriteBack(Constants.HSTS)); + } else { + additionalProperties.put(Constants.HSTS, getHstsFeatureEnabled()); + } + + if (additionalProperties.containsKey(Constants.CORS)) { + setCorsFeatureEnabled(convertPropertyToBooleanAndWriteBack(Constants.CORS)); + } else { + additionalProperties.put(Constants.CORS, getCorsFeatureEnabled()); + } + + if (additionalProperties.containsKey(Constants.COMPRESSION)) { + setCompressionFeatureEnabled(convertPropertyToBooleanAndWriteBack(Constants.COMPRESSION)); + } else { + additionalProperties.put(Constants.COMPRESSION, getCompressionFeatureEnabled()); + } + + Boolean generateApis = additionalProperties.containsKey(CodegenConstants.GENERATE_APIS) && (Boolean)additionalProperties.get(CodegenConstants.GENERATE_APIS); + String packageFolder = (sourceFolder + File.separator + packageName).replace(".", File.separator); + String resourcesFolder = "src/main/resources"; // not sure this can be user configurable. + + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + supportingFiles.add(new SupportingFile("Dockerfile.mustache", "", "Dockerfile")); + + supportingFiles.add(new SupportingFile("build.gradle.mustache", "", "build.gradle")); + supportingFiles.add(new SupportingFile("settings.gradle.mustache", "", "settings.gradle")); + supportingFiles.add(new SupportingFile("gradle.properties", "", "gradle.properties")); + + supportingFiles.add(new SupportingFile("AppMain.kt.mustache", packageFolder, "AppMain.kt")); + supportingFiles.add(new SupportingFile("Configuration.kt.mustache", packageFolder, "Configuration.kt")); + + if (generateApis) { + supportingFiles.add(new SupportingFile("Paths.kt.mustache", packageFolder, "Paths.kt")); + } + + supportingFiles.add(new SupportingFile("application.conf.mustache", resourcesFolder, "application.conf")); + supportingFiles.add(new SupportingFile("logback.xml", resourcesFolder, "logback.xml")); + + final String infrastructureFolder = (sourceFolder + File.separator + packageName + File.separator + "infrastructure").replace(".", File.separator); + + supportingFiles.add(new SupportingFile("ApiKeyAuth.kt.mustache", infrastructureFolder, "ApiKeyAuth.kt")); + + addMustacheLambdas(additionalProperties); + } + + private void addMustacheLambdas(Map objs) { + + Map lambdas = new ImmutableMap.Builder() + .put("lowercase", new LowercaseLambda()) + .put("uppercase", new UppercaseLambda()) + .put("titlecase", new TitlecaseLambda()) + .put("indented", new IndentedLambda()) + .put("indented_8", new IndentedLambda(8, " ")) + .put("indented_12", new IndentedLambda(12, " ")) + .put("indented_16", new IndentedLambda(16, " ")) + .build(); + + if (objs.containsKey("lambda")) { + LOGGER.warn("An property named 'lambda' already exists. Mustache lambdas renamed from 'lambda' to '_lambda'. " + + "You'll likely need to use a custom template, " + + "see https://github.com/swagger-api/swagger-codegen#modifying-the-client-library-format. "); + objs.put("_lambda", lambdas); + } else { + objs.put("lambda", lambdas); + } + } + + public static class Constants { + public final static String KTOR = "ktor"; + public final static String AUTOMATIC_HEAD_REQUESTS = "featureAutoHead"; + public final static String AUTOMATIC_HEAD_REQUESTS_DESC = "Automatically provide responses to HEAD requests for existing routes that have the GET verb defined."; + public final static String CONDITIONAL_HEADERS = "featureConditionalHeaders"; + public final static String CONDITIONAL_HEADERS_DESC = "Avoid sending content if client already has same content, by checking ETag or LastModified properties."; + public final static String HSTS = "featureHSTS"; + public final static String HSTS_DESC = "Avoid sending content if client already has same content, by checking ETag or LastModified properties."; + public final static String CORS = "featureCORS"; + public final static String CORS_DESC = "Ktor by default provides an interceptor for implementing proper support for Cross-Origin Resource Sharing (CORS). See enable-cors.org."; + public final static String COMPRESSION = "featureCompression"; + public final static String COMPRESSION_DESC = "Adds ability to compress outgoing content using gzip, deflate or custom encoder and thus reduce size of the response."; + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ScalaGatlingCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ScalaGatlingCodegen.java index 2e163ba305b..6bbedbd78b4 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ScalaGatlingCodegen.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ScalaGatlingCodegen.java @@ -196,7 +196,7 @@ public class ScalaGatlingCodegen extends AbstractScalaCodegen implements Codegen /** * Modifies the swagger doc to make mustache easier to use * - * @param swagger + * @param swagger input swagger document */ @Override public void preprocessSwagger(Swagger swagger) { diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/StaticHtmlGenerator.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/StaticHtmlGenerator.java index 6942b3b2996..359f6bfe90f 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/StaticHtmlGenerator.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/StaticHtmlGenerator.java @@ -74,7 +74,7 @@ public class StaticHtmlGenerator extends DefaultCodegen implements CodegenConfig /** * Convert Markdown (CommonMark) to HTML. This class also disables normal HTML - * escaping in the Mustache engine (see {@link #processCompiler(Compiler)} above.) + * escaping in the Mustache engine (see {@link DefaultCodegen#processCompiler(Compiler)} above.) */ @Override public String escapeText(String input) { diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/IndentedLambda.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/IndentedLambda.java new file mode 100644 index 00000000000..b7d271c63d7 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/IndentedLambda.java @@ -0,0 +1,93 @@ +package io.swagger.codegen.mustache; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.io.Writer; + +/** + * This naively prepends indention to all lines of a fragment. + *

+ * Generator authors may add helpers for explicitly adding prefixed spaces which fragments won't be aware of. + *

+ * Register: + *

+ * additionalProperties.put("indent4", new IndentedLambda(4));
+ * additionalProperties.put("indent8", new IndentedLambda(8));
+ * 
+ *

+ * Use: + *

{@code
+ *     {{#indent4}}{{>template}}{{/indent4}}
+ *         {{#indent8}}{{>other_template}}{{/indent8}}
+ * }
+ */ +public class IndentedLambda implements Mustache.Lambda { + private final int prefixSpaceCount; + private int spaceCode; + + /** + * Constructs a new instance of {@link IndentedLambda}, with an indent count of 4 spaces + */ + public IndentedLambda() { + this(4, " "); + } + + /** + * Constructs a new instance of {@link IndentedLambda}, with customized indent count and intention character + * + * @param prefixSpaceCount The number of indented characters to apply as a prefix to a fragment. + * @param indentionCharacter String representation of the character used in the indent (e.g. " ", "\t", "."). + */ + public IndentedLambda(int prefixSpaceCount, String indentionCharacter) { + this(prefixSpaceCount, Character.codePointAt(indentionCharacter, 0)); + } + + /** + * Constructs a new instance of {@link IndentedLambda} + * + * @param prefixSpaceCount The number of indented characters to apply as a prefix to a fragment. + */ + private IndentedLambda(int prefixSpaceCount, int indentionCodePoint) { + if (prefixSpaceCount <= 0) { + throw new IllegalArgumentException("prefixSpaceCount must be greater than 0"); + } + + if (!Character.isValidCodePoint(indentionCodePoint)) { + throw new IllegalArgumentException("indentionCodePoint is an invalid code point "); + } + + this.prefixSpaceCount = prefixSpaceCount; + this.spaceCode = indentionCodePoint; + } + + @Override + public void execute(Template.Fragment fragment, Writer writer) throws IOException { + String text = fragment.execute(); + if (text == null || text.length() == 0) { + return; + } + + String prefixedIndention = StringUtils.repeat(new String(Character.toChars(spaceCode)), prefixSpaceCount); + StringBuilder sb = new StringBuilder(); + String[] lines = text.split(System.lineSeparator()); + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + // Mustache will apply correct indentation to the first line of a template (to match declaration location). + // So, we want to skip the first line. + if (i > 0) { + sb.append(prefixedIndention); + } + + sb.append(line); + + // We've split on the system's line separator. We don't want to add an additional trailing line. + if (i < lines.length - 1) { + sb.append(System.lineSeparator()); + } + } + writer.write(sb.toString()); + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/LowercaseLambda.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/LowercaseLambda.java new file mode 100644 index 00000000000..1bc239e850a --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/LowercaseLambda.java @@ -0,0 +1,29 @@ +package io.swagger.codegen.mustache; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; + +import java.io.IOException; +import java.io.Writer; + +/** + * Converts text in a fragment to lowercase. + * + * Register: + *
+ * additionalProperties.put("lowercase", new LowercaseLambda());
+ * 
+ * + * Use: + *
+ * {{#lowercase}}{{httpMethod}}{{/lowercase}}
+ * 
+ */ +public class LowercaseLambda implements Mustache.Lambda { + @Override + public void execute(Template.Fragment fragment, Writer writer) throws IOException { + String text = fragment.execute(); + writer.write(text.toLowerCase()); + + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/TitlecaseLambda.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/TitlecaseLambda.java new file mode 100644 index 00000000000..e2bb2dd7481 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/TitlecaseLambda.java @@ -0,0 +1,68 @@ +package io.swagger.codegen.mustache; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; + +import java.io.IOException; +import java.io.Writer; + +/** + * Converts text in a fragment to title case. + * + * Register: + *
+ * additionalProperties.put("titlecase", new TitlecaseLambda());
+ * 
+ * + * Use: + *
+ * {{#titlecase}}{{classname}}{{/titlecase}}
+ * 
+ */ +public class TitlecaseLambda implements Mustache.Lambda { + private String delimiter; + + /** + * Constructs a new instance of {@link TitlecaseLambda}, which will convert all text + * in a space delimited string to title-case. + */ + public TitlecaseLambda() { + this(" "); + } + + /** + * Constructs a new instance of {@link TitlecaseLambda}, splitting on the specified + * delimiter and converting each word to title-case. + * + * NOTE: passing {@code null} results in a title-casing the first word only. + * + * @param delimiter Provided to allow an override for the default space delimiter. + */ + public TitlecaseLambda(String delimiter) { + this.delimiter = delimiter; + } + + private String titleCase(final String input) { + return input.substring(0, 1).toUpperCase() + input.substring(1); + } + + @Override + public void execute(Template.Fragment fragment, Writer writer) throws IOException { + String text = fragment.execute(); + if (delimiter == null) { + writer.write(titleCase(text)); + return; + } + + // Split accepts regex. \Q and \E wrap the delimiter to create a literal regex, + // so things like "." and "|" aren't treated as their regex equivalents. + String[] parts = text.split("\\Q" + delimiter + "\\E"); + for (int i = 0; i < parts.length; i++) { + String part = parts[i]; + writer.write(titleCase(part)); + if (i != parts.length - 1) { + writer.write(delimiter); + } + } + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/UppercaseLambda.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/UppercaseLambda.java new file mode 100644 index 00000000000..7953d0407de --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/mustache/UppercaseLambda.java @@ -0,0 +1,28 @@ +package io.swagger.codegen.mustache; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; + +import java.io.IOException; +import java.io.Writer; + +/** + * Converts text in a fragment to uppercase. + * + * Register: + *
+ * additionalProperties.put("uppercase", new UppercaseLambda());
+ * 
+ * + * Use: + *
+ * {{#uppercase}}{{summary}}{{/uppercase}}
+ * 
+ */ +public class UppercaseLambda implements Mustache.Lambda { + @Override + public void execute(Template.Fragment fragment, Writer writer) throws IOException { + String text = fragment.execute(); + writer.write(text.toUpperCase()); + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/utils/Markdown.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/utils/Markdown.java index f57f59e9842..05860bc0a63 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/utils/Markdown.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/utils/Markdown.java @@ -18,7 +18,7 @@ public class Markdown { /** * Convert input markdown text to HTML. * Simple text is not wrapped in

...

. - * @param markdown text with Markdown styles. If null, "" is returned. + * @param markdown text with Markdown styles. If null, "" is returned. * @return HTML rendering from the Markdown */ public String toHtml(String markdown) { diff --git a/modules/swagger-codegen/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig b/modules/swagger-codegen/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig index f7d0b32af89..b14bc875b5b 100644 --- a/modules/swagger-codegen/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig +++ b/modules/swagger-codegen/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig @@ -87,3 +87,4 @@ io.swagger.codegen.languages.TypeScriptJqueryClientCodegen io.swagger.codegen.languages.TypeScriptNodeClientCodegen io.swagger.codegen.languages.UndertowCodegen io.swagger.codegen.languages.ZendExpressivePathHandlerServerCodegen +io.swagger.codegen.languages.KotlinServerCodegen diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/README.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/README.mustache new file mode 100644 index 00000000000..7b53516d383 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/README.mustache @@ -0,0 +1,84 @@ +# {{packageName}} - Kotlin Server library for {{appName}} + +## Requires + +* Kotlin 1.1.2 +* Gradle 3.3 + +## Build + +First, create the gradle wrapper script: + +``` +gradle wrapper +``` + +Then, run: + +``` +./gradlew check assemble +``` + +This runs all tests and packages the library. + +## Features/Implementation Notes + +* Supports JSON inputs/outputs, File inputs, and Form inputs. +* Supports collection formats for query parameters: csv, tsv, ssv, pipes. +* Some Kotlin and Java types are fully qualified to avoid conflicts with types defined in Swagger definitions. + +{{#generateApiDocs}} + +## Documentation for API Endpoints + +All URIs are relative to *{{{basePath}}}* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{#summary}}{{{summary}}}{{/summary}} +{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} +{{/generateApiDocs}} + +{{#generateModelDocs}} + +## Documentation for Models + +{{#modelPackage}} +{{#models}}{{#model}} - [{{{modelPackage}}}.{{{classname}}}]({{modelDocPath}}{{{classname}}}.md) +{{/model}}{{/models}} +{{/modelPackage}} +{{^modelPackage}} +No model defined in this package +{{/modelPackage}} +{{/generateModelDocs}} + +{{! TODO: optional documentation for authorization? }} +## Documentation for Authorization + +{{^authMethods}} +All endpoints do not require authorization. +{{/authMethods}} +{{#authMethods}} +{{#last}} +Authentication schemes defined for the API: +{{/last}} +{{/authMethods}} +{{#authMethods}} + +### {{name}} + +{{#isApiKey}}- **Type**: API key +- **API key parameter name**: {{keyParamName}} +- **Location**: {{#isKeyInQuery}}URL query string{{/isKeyInQuery}}{{#isKeyInHeader}}HTTP header{{/isKeyInHeader}} +{{/isApiKey}} +{{#isBasic}}- **Type**: HTTP basic authentication +{{/isBasic}} +{{#isOAuth}}- **Type**: OAuth +- **Flow**: {{flow}} +- **Authorization URL**: {{authorizationUrl}} +- **Scopes**: {{^scopes}}N/A{{/scopes}} +{{#scopes}} - {{scope}}: {{description}} +{{/scopes}} +{{/isOAuth}} + +{{/authMethods}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/api_doc.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/api_doc.mustache new file mode 100644 index 00000000000..d288470ca88 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/api_doc.mustache @@ -0,0 +1,65 @@ +# {{classname}}{{#description}} +{{description}}{{/description}} + +All URIs are relative to *{{basePath}}* + +Method | HTTP request | Description +------------- | ------------- | ------------- +{{#operations}}{{#operation}}[**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{#summary}}{{summary}}{{/summary}} +{{/operation}}{{/operations}} + +{{#operations}} +{{#operation}} + +# **{{operationId}}** +> {{#returnType}}{{returnType}} {{/returnType}}{{operationId}}({{#allParams}}{{{paramName}}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) + +{{summary}}{{#notes}} + +{{notes}}{{/notes}} + +### Example +```kotlin +// Import classes: +//import {{{packageName}}}.infrastructure.* +//import {{{modelPackage}}}.* + +{{! TODO: Auth method documentation examples}} +val apiInstance = {{{classname}}}() +{{#allParams}} +val {{{paramName}}} : {{{dataType}}} = {{{example}}} // {{{dataType}}} | {{{description}}} +{{/allParams}} +try { + {{#returnType}}val result : {{{returnType}}} = {{/returnType}}apiInstance.{{{operationId}}}({{#allParams}}{{{paramName}}}{{#hasMore}}, {{/hasMore}}{{/allParams}}){{#returnType}} + println(result){{/returnType}} +} catch (e: ClientException) { + println("4xx response calling {{{classname}}}#{{{operationId}}}") + e.printStackTrace() +} catch (e: ServerException) { + println("5xx response calling {{{classname}}}#{{{operationId}}}") + e.printStackTrace() +} +``` + +### Parameters +{{^allParams}}This endpoint does not need any parameter.{{/allParams}}{{#allParams}}{{#-last}} +Name | Type | Description | Notes +------------- | ------------- | ------------- | -------------{{/-last}}{{/allParams}} +{{#allParams}} **{{paramName}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isFile}}**{{dataType}}**{{/isFile}}{{^isFile}}{{#generateModelDocs}}[**{{dataType}}**]({{baseType}}.md){{/generateModelDocs}}{{^generateModelDocs}}**{{dataType}}**{{/generateModelDocs}}{{/isFile}}{{/isPrimitiveType}}| {{description}} |{{^required}} [optional]{{/required}}{{#defaultValue}} [default to {{defaultValue}}]{{/defaultValue}}{{#allowableValues}} [enum: {{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}]{{/allowableValues}} +{{/allParams}} + +### Return type + +{{#returnType}}{{#returnTypeIsPrimitive}}**{{returnType}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}{{#generateModelDocs}}[**{{returnType}}**]({{returnBaseType}}.md){{/generateModelDocs}}{{^generateModelDocs}}**{{returnType}}**{{/generateModelDocs}}{{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}}null (empty response body){{/returnType}} + +### Authorization + +{{^authMethods}}No authorization required{{/authMethods}}{{#authMethods}}[{{name}}](../README.md#{{name}}){{^-last}}, {{/-last}}{{/authMethods}} + +### HTTP request headers + + - **Content-Type**: {{#consumes}}{{{mediaType}}}{{#hasMore}}, {{/hasMore}}{{/consumes}}{{^consumes}}Not defined{{/consumes}} + - **Accept**: {{#produces}}{{{mediaType}}}{{#hasMore}}, {{/hasMore}}{{/produces}}{{^produces}}Not defined{{/produces}} + +{{/operation}} +{{/operations}} diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/class_doc.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/class_doc.mustache new file mode 100644 index 00000000000..45e19942734 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/class_doc.mustache @@ -0,0 +1,15 @@ +# {{classname}} + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +{{#vars}}**{{name}}** | {{#isEnum}}[**inline**](#{{datatypeWithEnum}}){{/isEnum}}{{^isEnum}}{{#isPrimitiveType}}**{{datatype}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{datatype}}**]({{complexType}}.md){{/isPrimitiveType}}{{/isEnum}} | {{description}} | {{^required}} [optional]{{/required}}{{#readOnly}} [readonly]{{/readOnly}} +{{/vars}} +{{#vars}}{{#isEnum}} + +{{!NOTE: see java's resources "pojo_doc.mustache" once enums are fully implemented}} +## Enum: {{baseName}} +Name | Value +---- | -----{{#allowableValues}} +{{name}} | {{#values}}{{.}}{{^-last}}, {{/-last}}{{/values}}{{/allowableValues}} +{{/isEnum}}{{/vars}} diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/data_class.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/data_class.mustache new file mode 100644 index 00000000000..1237ec1f431 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/data_class.mustache @@ -0,0 +1,25 @@ +/** + * {{{description}}} +{{#vars}} + * @param {{name}} {{{description}}} +{{/vars}} + */ +data class {{classname}} ( +{{#requiredVars}} +{{>data_class_req_var}}{{^-last}}, +{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, +{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>data_class_opt_var}}{{^-last}}, +{{/-last}}{{/optionalVars}} +) { +{{#hasEnums}}{{#vars}}{{#isEnum}} + /** + * {{{description}}} + * Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} + */ + enum class {{nameInCamelCase}}(val value: {{dataType}}){ + {{#allowableValues}}{{#enumVars}} + {{&name}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} + {{/enumVars}}{{/allowableValues}} + } +{{/isEnum}}{{/vars}}{{/hasEnums}} +} diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/data_class_opt_var.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/data_class_opt_var.mustache new file mode 100644 index 00000000000..a88761ea900 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/data_class_opt_var.mustache @@ -0,0 +1,4 @@ +{{#description}} + /* {{{description}}} */ +{{/description}} + val {{{name}}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{/isEnum}}? = {{#defaultvalue}}{{defaultvalue}}{{/defaultvalue}}{{^defaultvalue}}null{{/defaultvalue}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/data_class_req_var.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/data_class_req_var.mustache new file mode 100644 index 00000000000..8a33a15a188 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/data_class_req_var.mustache @@ -0,0 +1,4 @@ +{{#description}} + /* {{{description}}} */ +{{/description}} + val {{{name}}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{/isEnum}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/enum_class.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/enum_class.mustache new file mode 100644 index 00000000000..791398b9789 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/enum_class.mustache @@ -0,0 +1,9 @@ +/** +* {{{description}}} +* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} +*/ +enum class {{classname}}(val value: {{dataType}}){ +{{#allowableValues}}{{#enumVars}} + {{&name}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} +{{/enumVars}}{{/allowableValues}} +} diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/enum_doc.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/enum_doc.mustache new file mode 100644 index 00000000000..fcb3d7e61aa --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/enum_doc.mustache @@ -0,0 +1,7 @@ +# {{classname}} + +## Enum + +{{#allowableValues}}{{#enumVars}} + * `{{name}}` (value: `{{{value}}}`) +{{/enumVars}}{{/allowableValues}} diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/ApiKeyAuth.kt.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/ApiKeyAuth.kt.mustache new file mode 100644 index 00000000000..39a0ea7a841 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/ApiKeyAuth.kt.mustache @@ -0,0 +1,57 @@ +package {{packageName}}.infrastructure + +import io.ktor.application.ApplicationCall +import io.ktor.application.call +import io.ktor.auth.* +import io.ktor.request.ApplicationRequest +import io.ktor.response.respond + + +import io.ktor.application.* +import io.ktor.pipeline.* +import io.ktor.request.* +import io.ktor.response.* +import java.util.* + +enum class ApiKeyLocation(val location: String) { + QUERY("query"), + HEADER("header") +} +data class ApiKey(val value: String): Credential +data class ApiPrincipal(val apiKey: ApiKey?) : Principal +fun ApplicationCall.apiKey(key: String, keyLocation: ApiKeyLocation = ApiKeyLocation.valueOf("header")): ApiKey? = request.apiKey(key, keyLocation) +fun ApplicationRequest.apiKey(key: String, keyLocation: ApiKeyLocation = ApiKeyLocation.valueOf("header")): ApiKey? { + val value: String? = when(keyLocation) { + ApiKeyLocation.QUERY -> this.queryParameters[key] + ApiKeyLocation.HEADER -> this.headers[key] + } + when (value) { + null -> return null + else -> return ApiKey(value) + } +} + +fun AuthenticationPipeline.apiKeyAuth(apiKeyName: String, authLocation: String, validate: suspend (ApiKey) -> ApiPrincipal?) { + intercept(AuthenticationPipeline.RequestAuthentication) { context -> + val credentials = call.request.apiKey(apiKeyName, ApiKeyLocation.values().first { it.location == authLocation }) + val principal = credentials?.let { validate(it) } + + val cause = when { + credentials == null -> AuthenticationFailedCause.NoCredentials + principal == null -> AuthenticationFailedCause.InvalidCredentials + else -> null + } + + if (cause != null) { + context.challenge(apiKeyName, cause) { + // TODO: Verify correct response structure here. + call.respond(UnauthorizedResponse(HttpAuthHeader.Parameterized("API_KEY", mapOf("key" to apiKeyName), HeaderValueEncoding.QUOTED_ALWAYS))) + it.complete() + } + } + if (principal != null) { + context.principal(principal) + } + } +} + diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/AppMain.kt.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/AppMain.kt.mustache new file mode 100644 index 00000000000..53d4a8ba397 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/AppMain.kt.mustache @@ -0,0 +1,74 @@ +package {{packageName}} + +import com.codahale.metrics.* +import com.typesafe.config.ConfigFactory +import io.ktor.application.* +import io.ktor.client.HttpClient +import io.ktor.client.engine.apache.Apache +import io.ktor.config.HoconApplicationConfig +import io.ktor.features.* +import io.ktor.gson.GsonConverter +import io.ktor.http.ContentType +import io.ktor.locations.* +import io.ktor.metrics.* +import io.ktor.routing.* +import java.util.concurrent.* +{{#generateApis}} +import {{apiPackage}}.* +{{/generateApis}} + +{{#imports}}import {{import}} +{{/imports}} + +internal val settings = HoconApplicationConfig(ConfigFactory.defaultApplication(HTTP::class.java.classLoader)) + +object HTTP { + val client = HttpClient(Apache) +} + +fun Application.main() { + install(DefaultHeaders) + install(Metrics) { + val reporter = Slf4jReporter.forRegistry(registry) + .outputTo(log) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build() + reporter.start(10, TimeUnit.SECONDS) + } +{{#generateApis}} + install(ContentNegotiation) { + register(ContentType.Application.Json, GsonConverter()) + } + {{#featureAutoHead}} + install(AutoHeadResponse) // see http://ktor.io/features/autoheadresponse.html + {{/featureAutoHead}} + {{#featureConditionalHeaders}} + install(ConditionalHeaders) // see http://ktor.io/features/conditional-headers.html + {{/featureConditionalHeaders}} + {{#featureHSTS}} + install(HSTS, ApplicationHstsConfiguration()) // see http://ktor.io/features/hsts.html + {{/featureHSTS}} + {{#featureCORS}} + install(CORS, ApplicationCORSConfiguration()) // see http://ktor.io/features/cors.html + {{/featureCORS}} + {{#featureCompression}} + install(Compression, ApplicationCompressionConfiguration()) // see http://ktor.io/features/compression.html + {{/featureCompression}} + install(Locations) // see http://ktor.io/features/locations.html + install(Routing) { + {{#apiInfo}} + {{#apis}} + {{#operations}} + {{classname}}() + {{/operations}} + {{/apis}} + {{/apiInfo}} + } +{{/generateApis}} + + environment.monitor.subscribe(ApplicationStopping) + { + HTTP.client.close() + } +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/Configuration.kt.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/Configuration.kt.mustache new file mode 100644 index 00000000000..1afaa5ff9ed --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/Configuration.kt.mustache @@ -0,0 +1,108 @@ +package {{packageName}} + +// Use this file to hold package-level internal functions that return receiver object passed to the `install` method. +import io.ktor.auth.OAuthServerSettings +import io.ktor.features.* +import io.ktor.http.* +import java.time.Duration +import java.util.concurrent.Executors + +import {{packageName}}.settings + +{{#featureCORS}} +/** + * Application block for [CORS] configuration. + * + * This file may be excluded in .swagger-codegen-ignore, + * and application specific configuration can be applied in this function. + * + * See http://ktor.io/features/cors.html + */ +internal fun ApplicationCORSConfiguration(): CORS.Configuration.() -> Unit { + return { + // method(HttpMethod.Options) + // header(HttpHeaders.XForwardedProto) + // anyHost() + // host("my-host") + // host("my-host:80") + // host("my-host", subDomains = listOf("www")) + // host("my-host", schemes = listOf("http", "https")) + // allowCredentials = true + // maxAge = Duration.ofDays(1) + } +} +{{/featureCORS}} + +{{#featureHSTS}} +/** + * Application block for [HSTS] configuration. + * + * This file may be excluded in .swagger-codegen-ignore, + * and application specific configuration can be applied in this function. + * + * See http://ktor.io/features/hsts.html + */ +internal fun ApplicationHstsConfiguration(): HSTS.Configuration.() -> Unit { + return { + maxAge = Duration.ofDays(365) + includeSubDomains = true + preload = false + + // You may also apply any custom directives supported by specific user-agent. For example: + // customDirectives.put("redirectHttpToHttps", "false") + } +} +{{/featureHSTS}} + +{{#featureCompression}} +/** + * Application block for [Compression] configuration. + * + * This file may be excluded in .swagger-codegen-ignore, + * and application specific configuration can be applied in this function. + * + * See http://ktor.io/features/compression.html + */ +internal fun ApplicationCompressionConfiguration(): Compression.Configuration.() -> Unit { + return { + gzip { + priority = 1.0 + } + deflate { + priority = 10.0 + minimumSize(1024) // condition + } + } +} +{{/featureCompression}} + +// Defines authentication mechanisms used throughout the application. +val ApplicationAuthProviders: Map = listOf( +{{#authMethods}} + {{#isOAuth}} + OAuthServerSettings.OAuth2ServerSettings( + name = "{{name}}", + authorizeUrl = "{{authorizationUrl}}", + accessTokenUrl = "{{tokenUrl}}", + requestMethod = HttpMethod.Get, + {{! TODO: flow, doesn't seem to be supported yet by ktor }} + clientId = settings.property("auth.oauth.{{name}}.clientId").getString(), + clientSecret = settings.property("auth.oauth.{{name}}.clientSecret").getString(), + defaultScopes = listOf({{#scopes}}"{{scope}}"{{#hasMore}}, {{/hasMore}}{{/scopes}}) + ){{#hasMore}},{{/hasMore}} + {{/isOAuth}} +{{/authMethods}} +// OAuthServerSettings.OAuth2ServerSettings( +// name = "facebook", +// authorizeUrl = "https://graph.facebook.com/oauth/authorize", +// accessTokenUrl = "https://graph.facebook.com/oauth/access_token", +// requestMethod = HttpMethod.Post, +// +// clientId = "settings.property("auth.oauth.facebook.clientId").getString()", +// clientSecret = "settings.property("auth.oauth.facebook.clientSecret").getString()", +// defaultScopes = listOf("public_profile") +// ) +).associateBy { it.name } + +// Provides an application-level fixed thread pool on which to execute coroutines (mainly) +internal val ApplicationExecutors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 4) diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/Dockerfile.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/Dockerfile.mustache new file mode 100644 index 00000000000..b9158ac26e3 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/Dockerfile.mustache @@ -0,0 +1,7 @@ +FROM openjdk:8-jre-alpine + +COPY ./build/libs/{{artifactId}}.jar /root/{{artifactId}}.jar + +WORKDIR /root + +CMD ["java", "-server", "-Xms4g", "-Xmx4g", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "{{artifactId}}.jar"] \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/Paths.kt.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/Paths.kt.mustache new file mode 100644 index 00000000000..a4a4bb7a10b --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/Paths.kt.mustache @@ -0,0 +1,43 @@ +{{>licenseInfo}} +package {{packageName}} + +import io.ktor.application.ApplicationCall +import io.ktor.http.HttpMethod +import io.ktor.locations.* +import io.ktor.pipeline.PipelineContext +import io.ktor.routing.Route +import io.ktor.routing.method +import {{modelPackage}}.* + +{{#imports}} +import {{import}} +{{/imports}} + +// NOTE: ktor-location@0.9.0 is missing extension for Route.delete. This includes it. +inline fun Route.delete(noinline body: suspend PipelineContext.(T) -> Unit): Route { + return location(T::class) { + method(HttpMethod.Delete) { + handle(body) + } + } +} + +{{#apiInfo}} +object Paths { +{{#apis}} +{{#operations}} + {{#operation}} + {{^bodyAllowed}} + /** + * {{summary}} + * {{#unescapedNotes}}{{.}}{{/unescapedNotes}} + {{#allParams}}* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}} + {{/allParams}}*/ + @Location("{{path}}") class {{operationId}}({{#allParams}}val {{paramName}}: {{{dataType}}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) + + {{/bodyAllowed}} + {{/operation}} +{{/operations}} +{{/apis}} +} +{{/apiInfo}} diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/README.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/README.mustache new file mode 100644 index 00000000000..0b7ca34e4ba --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/README.mustache @@ -0,0 +1,101 @@ +# {{packageName}} - Kotlin Server library for {{appName}} + +{{#unescapedAppDescription}} +{{.}} +{{/unescapedAppDescription}} + +Generated by Swagger Codegen {{generatorVersion}} ({{generatedDate}}). + +## Requires + +* Kotlin 1.2.10 +* Gradle 4.3 + +## Build + +First, create the gradle wrapper script: + +``` +gradle wrapper +``` + +Then, run: + +``` +./gradlew check assemble +``` + +This runs all tests and packages the library. + +## Running + +The server builds as a fat jar with a main entrypoint. To start the service, run `java -jar ./build/libs/{{artifactId}}.jar`. + +You may also run in docker: + +``` +docker build -t {{artifactId}} . +docker run -p 8080:8080 {{artifactId}} +``` + +## Features/Implementation Notes + +* Supports JSON inputs/outputs, File inputs, and Form inputs (see ktor documentation for more info). +* ~Supports collection formats for query parameters: csv, tsv, ssv, pipes.~ +* Some Kotlin and Java types are fully qualified to avoid conflicts with types defined in Swagger definitions. + +{{#generateApiDocs}} + +## Documentation for API Endpoints + +All URIs are relative to *{{{basePath}}}* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{#summary}}{{{summary}}}{{/summary}} +{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} +{{/generateApiDocs}} + +{{#generateModelDocs}} + +## Documentation for Models + +{{#modelPackage}} +{{#models}}{{#model}} - [{{{modelPackage}}}.{{{classname}}}]({{modelDocPath}}{{{classname}}}.md) +{{/model}}{{/models}} +{{/modelPackage}} +{{^modelPackage}} +No model defined in this package +{{/modelPackage}} +{{/generateModelDocs}} + +{{! TODO: optional documentation for authorization? }} +## Documentation for Authorization + +{{^authMethods}} +All endpoints do not require authorization. +{{/authMethods}} +{{#authMethods}} +{{#last}} +Authentication schemes defined for the API: +{{/last}} +{{/authMethods}} +{{#authMethods}} + +### {{name}} + +{{#isApiKey}}- **Type**: API key +- **API key parameter name**: {{keyParamName}} +- **Location**: {{#isKeyInQuery}}URL query string{{/isKeyInQuery}}{{#isKeyInHeader}}HTTP header{{/isKeyInHeader}} +{{/isApiKey}} +{{#isBasic}}- **Type**: HTTP basic authentication +{{/isBasic}} +{{#isOAuth}}- **Type**: OAuth +- **Flow**: {{flow}} +- **Authorization URL**: {{authorizationUrl}} +- **Scopes**: {{^scopes}}N/A{{/scopes}} +{{#scopes}} - {{scope}}: {{description}} +{{/scopes}} +{{/isOAuth}} + +{{/authMethods}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/_api_body.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/_api_body.mustache new file mode 100644 index 00000000000..9dcf500f49b --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/_api_body.mustache @@ -0,0 +1,25 @@ +{{#hasAuthMethods}} +{{>libraries/ktor/_principal}} +if (principal == null) { + call.respond(HttpStatusCode.Unauthorized) +} else { + {{#examples}} + {{#-first}} + {{#lambda.indented}}{{>_response}}{{/lambda.indented}} + {{/-first}} + {{/examples}} + {{^examples}} + call.respond(HttpStatusCode.NotImplemented) + {{/examples}} +} +{{/hasAuthMethods}} +{{^hasAuthMethods}} +{{#examples}} +{{#-first}} +{{>libraries/ktor/_response}} +{{/-first}} +{{/examples}} +{{^examples}} +call.respond(HttpStatusCode.NotImplemented) +{{/examples}} +{{/hasAuthMethods}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/_principal.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/_principal.mustache new file mode 100644 index 00000000000..d49c5d537f0 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/_principal.mustache @@ -0,0 +1,11 @@ +{{#authMethods}} +{{#isBasic}} +val principal = call.authentication.principal() +{{/isBasic}} +{{#isApiKey}} +val principal = call.authentication.principal() +{{/isApiKey}} +{{#isOAuth}} +val principal = call.authentication.principal() +{{/isOAuth}} +{{/authMethods}} diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/_response.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/_response.mustache new file mode 100644 index 00000000000..fb26fb4a7f1 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/_response.mustache @@ -0,0 +1,8 @@ +val exampleContentType = "{{{contentType}}}" +val exampleContentString = """{{&example}}""" + +when(exampleContentType) { + "application/json" -> call.respond(gson.fromJson(exampleContentString, empty::class.java)) + "application/xml" -> call.respondText(exampleContentString, ContentType.Text.Xml) + else -> call.respondText(exampleContentString) +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/api.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/api.mustache new file mode 100644 index 00000000000..16b6b9fbf6f --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/api.mustache @@ -0,0 +1,114 @@ +{{>licenseInfo}} +package {{apiPackage}} + +import com.google.gson.Gson +import io.ktor.application.call +import io.ktor.auth.UserIdPrincipal +import io.ktor.auth.authentication +import io.ktor.auth.basicAuthentication +import io.ktor.auth.oauth +import io.ktor.auth.OAuthAccessTokenResponse +import io.ktor.auth.OAuthServerSettings +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.locations.* +import io.ktor.response.respond +import io.ktor.response.respondText +import io.ktor.routing.* + +import kotlinx.coroutines.experimental.asCoroutineDispatcher + +import {{packageName}}.ApplicationAuthProviders +import {{packageName}}.Paths +import {{packageName}}.ApplicationExecutors +import {{packageName}}.HTTP.client +import {{packageName}}.infrastructure.ApiPrincipal +import {{packageName}}.infrastructure.apiKeyAuth + +// ktor 0.9.x is missing io.ktor.locations.DELETE, this adds it. +// see https://github.com/ktorio/ktor/issues/288 +import {{packageName}}.delete + +{{#imports}}import {{import}} +{{/imports}} + +{{#operations}} +fun Route.{{classname}}() { + val gson = Gson() + val empty = mutableMapOf() +{{#operation}} + {{#bodyAllowed}} + + route("{{path}}") { + {{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}} { + {{#lambda.indented_12}}{{>libraries/ktor/_api_body}}{{/lambda.indented_12}} + } + } + {{/bodyAllowed}} + {{^bodyAllowed}} + + {{! NOTE: Locations can be used on routes without body parameters.}} + {{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}} { it: Paths.{{operationId}} -> + {{#lambda.indented_8}}{{>libraries/ktor/_api_body}}{{/lambda.indented_8}} + } + {{/bodyAllowed}} + {{! THis looks a little weird, but it's completely valid Kotlin code, and simplifies templated route logic above. }} + {{#hasAuthMethods}}.apply { + // TODO: ktor doesn't allow different authentication registrations for endpoints sharing the same path but different methods. + // It could be the authentication block is being abused here. Until this is resolved, swallow duplicate exceptions. + + try { + authentication { + {{#authMethods}} + {{#isBasic}} + basicAuthentication("{{{name}}}") { credentials -> + // TODO: "Apply your basic authentication functionality." + // Accessible in-method via call.principal() + if (credentials.name == "Swagger" && "Codegen" == credentials.password) { + UserIdPrincipal(credentials.name) + } else { + null + } + } + {{/isBasic}} + {{#isApiKey}} + // "Implement API key auth ({{{name}}}) for parameter name '{{{keyParamName}}}'." + apiKeyAuth("{{{keyParamName}}}", {{#isKeyInQuery}}"query"{{/isKeyInQuery}}{{#isKeyInHeader}}"header"{{/isKeyInHeader}}) { + // TODO: "Verify key here , accessible as it.value" + if (it.value == "keyboardcat") { + ApiPrincipal(it) + } else { + null + } + } + {{/isApiKey}} + {{#isOAuth}} + {{#bodyAllowed}} + oauth(client, ApplicationExecutors.asCoroutineDispatcher(), { ApplicationAuthProviders["{{{name}}}"] }, { + // TODO: define a callback url here. + "/" + }) + {{/bodyAllowed}} + {{^bodyAllowed}} + oauthAtLocation(client, ApplicationExecutors.asCoroutineDispatcher(), + providerLookup = { ApplicationAuthProviders["{{{name}}}"] }, + urlProvider = { currentLocation, provider -> + // TODO: define a callback url here. + "/" + }) + {{/bodyAllowed}} + {{/isOAuth}} + {{/authMethods}} + } + } catch(e: io.ktor.application.DuplicateApplicationFeatureException){ + application.environment.log.warn("authentication block for '{{path}}' is duplicated in code. " + + "Generated endpoints may need to be merged under a 'route' entry.") + } + } + {{/hasAuthMethods}} + {{^hasAuthMethods}} + + {{/hasAuthMethods}} +{{/operation}} +} +{{/operations}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/application.conf.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/application.conf.mustache new file mode 100644 index 00000000000..032be42fa11 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/application.conf.mustache @@ -0,0 +1,27 @@ +ktor { + deployment { + environment = development + port = 8080 + autoreload = true + watch = [ {{packageName}} ] + } + + application { + modules = [ {{packageName}}.AppMainKt.main ] + } +} + +# Typesafe config allows multiple ways to provide configuration values without hard-coding them here. +# Please see https://github.com/lightbend/config for details. +auth { + oauth { +{{#authMethods}} +{{#isOAuth}} + {{name}} { + clientId = "" + clientSecret = "" + } +{{/isOAuth}} +{{/authMethods}} + } +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/build.gradle.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/build.gradle.mustache new file mode 100644 index 00000000000..494ea0e76f5 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/build.gradle.mustache @@ -0,0 +1,74 @@ +group '{{groupId}}' +version '{{artifactVersion}}' + +task wrapper(type: Wrapper) { + gradleVersion = '4.3' + distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip" +} + +buildscript { + ext.kotlin_version = '1.2.10' + ext.ktor_version = '0.9.1-alpha-9' + ext.shadow_version = '2.0.2' + + repositories { + mavenCentral() + maven { + url "https://plugins.gradle.org/m2/" + } + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "com.github.jengelman.gradle.plugins:shadow:$shadow_version" + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' +apply plugin: 'application' + +mainClassName = "io.ktor.server.netty.DevelopmentEngine" + +// Initialization order with shadow 2.0.1 and Gradle 4.3 is weird. +// See https://github.com/johnrengelman/shadow/issues/336#issuecomment-355402508 +apply plugin: 'com.github.johnrengelman.shadow' + +sourceCompatibility = 1.8 + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +kotlin { + experimental { + coroutines "enable" + } +} + +shadowJar { + baseName = '{{artifactId}}' + classifier = null + version = null +} + +repositories { + mavenCentral() + maven { url "http://dl.bintray.com/kotlin/ktor" } + maven { url "https://dl.bintray.com/kotlin/kotlinx" } +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "io.ktor:ktor-server-netty:$ktor_version" + compile "io.ktor:ktor-metrics:$ktor_version" + compile "io.ktor:ktor-locations:$ktor_version" + compile "io.ktor:ktor-gson:$ktor_version" + compile "io.ktor:ktor-client-core:$ktor_version" + compile "io.ktor:ktor-client-apache:$ktor_version" + compile "ch.qos.logback:logback-classic:1.2.1" + testCompile group: 'junit', name: 'junit', version: '4.12' +} diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/gradle.properties b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/gradle.properties new file mode 100644 index 00000000000..5f1ed7bbe02 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/gradle.properties @@ -0,0 +1 @@ +org.gradle.caching=true \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/licenseInfo.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/licenseInfo.mustache new file mode 100644 index 00000000000..aee680977df --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/licenseInfo.mustache @@ -0,0 +1,11 @@ +/** +* {{{appName}}} +* {{{appDescription}}} +* +* {{#version}}OpenAPI spec version: {{{version}}}{{/version}} +* {{#infoEmail}}Contact: {{{infoEmail}}}{{/infoEmail}} +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/logback.xml b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/logback.xml new file mode 100644 index 00000000000..d0eaba8debd --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/libraries/ktor/logback.xml @@ -0,0 +1,15 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/licenseInfo.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/licenseInfo.mustache new file mode 100644 index 00000000000..aee680977df --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/licenseInfo.mustache @@ -0,0 +1,11 @@ +/** +* {{{appName}}} +* {{{appDescription}}} +* +* {{#version}}OpenAPI spec version: {{{version}}}{{/version}} +* {{#infoEmail}}Contact: {{{infoEmail}}}{{/infoEmail}} +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/model.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/model.mustache new file mode 100644 index 00000000000..780dd84b97e --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/model.mustache @@ -0,0 +1,11 @@ +{{>licenseInfo}} +package {{modelPackage}} + +{{#imports}}import {{import}} +{{/imports}} + +{{#models}} +{{#model}} +{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{>data_class}}{{/isEnum}} +{{/model}} +{{/models}} diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/model_doc.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/model_doc.mustache new file mode 100644 index 00000000000..e3b71842118 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/model_doc.mustache @@ -0,0 +1,3 @@ +{{#models}}{{#model}} +{{#isEnum}}{{>enum_doc}}{{/isEnum}}{{^isEnum}}{{>class_doc}}{{/isEnum}} +{{/model}}{{/models}} diff --git a/modules/swagger-codegen/src/main/resources/kotlin-server/settings.gradle.mustache b/modules/swagger-codegen/src/main/resources/kotlin-server/settings.gradle.mustache new file mode 100644 index 00000000000..448dc07602e --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/kotlin-server/settings.gradle.mustache @@ -0,0 +1 @@ +rootProject.name = '{{artifactId}}' \ No newline at end of file diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/IndentedLambdaTest.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/IndentedLambdaTest.java new file mode 100644 index 00000000000..8523f8a034f --- /dev/null +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/IndentedLambdaTest.java @@ -0,0 +1,87 @@ +package io.swagger.codegen.mustache; + +import org.apache.commons.lang3.StringUtils; +import org.testng.annotations.Test; + +import java.util.Arrays; + +import static org.testng.Assert.assertEquals; + +public class IndentedLambdaTest extends MustacheTestBase { + + @Test(description = "indents by 4 spaces by default") + public void testFourSpaceIndent() throws Exception { + // Arrange + String indent = StringUtils.repeat(" ", 4); + String template = " {{#indent_4}}{{value}}{{/indent_4}}"; + String input = StringUtils.join( + Arrays.asList( + "line1", + "line2", + "", + "line3" + ), System.lineSeparator()); + String expected = StringUtils.join( + Arrays.asList( + indent + "line1", + indent + "line2", + indent + "", + indent + "line3" + ), System.lineSeparator()); + Object ctx = context( + "indent_4", new IndentedLambda(), + "value", input + ); + + // Act + String actual = compile(template, ctx); + + // Assert + assertEquals(actual, expected); + } + + @Test + public void testCustomCountAndDelim() throws Exception { + // Arrange + int count = 12; + String delim = "."; + String indent = StringUtils.repeat(delim, count); + String template = indent + "{{#indent_"+count+"}}{{value}}{{/indent_"+count+"}}"; + String input = StringUtils.join( + Arrays.asList( + "line1", + "line2", + "", + "line3" + ), System.lineSeparator()); + String expected = StringUtils.join( + Arrays.asList( + indent + "line1", + indent + "line2", + indent + "", + indent + "line3" + ), System.lineSeparator()); + Object ctx = context( + "indent_" + count, new IndentedLambda(count, delim), + "value", input + ); + + // Act + String actual = compile(template, ctx); + + // Assert + assertEquals(actual, expected); + } + + @Test(description = "throws illegal arg for count < 0.", + expectedExceptions = { IllegalArgumentException.class }, + expectedExceptionsMessageRegExp = "prefixSpaceCount must be greater than 0" + ) + public void testRequiresValidCount() throws Exception { + // Arrange + int count = -1; + + // Act + IndentedLambda instance = new IndentedLambda(count, " "); + } +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/LowercaseLambdaTest.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/LowercaseLambdaTest.java new file mode 100644 index 00000000000..8405f732cf4 --- /dev/null +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/LowercaseLambdaTest.java @@ -0,0 +1,31 @@ +package io.swagger.codegen.mustache; + +import org.testng.annotations.Test; + +import static org.testng.Assert.*; + +public class LowercaseLambdaTest extends MustacheTestBase { + + @Test(description = "lowercases expected inputs") + public void testExecute() throws Exception { + // Arrange + String template = "{{#lowercase}}{{value}}{{/lowercase}}"; + Object lowercaseCtx = context( + "lowercase", new LowercaseLambda(), + "value", "lowercase input" + ); + Object uppercaseCtx = context( + "lowercase", new LowercaseLambda(), + "value", "UPPERCASE INPUT" + ); + + // Act + String lowercaseResult = compile(template, lowercaseCtx); + String uppercaseResult = compile(template, uppercaseCtx); + + + // Assert + assertEquals(lowercaseResult, "lowercase input"); + assertEquals(uppercaseResult, "uppercase input"); + } +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/MustacheTestBase.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/MustacheTestBase.java new file mode 100644 index 00000000000..4580b2b00d3 --- /dev/null +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/MustacheTestBase.java @@ -0,0 +1,22 @@ +package io.swagger.codegen.mustache; + +import com.samskivert.mustache.Mustache; + +import java.util.HashMap; +import java.util.Map; + +public abstract class MustacheTestBase { + protected Object context(Object... data) { + Map ctx = new HashMap<>(); + if (data.length % 2 != 0) { + throw new IllegalArgumentException("context helper accepts pairs of key/value varargs"); + } + for (int i = 0; i < data.length; i += 2) { + ctx.put(data[i].toString(), data[i + 1]); + } + return ctx; + } + protected String compile(String template, Object context) { + return Mustache.compiler().compile(template).execute(context); + } +} diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/TitlecaseLambdaTest.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/TitlecaseLambdaTest.java new file mode 100644 index 00000000000..b5ca7fa4493 --- /dev/null +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/TitlecaseLambdaTest.java @@ -0,0 +1,78 @@ +package io.swagger.codegen.mustache; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +public class TitlecaseLambdaTest extends MustacheTestBase { + + private String template = "{{#titlecase}}{{value}}{{/titlecase}}"; + + @Test(description = "title cases single word") + public void testTitlecase() throws Exception { + // Arrange + String input = "single"; + String expected = "Single"; + Object ctx = context( + "titlecase", new TitlecaseLambda(), + "value", input + ); + + // Act + String actual = compile(template, ctx); + + // Assert + assertEquals(actual, expected); + } + + @Test(description = "title cases multiple words based on custom delimeter") + public void testTitlecaseCustomDelim() throws Exception { + // Arrange + String input = "one|or|more|words"; + String expected = "One|Or|More|Words"; + Object ctx = context( + "titlecase", new TitlecaseLambda("|"), + "value", input + ); + + // Act + String actual = compile(template, ctx); + + // Assert + assertEquals(actual, expected); + } + + @Test(description = "title cases first word when delim is null") + public void testTitlecaseFirstWord() throws Exception { + // Arrange + String input = "one or more words"; + String expected = "One or more words"; + Object ctx = context( + "titlecase", new TitlecaseLambda(null), + "value", input + ); + + // Act + String actual = compile(template, ctx); + + // Assert + assertEquals(actual, expected); + } + + @Test(description = "title cases multiple words") + public void testTitlecaseMultipleWords() throws Exception { + // Arrange + String input = "one or more words"; + String expected = "One Or More Words"; + Object ctx = context( + "titlecase", new TitlecaseLambda(), + "value", input + ); + + // Act + String actual = compile(template, ctx); + + // Assert + assertEquals(actual, expected); + } +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/UppercaseLambdaTest.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/UppercaseLambdaTest.java new file mode 100644 index 00000000000..78e1d05b96e --- /dev/null +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/mustache/UppercaseLambdaTest.java @@ -0,0 +1,36 @@ +package io.swagger.codegen.mustache; + +import com.samskivert.mustache.Mustache; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.testng.Assert.*; + +public class UppercaseLambdaTest extends MustacheTestBase { + + @Test(description = "uppercases expected inputs") + public void testExecute() throws Exception { + // Arrange + String template = "{{#uppercase}}{{value}}{{/uppercase}}"; + Object lowercaseCtx = context( + "uppercase", new UppercaseLambda(), + "value", "lowercase input" + ); + Object uppercaseCtx = context( + "uppercase", new UppercaseLambda(), + "value", "UPPERCASE INPUT" + ); + + // Act + String lowercaseResult = compile(template, lowercaseCtx); + String uppercaseResult = compile(template, uppercaseCtx); + + + // Assert + assertEquals(lowercaseResult, "LOWERCASE INPUT"); + assertEquals(uppercaseResult, "UPPERCASE INPUT"); + } +} \ No newline at end of file diff --git a/new.sh b/new.sh index cace3efcad3..9e8f2d6f809 100755 --- a/new.sh +++ b/new.sh @@ -1,34 +1,76 @@ #!/usr/bin/env bash set -euo pipefail +set -o noclobber usage() { - echo "Stubs out files for new generators" && \ - echo "usage:" && \ - echo "$0 [options]" && \ - echo " Options:" - grep "[[:space:]].)\ #" $0 | tr -d "#" | sed -r 's/( \| \*)//' | sed -r 's/([a-z])\)/-\1/'; +cat <&2 + usage >&2 + exit 1 + fi +} [ $# -eq 0 ] && usage while getopts ":hcsdtn:" arg; do case ${arg} in - n) # Required. Specify generator name. + n) # Required. Specify generator name, should be kebab-cased. gen_name=${OPTARG} ;; c) # Create a client generator + checkPreviousGenType gen_type=client ;; s) # Create a server generator + checkPreviousGenType gen_type=server ;; d) # Create a documentation generator + checkPreviousGenType gen_type=documentation ;; t) # When specified, creates test file(s) for the generator. @@ -43,9 +85,26 @@ done [ -z "${gen_name}" ] && usage -lang_classname=$(echo "${gen_name}-${gen_type}Codegen" | sed -r 's/(^|_|-)([a-z])/\U\2/g') -gen_name_camel=$(echo "${gen_name}" | sed -r 's/(^|_|-)([a-z])/\U\2/g' | sed 's/^./\L&/') -codegen_type_enum=$(echo "${gen_type}" | sed -r 's/(.)/\U\1/g') +titleCase() { + if [ "$os" == "darwin" ]; then + echo $1 | tr '-' ' ' | tr '_' ' ' | ruby -e "print STDIN.gets.split.map(&:capitalize).join(' ')" | tr -d ' ' + else + read -ra words <<< $(echo $1 | tr '-' ' ' | tr '_' ' ') + echo "${words[@]^}" | tr -d ' ' + fi +} + +kebabCase() { + echo $1 | tr '-' ' ' | tr '_' ' ' | tr '[:upper:]' '[:lower:]' | tr ' ' '-' +} + +upperCase() { + echo $1 | tr '[[:lower:]]' '[[:upper:]]' +} + +declare lang_classname=$(titleCase "${gen_name}-${gen_type}-Codegen") +declare gen_name_camel=$(kebabCase "${gen_name}") +declare codegen_type_enum=$(upperCase "${gen_type}") # Step 1: Add Language Generator [ -f "${root}/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/${lang_classname}.java" ] && \ @@ -109,7 +168,7 @@ echo "Creating modules/swagger-codegen/src/main/resources/${gen_name}-${gen_type touch "${root}/modules/swagger-codegen/src/main/resources/${gen_name}-${gen_type}/README.md" echo "Creating modules/swagger-codegen/src/main/resources/${gen_name}-${gen_type}/model.mustache" && \ touch "${root}/modules/swagger-codegen/src/main/resources/${gen_name}-${gen_type}/model.mustache" -echo "Creating /modules/swagger-codegen/src/main/resources/${gen_name}-${gen_type}/api.mustache" && \ +echo "Creating modules/swagger-codegen/src/main/resources/${gen_name}-${gen_type}/api.mustache" && \ touch "${root}/modules/swagger-codegen/src/main/resources/${gen_name}-${gen_type}/api.mustache" # Step 4: Create bash/batch scripts diff --git a/samples/server/petstore/kotlin-server/ktor/.gitignore b/samples/server/petstore/kotlin-server/ktor/.gitignore new file mode 100644 index 00000000000..e0f5a83cef1 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/.gitignore @@ -0,0 +1,4 @@ +build/ +gradle/ +gradlew +gradlew.bat diff --git a/samples/server/petstore/kotlin-server/ktor/.swagger-codegen-ignore b/samples/server/petstore/kotlin-server/ktor/.swagger-codegen-ignore new file mode 100644 index 00000000000..c5fa491b4c5 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/.swagger-codegen-ignore @@ -0,0 +1,23 @@ +# Swagger Codegen Ignore +# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/server/petstore/kotlin-server/ktor/.swagger-codegen/VERSION b/samples/server/petstore/kotlin-server/ktor/.swagger-codegen/VERSION new file mode 100644 index 00000000000..cc6612c36e0 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/.swagger-codegen/VERSION @@ -0,0 +1 @@ +2.3.0 \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor/Dockerfile b/samples/server/petstore/kotlin-server/ktor/Dockerfile new file mode 100644 index 00000000000..0c1f942d32a --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:8-jre-alpine + +COPY ./build/libs/kotlin-server.jar /root/kotlin-server.jar + +WORKDIR /root + +CMD ["java", "-server", "-Xms4g", "-Xmx4g", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "kotlin-server.jar"] \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor/README.md b/samples/server/petstore/kotlin-server/ktor/README.md new file mode 100644 index 00000000000..c7619e10b0a --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/README.md @@ -0,0 +1,104 @@ +# io.swagger.server - Kotlin Server library for Swagger Petstore + +This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. + +Generated by Swagger Codegen 2.3.0 (2018-01-21T22:11:17.518-05:00). + +## Requires + +* Kotlin 1.2.10 +* Gradle 4.3 + +## Build + +First, create the gradle wrapper script: + +``` +gradle wrapper +``` + +Then, run: + +``` +./gradlew check assemble +``` + +This runs all tests and packages the library. + +## Running + +The server builds as a fat jar with a main entrypoint. To start the service, run `java -jar ./build/libs/kotlin-server.jar`. + +You may also run in docker: + +``` +docker build -t kotlin-server . +docker run -p 8080:8080 kotlin-server +``` + +## Features/Implementation Notes + +* Supports JSON inputs/outputs, File inputs, and Form inputs (see ktor documentation for more info). +* ~Supports collection formats for query parameters: csv, tsv, ssv, pipes.~ +* Some Kotlin and Java types are fully qualified to avoid conflicts with types defined in Swagger definitions. + + +## Documentation for API Endpoints + +All URIs are relative to *http://petstore.swagger.io/v2* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +*PetApi* | [**addPet**](docs/PetApi.md#addpet) | **POST** /pet | Add a new pet to the store +*PetApi* | [**deletePet**](docs/PetApi.md#deletepet) | **DELETE** /pet/{petId} | Deletes a pet +*PetApi* | [**findPetsByStatus**](docs/PetApi.md#findpetsbystatus) | **GET** /pet/findByStatus | Finds Pets by status +*PetApi* | [**findPetsByTags**](docs/PetApi.md#findpetsbytags) | **GET** /pet/findByTags | Finds Pets by tags +*PetApi* | [**getPetById**](docs/PetApi.md#getpetbyid) | **GET** /pet/{petId} | Find pet by ID +*PetApi* | [**updatePet**](docs/PetApi.md#updatepet) | **PUT** /pet | Update an existing pet +*PetApi* | [**updatePetWithForm**](docs/PetApi.md#updatepetwithform) | **POST** /pet/{petId} | Updates a pet in the store with form data +*PetApi* | [**uploadFile**](docs/PetApi.md#uploadfile) | **POST** /pet/{petId}/uploadImage | uploads an image +*StoreApi* | [**deleteOrder**](docs/StoreApi.md#deleteorder) | **DELETE** /store/order/{orderId} | Delete purchase order by ID +*StoreApi* | [**getInventory**](docs/StoreApi.md#getinventory) | **GET** /store/inventory | Returns pet inventories by status +*StoreApi* | [**getOrderById**](docs/StoreApi.md#getorderbyid) | **GET** /store/order/{orderId} | Find purchase order by ID +*StoreApi* | [**placeOrder**](docs/StoreApi.md#placeorder) | **POST** /store/order | Place an order for a pet +*UserApi* | [**createUser**](docs/UserApi.md#createuser) | **POST** /user | Create user +*UserApi* | [**createUsersWithArrayInput**](docs/UserApi.md#createuserswitharrayinput) | **POST** /user/createWithArray | Creates list of users with given input array +*UserApi* | [**createUsersWithListInput**](docs/UserApi.md#createuserswithlistinput) | **POST** /user/createWithList | Creates list of users with given input array +*UserApi* | [**deleteUser**](docs/UserApi.md#deleteuser) | **DELETE** /user/{username} | Delete user +*UserApi* | [**getUserByName**](docs/UserApi.md#getuserbyname) | **GET** /user/{username} | Get user by user name +*UserApi* | [**loginUser**](docs/UserApi.md#loginuser) | **GET** /user/login | Logs user into the system +*UserApi* | [**logoutUser**](docs/UserApi.md#logoutuser) | **GET** /user/logout | Logs out current logged in user session +*UserApi* | [**updateUser**](docs/UserApi.md#updateuser) | **PUT** /user/{username} | Updated user + + + +## Documentation for Models + + - [io.swagger.server.models.ApiResponse](docs/ApiResponse.md) + - [io.swagger.server.models.Category](docs/Category.md) + - [io.swagger.server.models.Order](docs/Order.md) + - [io.swagger.server.models.Pet](docs/Pet.md) + - [io.swagger.server.models.Tag](docs/Tag.md) + - [io.swagger.server.models.User](docs/User.md) + + + +## Documentation for Authorization + + +### api_key + +- **Type**: API key +- **API key parameter name**: api_key +- **Location**: HTTP header + + +### petstore_auth + +- **Type**: OAuth +- **Flow**: implicit +- **Authorization URL**: http://petstore.swagger.io/api/oauth/dialog +- **Scopes**: + - write:pets: modify pets in your account + - read:pets: read your pets + diff --git a/samples/server/petstore/kotlin-server/ktor/build.gradle b/samples/server/petstore/kotlin-server/ktor/build.gradle new file mode 100644 index 00000000000..1a4788c1d15 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/build.gradle @@ -0,0 +1,74 @@ +group 'io.swagger' +version '1.0.0' + +task wrapper(type: Wrapper) { + gradleVersion = '4.3' + distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip" +} + +buildscript { + ext.kotlin_version = '1.2.10' + ext.ktor_version = '0.9.1-alpha-9' + ext.shadow_version = '2.0.2' + + repositories { + mavenCentral() + maven { + url "https://plugins.gradle.org/m2/" + } + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "com.github.jengelman.gradle.plugins:shadow:$shadow_version" + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' +apply plugin: 'application' + +mainClassName = "io.ktor.server.netty.DevelopmentEngine" + +// Initialization order with shadow 2.0.1 and Gradle 4.3 is weird. +// See https://github.com/johnrengelman/shadow/issues/336#issuecomment-355402508 +apply plugin: 'com.github.johnrengelman.shadow' + +sourceCompatibility = 1.8 + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +kotlin { + experimental { + coroutines "enable" + } +} + +shadowJar { + baseName = 'kotlin-server' + classifier = null + version = null +} + +repositories { + mavenCentral() + maven { url "http://dl.bintray.com/kotlin/ktor" } + maven { url "https://dl.bintray.com/kotlin/kotlinx" } +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "io.ktor:ktor-server-netty:$ktor_version" + compile "io.ktor:ktor-metrics:$ktor_version" + compile "io.ktor:ktor-locations:$ktor_version" + compile "io.ktor:ktor-gson:$ktor_version" + compile "io.ktor:ktor-client-core:$ktor_version" + compile "io.ktor:ktor-client-apache:$ktor_version" + compile "ch.qos.logback:logback-classic:1.2.1" + testCompile group: 'junit', name: 'junit', version: '4.12' +} diff --git a/samples/server/petstore/kotlin-server/ktor/gradle.properties b/samples/server/petstore/kotlin-server/ktor/gradle.properties new file mode 100644 index 00000000000..5f1ed7bbe02 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/gradle.properties @@ -0,0 +1 @@ +org.gradle.caching=true \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor/settings.gradle b/samples/server/petstore/kotlin-server/ktor/settings.gradle new file mode 100644 index 00000000000..a09a58efab1 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'kotlin-server' \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/AppMain.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/AppMain.kt new file mode 100644 index 00000000000..98864bb1fa2 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/AppMain.kt @@ -0,0 +1,52 @@ +package io.swagger.server + +import com.codahale.metrics.* +import com.typesafe.config.ConfigFactory +import io.ktor.application.* +import io.ktor.client.HttpClient +import io.ktor.client.engine.apache.Apache +import io.ktor.config.HoconApplicationConfig +import io.ktor.features.* +import io.ktor.gson.GsonConverter +import io.ktor.http.ContentType +import io.ktor.locations.* +import io.ktor.metrics.* +import io.ktor.routing.* +import java.util.concurrent.* +import io.swagger.server.apis.* + + +internal val settings = HoconApplicationConfig(ConfigFactory.defaultApplication(HTTP::class.java.classLoader)) + +object HTTP { + val client = HttpClient(Apache) +} + +fun Application.main() { + install(DefaultHeaders) + install(Metrics) { + val reporter = Slf4jReporter.forRegistry(registry) + .outputTo(log) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build() + reporter.start(10, TimeUnit.SECONDS) + } + install(ContentNegotiation) { + register(ContentType.Application.Json, GsonConverter()) + } + install(AutoHeadResponse) // see http://ktor.io/features/autoheadresponse.html + install(HSTS, ApplicationHstsConfiguration()) // see http://ktor.io/features/hsts.html + install(Compression, ApplicationCompressionConfiguration()) // see http://ktor.io/features/compression.html + install(Locations) // see http://ktor.io/features/locations.html + install(Routing) { + PetApi() + StoreApi() + UserApi() + } + + environment.monitor.subscribe(ApplicationStopping) + { + HTTP.client.close() + } +} \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/Configuration.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/Configuration.kt new file mode 100644 index 00000000000..97c522e11fd --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/Configuration.kt @@ -0,0 +1,76 @@ +package io.swagger.server + +// Use this file to hold package-level internal functions that return receiver object passed to the `install` method. +import io.ktor.auth.OAuthServerSettings +import io.ktor.features.* +import io.ktor.http.* +import java.time.Duration +import java.util.concurrent.Executors + +import io.swagger.server.settings + + +/** + * Application block for [HSTS] configuration. + * + * This file may be excluded in .swagger-codegen-ignore, + * and application specific configuration can be applied in this function. + * + * See http://ktor.io/features/hsts.html + */ +internal fun ApplicationHstsConfiguration(): HSTS.Configuration.() -> Unit { + return { + maxAge = Duration.ofDays(365) + includeSubDomains = true + preload = false + + // You may also apply any custom directives supported by specific user-agent. For example: + // customDirectives.put("redirectHttpToHttps", "false") + } +} + +/** + * Application block for [Compression] configuration. + * + * This file may be excluded in .swagger-codegen-ignore, + * and application specific configuration can be applied in this function. + * + * See http://ktor.io/features/compression.html + */ +internal fun ApplicationCompressionConfiguration(): Compression.Configuration.() -> Unit { + return { + gzip { + priority = 1.0 + } + deflate { + priority = 10.0 + minimumSize(1024) // condition + } + } +} + +// Defines authentication mechanisms used throughout the application. +val ApplicationAuthProviders: Map = listOf( + OAuthServerSettings.OAuth2ServerSettings( + name = "petstore_auth", + authorizeUrl = "http://petstore.swagger.io/api/oauth/dialog", + accessTokenUrl = "", + requestMethod = HttpMethod.Get, + clientId = settings.property("auth.oauth.petstore_auth.clientId").getString(), + clientSecret = settings.property("auth.oauth.petstore_auth.clientSecret").getString(), + defaultScopes = listOf("write:pets", "read:pets") + ) +// OAuthServerSettings.OAuth2ServerSettings( +// name = "facebook", +// authorizeUrl = "https://graph.facebook.com/oauth/authorize", +// accessTokenUrl = "https://graph.facebook.com/oauth/access_token", +// requestMethod = HttpMethod.Post, +// +// clientId = "settings.property("auth.oauth.facebook.clientId").getString()", +// clientSecret = "settings.property("auth.oauth.facebook.clientSecret").getString()", +// defaultScopes = listOf("public_profile") +// ) +).associateBy { it.name } + +// Provides an application-level fixed thread pool on which to execute coroutines (mainly) +internal val ApplicationExecutors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 4) diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/Paths.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/Paths.kt new file mode 100644 index 00000000000..336c3059228 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/Paths.kt @@ -0,0 +1,110 @@ +/** +* Swagger Petstore +* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. +* +* OpenAPI spec version: 1.0.0 +* Contact: apiteam@swagger.io +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ +package io.swagger.server + +import io.ktor.application.ApplicationCall +import io.ktor.http.HttpMethod +import io.ktor.locations.* +import io.ktor.pipeline.PipelineContext +import io.ktor.routing.Route +import io.ktor.routing.method +import io.swagger.server.models.* + + +// NOTE: ktor-location@0.9.0 is missing extension for Route.delete. This includes it. +inline fun Route.delete(noinline body: suspend PipelineContext.(T) -> Unit): Route { + return location(T::class) { + method(HttpMethod.Delete) { + handle(body) + } + } +} + +object Paths { + /** + * Deletes a pet + * + * @param petId Pet id to delete + * @param apiKey (optional) + */ + @Location("/pet/{petId}") class deletePet(val petId: kotlin.Long, val apiKey: kotlin.String) + + /** + * Finds Pets by status + * Multiple status values can be provided with comma separated strings + * @param status Status values that need to be considered for filter + */ + @Location("/pet/findByStatus") class findPetsByStatus(val status: kotlin.Array) + + /** + * Finds Pets by tags + * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + * @param tags Tags to filter by + */ + @Location("/pet/findByTags") class findPetsByTags(val tags: kotlin.Array) + + /** + * Find pet by ID + * Returns a single pet + * @param petId ID of pet to return + */ + @Location("/pet/{petId}") class getPetById(val petId: kotlin.Long) + + /** + * Delete purchase order by ID + * For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + * @param orderId ID of the order that needs to be deleted + */ + @Location("/store/order/{orderId}") class deleteOrder(val orderId: kotlin.String) + + /** + * Returns pet inventories by status + * Returns a map of status codes to quantities + */ + @Location("/store/inventory") class getInventory() + + /** + * Find purchase order by ID + * For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions + * @param orderId ID of pet that needs to be fetched + */ + @Location("/store/order/{orderId}") class getOrderById(val orderId: kotlin.Long) + + /** + * Delete user + * This can only be done by the logged in user. + * @param username The name that needs to be deleted + */ + @Location("/user/{username}") class deleteUser(val username: kotlin.String) + + /** + * Get user by user name + * + * @param username The name that needs to be fetched. Use user1 for testing. + */ + @Location("/user/{username}") class getUserByName(val username: kotlin.String) + + /** + * Logs user into the system + * + * @param username The user name for login + * @param password The password for login in clear text + */ + @Location("/user/login") class loginUser(val username: kotlin.String, val password: kotlin.String) + + /** + * Logs out current logged in user session + * + */ + @Location("/user/logout") class logoutUser() + +} diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/apis/PetApi.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/apis/PetApi.kt new file mode 100644 index 00000000000..bf58dd3588e --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/apis/PetApi.kt @@ -0,0 +1,334 @@ +/** +* Swagger Petstore +* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. +* +* OpenAPI spec version: 1.0.0 +* Contact: apiteam@swagger.io +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ +package io.swagger.server.apis + +import com.google.gson.Gson +import io.ktor.application.call +import io.ktor.auth.UserIdPrincipal +import io.ktor.auth.authentication +import io.ktor.auth.basicAuthentication +import io.ktor.auth.oauth +import io.ktor.auth.OAuthAccessTokenResponse +import io.ktor.auth.OAuthServerSettings +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.locations.* +import io.ktor.response.respond +import io.ktor.response.respondText +import io.ktor.routing.* + +import kotlinx.coroutines.experimental.asCoroutineDispatcher + +import io.swagger.server.ApplicationAuthProviders +import io.swagger.server.Paths +import io.swagger.server.ApplicationExecutors +import io.swagger.server.HTTP.client +import io.swagger.server.infrastructure.ApiPrincipal +import io.swagger.server.infrastructure.apiKeyAuth + +// ktor 0.9.x is missing io.ktor.locations.DELETE, this adds it. +// see https://github.com/ktorio/ktor/issues/288 +import io.swagger.server.delete + +import io.swagger.server.models.ApiResponse +import io.swagger.server.models.Pet + +fun Route.PetApi() { + val gson = Gson() + val empty = mutableMapOf() + + route("/pet") { + post { + val principal = call.authentication.principal() + + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized) + } else { + call.respond(HttpStatusCode.NotImplemented) + } + } + } + .apply { + // TODO: ktor doesn't allow different authentication registrations for endpoints sharing the same path but different methods. + // It could be the authentication block is being abused here. Until this is resolved, swallow duplicate exceptions. + + try { + authentication { + oauth(client, ApplicationExecutors.asCoroutineDispatcher(), { ApplicationAuthProviders["petstore_auth"] }, { + // TODO: define a callback url here. + "/" + }) + } + } catch(e: io.ktor.application.DuplicateApplicationFeatureException){ + application.environment.log.warn("authentication block for '/pet' is duplicated in code. " + + "Generated endpoints may need to be merged under a 'route' entry.") + } + } + + delete { it: Paths.deletePet -> + val principal = call.authentication.principal() + + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized) + } else { + call.respond(HttpStatusCode.NotImplemented) + } + } + .apply { + // TODO: ktor doesn't allow different authentication registrations for endpoints sharing the same path but different methods. + // It could be the authentication block is being abused here. Until this is resolved, swallow duplicate exceptions. + + try { + authentication { + oauthAtLocation(client, ApplicationExecutors.asCoroutineDispatcher(), + providerLookup = { ApplicationAuthProviders["petstore_auth"] }, + urlProvider = { currentLocation, provider -> + // TODO: define a callback url here. + "/" + }) + } + } catch(e: io.ktor.application.DuplicateApplicationFeatureException){ + application.environment.log.warn("authentication block for '/pet/{petId}' is duplicated in code. " + + "Generated endpoints may need to be merged under a 'route' entry.") + } + } + + get { it: Paths.findPetsByStatus -> + val principal = call.authentication.principal() + + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized) + } else { + val exampleContentType = "application/xml" + val exampleContentString = """ + 123456789 + doggie + + aeiou + + + + aeiou + """ + + when(exampleContentType) { + "application/json" -> call.respond(gson.fromJson(exampleContentString, empty::class.java)) + "application/xml" -> call.respondText(exampleContentString, ContentType.Text.Xml) + else -> call.respondText(exampleContentString) + } + } + } + .apply { + // TODO: ktor doesn't allow different authentication registrations for endpoints sharing the same path but different methods. + // It could be the authentication block is being abused here. Until this is resolved, swallow duplicate exceptions. + + try { + authentication { + oauthAtLocation(client, ApplicationExecutors.asCoroutineDispatcher(), + providerLookup = { ApplicationAuthProviders["petstore_auth"] }, + urlProvider = { currentLocation, provider -> + // TODO: define a callback url here. + "/" + }) + } + } catch(e: io.ktor.application.DuplicateApplicationFeatureException){ + application.environment.log.warn("authentication block for '/pet/findByStatus' is duplicated in code. " + + "Generated endpoints may need to be merged under a 'route' entry.") + } + } + + get { it: Paths.findPetsByTags -> + val principal = call.authentication.principal() + + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized) + } else { + val exampleContentType = "application/xml" + val exampleContentString = """ + 123456789 + doggie + + aeiou + + + + aeiou + """ + + when(exampleContentType) { + "application/json" -> call.respond(gson.fromJson(exampleContentString, empty::class.java)) + "application/xml" -> call.respondText(exampleContentString, ContentType.Text.Xml) + else -> call.respondText(exampleContentString) + } + } + } + .apply { + // TODO: ktor doesn't allow different authentication registrations for endpoints sharing the same path but different methods. + // It could be the authentication block is being abused here. Until this is resolved, swallow duplicate exceptions. + + try { + authentication { + oauthAtLocation(client, ApplicationExecutors.asCoroutineDispatcher(), + providerLookup = { ApplicationAuthProviders["petstore_auth"] }, + urlProvider = { currentLocation, provider -> + // TODO: define a callback url here. + "/" + }) + } + } catch(e: io.ktor.application.DuplicateApplicationFeatureException){ + application.environment.log.warn("authentication block for '/pet/findByTags' is duplicated in code. " + + "Generated endpoints may need to be merged under a 'route' entry.") + } + } + + get { it: Paths.getPetById -> + val principal = call.authentication.principal() + + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized) + } else { + val exampleContentType = "application/xml" + val exampleContentString = """ + 123456789 + doggie + + aeiou + + + + aeiou + """ + + when(exampleContentType) { + "application/json" -> call.respond(gson.fromJson(exampleContentString, empty::class.java)) + "application/xml" -> call.respondText(exampleContentString, ContentType.Text.Xml) + else -> call.respondText(exampleContentString) + } + } + } + .apply { + // TODO: ktor doesn't allow different authentication registrations for endpoints sharing the same path but different methods. + // It could be the authentication block is being abused here. Until this is resolved, swallow duplicate exceptions. + + try { + authentication { + // "Implement API key auth (api_key) for parameter name 'api_key'." + apiKeyAuth("api_key", "header") { + // TODO: "Verify key here , accessible as it.value" + if (it.value == "keyboardcat") { + ApiPrincipal(it) + } else { + null + } + } + } + } catch(e: io.ktor.application.DuplicateApplicationFeatureException){ + application.environment.log.warn("authentication block for '/pet/{petId}' is duplicated in code. " + + "Generated endpoints may need to be merged under a 'route' entry.") + } + } + + route("/pet") { + put { + val principal = call.authentication.principal() + + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized) + } else { + call.respond(HttpStatusCode.NotImplemented) + } + } + } + .apply { + // TODO: ktor doesn't allow different authentication registrations for endpoints sharing the same path but different methods. + // It could be the authentication block is being abused here. Until this is resolved, swallow duplicate exceptions. + + try { + authentication { + oauth(client, ApplicationExecutors.asCoroutineDispatcher(), { ApplicationAuthProviders["petstore_auth"] }, { + // TODO: define a callback url here. + "/" + }) + } + } catch(e: io.ktor.application.DuplicateApplicationFeatureException){ + application.environment.log.warn("authentication block for '/pet' is duplicated in code. " + + "Generated endpoints may need to be merged under a 'route' entry.") + } + } + + route("/pet/{petId}") { + post { + val principal = call.authentication.principal() + + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized) + } else { + call.respond(HttpStatusCode.NotImplemented) + } + } + } + .apply { + // TODO: ktor doesn't allow different authentication registrations for endpoints sharing the same path but different methods. + // It could be the authentication block is being abused here. Until this is resolved, swallow duplicate exceptions. + + try { + authentication { + oauth(client, ApplicationExecutors.asCoroutineDispatcher(), { ApplicationAuthProviders["petstore_auth"] }, { + // TODO: define a callback url here. + "/" + }) + } + } catch(e: io.ktor.application.DuplicateApplicationFeatureException){ + application.environment.log.warn("authentication block for '/pet/{petId}' is duplicated in code. " + + "Generated endpoints may need to be merged under a 'route' entry.") + } + } + + route("/pet/{petId}/uploadImage") { + post { + val principal = call.authentication.principal() + + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized) + } else { + val exampleContentType = "application/json" + val exampleContentString = """{ + "code" : 0, + "type" : "type", + "message" : "message" + }""" + + when(exampleContentType) { + "application/json" -> call.respond(gson.fromJson(exampleContentString, empty::class.java)) + "application/xml" -> call.respondText(exampleContentString, ContentType.Text.Xml) + else -> call.respondText(exampleContentString) + } + } + } + } + .apply { + // TODO: ktor doesn't allow different authentication registrations for endpoints sharing the same path but different methods. + // It could be the authentication block is being abused here. Until this is resolved, swallow duplicate exceptions. + + try { + authentication { + oauth(client, ApplicationExecutors.asCoroutineDispatcher(), { ApplicationAuthProviders["petstore_auth"] }, { + // TODO: define a callback url here. + "/" + }) + } + } catch(e: io.ktor.application.DuplicateApplicationFeatureException){ + application.environment.log.warn("authentication block for '/pet/{petId}/uploadImage' is duplicated in code. " + + "Generated endpoints may need to be merged under a 'route' entry.") + } + } +} diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/apis/StoreApi.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/apis/StoreApi.kt new file mode 100644 index 00000000000..c27b2134731 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/apis/StoreApi.kt @@ -0,0 +1,132 @@ +/** +* Swagger Petstore +* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. +* +* OpenAPI spec version: 1.0.0 +* Contact: apiteam@swagger.io +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ +package io.swagger.server.apis + +import com.google.gson.Gson +import io.ktor.application.call +import io.ktor.auth.UserIdPrincipal +import io.ktor.auth.authentication +import io.ktor.auth.basicAuthentication +import io.ktor.auth.oauth +import io.ktor.auth.OAuthAccessTokenResponse +import io.ktor.auth.OAuthServerSettings +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.locations.* +import io.ktor.response.respond +import io.ktor.response.respondText +import io.ktor.routing.* + +import kotlinx.coroutines.experimental.asCoroutineDispatcher + +import io.swagger.server.ApplicationAuthProviders +import io.swagger.server.Paths +import io.swagger.server.ApplicationExecutors +import io.swagger.server.HTTP.client +import io.swagger.server.infrastructure.ApiPrincipal +import io.swagger.server.infrastructure.apiKeyAuth + +// ktor 0.9.x is missing io.ktor.locations.DELETE, this adds it. +// see https://github.com/ktorio/ktor/issues/288 +import io.swagger.server.delete + +import io.swagger.server.models.Order + +fun Route.StoreApi() { + val gson = Gson() + val empty = mutableMapOf() + + delete { it: Paths.deleteOrder -> + call.respond(HttpStatusCode.NotImplemented) + } + + + get { it: Paths.getInventory -> + val principal = call.authentication.principal() + + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized) + } else { + val exampleContentType = "application/json" + val exampleContentString = """{ + "key" : 0 + }""" + + when(exampleContentType) { + "application/json" -> call.respond(gson.fromJson(exampleContentString, empty::class.java)) + "application/xml" -> call.respondText(exampleContentString, ContentType.Text.Xml) + else -> call.respondText(exampleContentString) + } + } + } + .apply { + // TODO: ktor doesn't allow different authentication registrations for endpoints sharing the same path but different methods. + // It could be the authentication block is being abused here. Until this is resolved, swallow duplicate exceptions. + + try { + authentication { + // "Implement API key auth (api_key) for parameter name 'api_key'." + apiKeyAuth("api_key", "header") { + // TODO: "Verify key here , accessible as it.value" + if (it.value == "keyboardcat") { + ApiPrincipal(it) + } else { + null + } + } + } + } catch(e: io.ktor.application.DuplicateApplicationFeatureException){ + application.environment.log.warn("authentication block for '/store/inventory' is duplicated in code. " + + "Generated endpoints may need to be merged under a 'route' entry.") + } + } + + get { it: Paths.getOrderById -> + val exampleContentType = "application/xml" + val exampleContentString = """ + 123456789 + 123456789 + 123 + 2000-01-23T04:56:07.000Z + aeiou + true + """ + + when(exampleContentType) { + "application/json" -> call.respond(gson.fromJson(exampleContentString, empty::class.java)) + "application/xml" -> call.respondText(exampleContentString, ContentType.Text.Xml) + else -> call.respondText(exampleContentString) + } + } + + + route("/store/order") { + post { + val exampleContentType = "application/xml" + val exampleContentString = """ + 123456789 + 123456789 + 123 + 2000-01-23T04:56:07.000Z + aeiou + true + """ + + when(exampleContentType) { + "application/json" -> call.respond(gson.fromJson(exampleContentString, empty::class.java)) + "application/xml" -> call.respondText(exampleContentString, ContentType.Text.Xml) + else -> call.respondText(exampleContentString) + } + } + } + +} diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/apis/UserApi.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/apis/UserApi.kt new file mode 100644 index 00000000000..20804c3eda6 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/apis/UserApi.kt @@ -0,0 +1,118 @@ +/** +* Swagger Petstore +* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. +* +* OpenAPI spec version: 1.0.0 +* Contact: apiteam@swagger.io +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ +package io.swagger.server.apis + +import com.google.gson.Gson +import io.ktor.application.call +import io.ktor.auth.UserIdPrincipal +import io.ktor.auth.authentication +import io.ktor.auth.basicAuthentication +import io.ktor.auth.oauth +import io.ktor.auth.OAuthAccessTokenResponse +import io.ktor.auth.OAuthServerSettings +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.locations.* +import io.ktor.response.respond +import io.ktor.response.respondText +import io.ktor.routing.* + +import kotlinx.coroutines.experimental.asCoroutineDispatcher + +import io.swagger.server.ApplicationAuthProviders +import io.swagger.server.Paths +import io.swagger.server.ApplicationExecutors +import io.swagger.server.HTTP.client +import io.swagger.server.infrastructure.ApiPrincipal +import io.swagger.server.infrastructure.apiKeyAuth + +// ktor 0.9.x is missing io.ktor.locations.DELETE, this adds it. +// see https://github.com/ktorio/ktor/issues/288 +import io.swagger.server.delete + +import io.swagger.server.models.User + +fun Route.UserApi() { + val gson = Gson() + val empty = mutableMapOf() + + route("/user") { + post { + call.respond(HttpStatusCode.NotImplemented) + } + } + + + route("/user/createWithArray") { + post { + call.respond(HttpStatusCode.NotImplemented) + } + } + + + route("/user/createWithList") { + post { + call.respond(HttpStatusCode.NotImplemented) + } + } + + + delete { it: Paths.deleteUser -> + call.respond(HttpStatusCode.NotImplemented) + } + + + get { it: Paths.getUserByName -> + val exampleContentType = "application/xml" + val exampleContentString = """ + 123456789 + aeiou + aeiou + aeiou + aeiou + aeiou + aeiou + 123 + """ + + when(exampleContentType) { + "application/json" -> call.respond(gson.fromJson(exampleContentString, empty::class.java)) + "application/xml" -> call.respondText(exampleContentString, ContentType.Text.Xml) + else -> call.respondText(exampleContentString) + } + } + + + get { it: Paths.loginUser -> + val exampleContentType = "application/xml" + val exampleContentString = """aeiou""" + + when(exampleContentType) { + "application/json" -> call.respond(gson.fromJson(exampleContentString, empty::class.java)) + "application/xml" -> call.respondText(exampleContentString, ContentType.Text.Xml) + else -> call.respondText(exampleContentString) + } + } + + + get { it: Paths.logoutUser -> + call.respond(HttpStatusCode.NotImplemented) + } + + + route("/user/{username}") { + put { + call.respond(HttpStatusCode.NotImplemented) + } + } + +} diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/infrastructure/ApiKeyAuth.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/infrastructure/ApiKeyAuth.kt new file mode 100644 index 00000000000..f22067276eb --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/infrastructure/ApiKeyAuth.kt @@ -0,0 +1,57 @@ +package io.swagger.server.infrastructure + +import io.ktor.application.ApplicationCall +import io.ktor.application.call +import io.ktor.auth.* +import io.ktor.request.ApplicationRequest +import io.ktor.response.respond + + +import io.ktor.application.* +import io.ktor.pipeline.* +import io.ktor.request.* +import io.ktor.response.* +import java.util.* + +enum class ApiKeyLocation(val location: String) { + QUERY("query"), + HEADER("header") +} +data class ApiKey(val value: String): Credential +data class ApiPrincipal(val apiKey: ApiKey?) : Principal +fun ApplicationCall.apiKey(key: String, keyLocation: ApiKeyLocation = ApiKeyLocation.valueOf("header")): ApiKey? = request.apiKey(key, keyLocation) +fun ApplicationRequest.apiKey(key: String, keyLocation: ApiKeyLocation = ApiKeyLocation.valueOf("header")): ApiKey? { + val value: String? = when(keyLocation) { + ApiKeyLocation.QUERY -> this.queryParameters[key] + ApiKeyLocation.HEADER -> this.headers[key] + } + when (value) { + null -> return null + else -> return ApiKey(value) + } +} + +fun AuthenticationPipeline.apiKeyAuth(apiKeyName: String, authLocation: String, validate: suspend (ApiKey) -> ApiPrincipal?) { + intercept(AuthenticationPipeline.RequestAuthentication) { context -> + val credentials = call.request.apiKey(apiKeyName, ApiKeyLocation.values().first { it.location == authLocation }) + val principal = credentials?.let { validate(it) } + + val cause = when { + credentials == null -> AuthenticationFailedCause.NoCredentials + principal == null -> AuthenticationFailedCause.InvalidCredentials + else -> null + } + + if (cause != null) { + context.challenge(apiKeyName, cause) { + // TODO: Verify correct response structure here. + call.respond(UnauthorizedResponse(HttpAuthHeader.Parameterized("API_KEY", mapOf("key" to apiKeyName), HeaderValueEncoding.QUOTED_ALWAYS))) + it.complete() + } + } + if (principal != null) { + context.principal(principal) + } + } +} + diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/ApiResponse.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/ApiResponse.kt new file mode 100644 index 00000000000..f7b993ae6dc --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/ApiResponse.kt @@ -0,0 +1,28 @@ +/** +* Swagger Petstore +* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. +* +* OpenAPI spec version: 1.0.0 +* Contact: apiteam@swagger.io +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ +package io.swagger.server.models + + +/** + * Describes the result of uploading an image resource + * @param code + * @param type + * @param message + */ +data class ApiResponse ( + val code: kotlin.Int? = null, + val type: kotlin.String? = null, + val message: kotlin.String? = null +) { + +} + diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Category.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Category.kt new file mode 100644 index 00000000000..55ce04095dc --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Category.kt @@ -0,0 +1,26 @@ +/** +* Swagger Petstore +* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. +* +* OpenAPI spec version: 1.0.0 +* Contact: apiteam@swagger.io +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ +package io.swagger.server.models + + +/** + * A category for a pet + * @param id + * @param name + */ +data class Category ( + val id: kotlin.Long? = null, + val name: kotlin.String? = null +) { + +} + diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Order.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Order.kt new file mode 100644 index 00000000000..2f1ba9ea5a0 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Order.kt @@ -0,0 +1,49 @@ +/** +* Swagger Petstore +* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. +* +* OpenAPI spec version: 1.0.0 +* Contact: apiteam@swagger.io +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ +package io.swagger.server.models + + +/** + * An order for a pets from the pet store + * @param id + * @param petId + * @param quantity + * @param shipDate + * @param status Order Status + * @param complete + */ +data class Order ( + val id: kotlin.Long? = null, + val petId: kotlin.Long? = null, + val quantity: kotlin.Int? = null, + val shipDate: java.time.LocalDateTime? = null, + /* Order Status */ + val status: Order.Status? = null, + val complete: kotlin.Boolean? = null +) { + + /** + * Order Status + * Values: placed,approved,delivered + */ + enum class Status(val value: kotlin.Any){ + + placed("placed"), + + approved("approved"), + + delivered("delivered"); + + } + +} + diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Pet.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Pet.kt new file mode 100644 index 00000000000..2c1ac268d1d --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Pet.kt @@ -0,0 +1,51 @@ +/** +* Swagger Petstore +* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. +* +* OpenAPI spec version: 1.0.0 +* Contact: apiteam@swagger.io +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ +package io.swagger.server.models + +import io.swagger.server.models.Category +import io.swagger.server.models.Tag + +/** + * A pet for sale in the pet store + * @param id + * @param category + * @param name + * @param photoUrls + * @param tags + * @param status pet status in the store + */ +data class Pet ( + val name: kotlin.String, + val photoUrls: kotlin.Array, + val id: kotlin.Long? = null, + val category: Category? = null, + val tags: kotlin.Array? = null, + /* pet status in the store */ + val status: Pet.Status? = null +) { + + /** + * pet status in the store + * Values: available,pending,sold + */ + enum class Status(val value: kotlin.Any){ + + available("available"), + + pending("pending"), + + sold("sold"); + + } + +} + diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Tag.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Tag.kt new file mode 100644 index 00000000000..387414b837f --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/Tag.kt @@ -0,0 +1,26 @@ +/** +* Swagger Petstore +* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. +* +* OpenAPI spec version: 1.0.0 +* Contact: apiteam@swagger.io +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ +package io.swagger.server.models + + +/** + * A tag for a pet + * @param id + * @param name + */ +data class Tag ( + val id: kotlin.Long? = null, + val name: kotlin.String? = null +) { + +} + diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/User.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/User.kt new file mode 100644 index 00000000000..07cf96822cb --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/io/swagger/server/models/User.kt @@ -0,0 +1,39 @@ +/** +* Swagger Petstore +* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. +* +* OpenAPI spec version: 1.0.0 +* Contact: apiteam@swagger.io +* +* NOTE: This class is auto generated by the swagger code generator program. +* https://github.com/swagger-api/swagger-codegen.git +* Do not edit the class manually. +*/ +package io.swagger.server.models + + +/** + * A User who is purchasing from the pet store + * @param id + * @param username + * @param firstName + * @param lastName + * @param email + * @param password + * @param phone + * @param userStatus User Status + */ +data class User ( + val id: kotlin.Long? = null, + val username: kotlin.String? = null, + val firstName: kotlin.String? = null, + val lastName: kotlin.String? = null, + val email: kotlin.String? = null, + val password: kotlin.String? = null, + val phone: kotlin.String? = null, + /* User Status */ + val userStatus: kotlin.Int? = null +) { + +} + diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/resources/application.conf b/samples/server/petstore/kotlin-server/ktor/src/main/resources/application.conf new file mode 100644 index 00000000000..f16145804ed --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/resources/application.conf @@ -0,0 +1,23 @@ +ktor { + deployment { + environment = development + port = 8080 + autoreload = true + watch = [ io.swagger.server ] + } + + application { + modules = [ io.swagger.server.AppMainKt.main ] + } +} + +# Typesafe config allows multiple ways to provide configuration values without hard-coding them here. +# Please see https://github.com/lightbend/config for details. +auth { + oauth { + petstore_auth { + clientId = "" + clientSecret = "" + } + } +} \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/resources/logback.xml b/samples/server/petstore/kotlin-server/ktor/src/main/resources/logback.xml new file mode 100644 index 00000000000..d0eaba8debd --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + +