diff --git a/.gitignore b/.gitignore index 2419a1981a..f8d6516c04 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ # BlueJ files *.ctxt +# Visual Studio Code +.project +.vscode + # Mobile Tools for Java (J2ME) .mtj.tmp/ @@ -22,4 +26,4 @@ hs_err_pid* .idea -target/ +target/ \ No newline at end of file diff --git a/src/main/java/io/swagger/codegen/languages/kotlin/AbstractKotlinCodegen.java b/src/main/java/io/swagger/codegen/languages/kotlin/AbstractKotlinCodegen.java new file mode 100644 index 0000000000..502cf0555c --- /dev/null +++ b/src/main/java/io/swagger/codegen/languages/kotlin/AbstractKotlinCodegen.java @@ -0,0 +1,587 @@ +package io.swagger.codegen.languages.kotlin; + +import io.swagger.codegen.CodegenModel; +import io.swagger.codegen.CliOption; +import io.swagger.codegen.languages.DefaultCodegenConfig; +import io.swagger.codegen.CodegenConstants; +import io.swagger.codegen.DefaultCodegen; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.MapSchema; +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; +import java.util.Set; + +public abstract class AbstractKotlinCodegen extends DefaultCodegenConfig { + + private static Logger LOGGER = LoggerFactory.getLogger(AbstractKotlinCodegen.class); + + private Set instantiationLibraryFunction; + + + + 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.Any", + "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" + )); + + instantiationLibraryFunction = new HashSet(Arrays.asList( + "arrayOf", + "mapOf" + )); + + 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 getArgumentsLocation() { + return ""; + } + + @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()); + } + } + + /** + * Output the type declaration of the property + * + * @param propertySchema Swagger Property object + * @return a string presentation of the property type + */ + @Override + public String getTypeDeclaration(Schema propertySchema) { + if (propertySchema instanceof ArraySchema) { + return getArrayTypeDeclaration((ArraySchema) propertySchema); + } else if (propertySchema instanceof MapSchema || propertySchema.getAdditionalProperties() != null) { + Schema inner = (Schema) propertySchema.getAdditionalProperties(); + if (inner == null) { + LOGGER.warn(propertySchema.getName() + "(map property) does not have a proper inner type defined"); + // TODO maybe better defaulting to StringProperty than returning null + return null; + } + // Maps will be keyed only by primitive Kotlin string + return String.format("%s", getSchemaType(propertySchema), getTypeDeclaration(inner)); + } + return super.getTypeDeclaration(propertySchema); + } + + @Override + public String getAlias(String name) { + if (typeAliases != null && typeAliases.containsKey(name)) { + return typeAliases.get(name); + } + return name; + } + + @Override + public String getSchemaType(Schema schema) { + String schemaType = super.getSchemaType(schema); + + // don't apply renaming on types from the typeMapping + if (typeMapping.containsKey(schemaType)) { + return toModelName(typeMapping.get(schemaType)); + } + + if (null == schemaType) { + if (schema.getName() != null) { + LOGGER.warn("No Type defined for Property " + schema.getName()); + return toModelName(schema.getName()); + } else { + return toModelName("kotlin.Any"); + } + } + return toModelName(schemaType); + } + + @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(Schema p) { + if (p instanceof ArraySchema) { + return getArrayTypeDeclaration((ArraySchema) 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); + } + + @Override + public String toVarName(String name) { + return super.toVarName(sanitizeKotlinSpecificNames(name)); + } + + /** + * Provides a strongly typed declaration for simple arrays of some type and arrays of arrays of some type. + * + * @param arr + * @return + */ + private String getArrayTypeDeclaration(ArraySchema arraySchema) { + // 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); + Schema items = arraySchema.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 = removeNonNameElementToCamelCase(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) && + !instantiationLibraryFunction.contains(type); + + return imports; + } +} diff --git a/src/main/java/io/swagger/codegen/languages/kotlin/KotlinClientCodegen.java b/src/main/java/io/swagger/codegen/languages/kotlin/KotlinClientCodegen.java new file mode 100644 index 0000000000..11a640a1ce --- /dev/null +++ b/src/main/java/io/swagger/codegen/languages/kotlin/KotlinClientCodegen.java @@ -0,0 +1,126 @@ +package io.swagger.codegen.languages.kotlin; + +import io.swagger.codegen.CliOption; +import io.swagger.codegen.CodegenConstants; +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.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +public class KotlinClientCodegen extends AbstractKotlinCodegen { + + public static final String DATE_LIBRARY = "dateLibrary"; + private static Logger LOGGER = LoggerFactory.getLogger(KotlinClientCodegen.class); + + protected String dateLibrary = DateLibrary.JAVA8.value; + + public enum DateLibrary { + STRING("string"), + THREETENBP("threetenbp"), + JAVA8("java8"); + + public final String value; + + DateLibrary(String value) { + this.value = value; + } + } + + /** + * 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"); + modelDocTemplateFiles.put("model_doc.mustache", ".md"); + apiDocTemplateFiles.put("api_doc.mustache", ".md"); + embeddedTemplateDir = templateDir = "kotlin-client"; + apiPackage = packageName + ".apis"; + modelPackage = packageName + ".models"; + + CliOption dateLibrary = new CliOption(DATE_LIBRARY, "Option. Date library to use"); + Map dateOptions = new HashMap<>(); + dateOptions.put(DateLibrary.THREETENBP.value, "Threetenbp"); + dateOptions.put(DateLibrary.STRING.value, "String"); + dateOptions.put(DateLibrary.JAVA8.value, "Java 8 native JSR310"); + dateLibrary.setEnum(dateOptions); + cliOptions.add(dateLibrary); + } + + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + public String getName() { + return "kotlin-client"; + } + + public String getHelp() { + return "Generates a kotlin client."; + } + + public void setDateLibrary(String library) { + this.dateLibrary = library; + } + + @Override + public void processOpts() { + super.processOpts(); + + String templateVersion = getTemplateVersion(); + if (StringUtils.isNotBlank(templateVersion)) { + embeddedTemplateDir = templateDir = String.format("%s/kotlin-client", templateVersion); + } else { + embeddedTemplateDir = templateDir = String.format("%s/kotlin-client", DEFAULT_TEMPLATE_VERSION); + } + + if (additionalProperties.containsKey(DATE_LIBRARY)) { + setDateLibrary(additionalProperties.get(DATE_LIBRARY).toString()); + } + + if (DateLibrary.THREETENBP.value.equals(dateLibrary)) { + additionalProperties.put(DateLibrary.THREETENBP.value, true); + typeMapping.put("date", "LocalDate"); + typeMapping.put("DateTime", "LocalDateTime"); + importMapping.put("LocalDate", "org.threeten.bp.LocalDate"); + importMapping.put("LocalDateTime", "org.threeten.bp.LocalDateTime"); + defaultIncludes.add("org.threeten.bp.LocalDateTime"); + } else if (DateLibrary.STRING.value.equals(dateLibrary)) { + typeMapping.put("date-time", "kotlin.String"); + typeMapping.put("date", "kotlin.String"); + typeMapping.put("Date", "kotlin.String"); + typeMapping.put("DateTime", "kotlin.String"); + } else if (DateLibrary.JAVA8.value.equals(dateLibrary)) { + additionalProperties.put(DateLibrary.JAVA8.value, true); + } + + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + + supportingFiles.add(new SupportingFile("build.gradle.mustache", "", "build.gradle")); + supportingFiles.add(new SupportingFile("settings.gradle.mustache", "", "settings.gradle")); + + final String infrastructureFolder = (sourceFolder + File.separator + packageName + File.separator + "infrastructure").replace(".", "/"); + + supportingFiles.add(new SupportingFile("infrastructure/ApiClient.kt.mustache", infrastructureFolder, "ApiClient.kt")); + supportingFiles.add(new SupportingFile("infrastructure/ApiAbstractions.kt.mustache", infrastructureFolder, "ApiAbstractions.kt")); + supportingFiles.add(new SupportingFile("infrastructure/ApiInfrastructureResponse.kt.mustache", infrastructureFolder, "ApiInfrastructureResponse.kt")); + supportingFiles.add(new SupportingFile("infrastructure/ApplicationDelegates.kt.mustache", infrastructureFolder, "ApplicationDelegates.kt")); + supportingFiles.add(new SupportingFile("infrastructure/RequestConfig.kt.mustache", infrastructureFolder, "RequestConfig.kt")); + supportingFiles.add(new SupportingFile("infrastructure/RequestMethod.kt.mustache", infrastructureFolder, "RequestMethod.kt")); + supportingFiles.add(new SupportingFile("infrastructure/ResponseExtensions.kt.mustache", infrastructureFolder, "ResponseExtensions.kt")); + supportingFiles.add(new SupportingFile("infrastructure/Serializer.kt.mustache", infrastructureFolder, "Serializer.kt")); + supportingFiles.add(new SupportingFile("infrastructure/Errors.kt.mustache", infrastructureFolder, "Errors.kt")); + } +} diff --git a/src/main/java/io/swagger/codegen/languages/kotlin/KotlinServerCodegen.java b/src/main/java/io/swagger/codegen/languages/kotlin/KotlinServerCodegen.java new file mode 100644 index 0000000000..40f3840858 --- /dev/null +++ b/src/main/java/io/swagger/codegen/languages/kotlin/KotlinServerCodegen.java @@ -0,0 +1,259 @@ +package io.swagger.codegen.languages.kotlin; + +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.parameters.Parameter; + +import io.swagger.codegen.CliOption; +import io.swagger.codegen.CodegenConstants; +import io.swagger.codegen.CodegenType; +import io.swagger.codegen.CodegenModel; +import io.swagger.codegen.CodegenProperty; +import io.swagger.codegen.CodegenOperation; +import io.swagger.codegen.SupportingFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import com.github.jknack.handlebars.helper.StringHelpers; +import com.github.jknack.handlebars.Handlebars; +import org.apache.commons.lang3.StringUtils; + +import static java.util.Collections.singletonMap; + +public class KotlinServerCodegen extends AbstractKotlinCodegen { + + public static final String DEFAULT_LIBRARY = Constants.KTOR; + public static final String GENERATE_APIS = "generateApis"; + + private 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 = + singletonMap(Constants.KTOR, + Arrays.asList( + Constants.AUTOMATIC_HEAD_REQUESTS, + Constants.CONDITIONAL_HEADERS, + Constants.HSTS, + Constants.CORS, + Constants.COMPRESSION + )); + + /** + * 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; + } + + /** + * Handle typealias for schema of Array type + */ + @Override + public CodegenModel fromModel(String name, Schema schema, Map allDefinitions) { + CodegenModel codegenModel = super.fromModel(name, schema, allDefinitions); + + if (schema instanceof ArraySchema) { + codegenModel.dataType = getTypeDeclaration(schema); + } + return codegenModel; + } + + @Override + public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, Map schemas, OpenAPI openAPI) { + // Ensure that the parameter names in the path are valid kotlin names + // they need to match the names in the generated data class, this is required by ktor Location + String modifiedPath = path; + if (operation.getParameters() != null) { + for (Parameter param : operation.getParameters()) { + String pathParamName = param.getName(); + String kotlinName = toVarName(pathParamName); + modifiedPath = modifiedPath.replace("{" + pathParamName + "}", "{" + kotlinName + "}"); + } + } + + return super.fromOperation(modifiedPath, httpMethod, operation, schemas, openAPI); + } + + @Override + public void processOpts() { + super.processOpts(); + + String templateVersion = getTemplateVersion(); + if (StringUtils.isNotBlank(templateVersion)) { + embeddedTemplateDir = templateDir = String.format("%s/kotlin-server", templateVersion); + } else { + embeddedTemplateDir = templateDir = String.format("%s/kotlin-server", DEFAULT_TEMPLATE_VERSION); + } + + if (!additionalProperties.containsKey(GENERATE_APIS)) { + additionalProperties.put(GENERATE_APIS, true); + } + + if (additionalProperties.containsKey(CodegenConstants.LIBRARY)) { + this.setLibrary((String) additionalProperties.get(CodegenConstants.LIBRARY)); + } else { + this.setLibrary(DEFAULT_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()); + } + + String packageFolder = (sourceFolder + File.separator + packageName).replace(".", File.separator); + String resourcesFolder = "src/main/resources"; // not sure this can be user configurable. + Boolean generateApis = additionalProperties.containsKey(GENERATE_APIS) && (Boolean)additionalProperties.get(GENERATE_APIS); + + 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")); + } + + @Override + public void addHandlebarHelpers(Handlebars handlebars) { + super.addHandlebarHelpers(handlebars); + handlebars.registerHelpers(StringHelpers.class); + } + + 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/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig b/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig index 9f2ad3ad43..ae01f7c842 100644 --- a/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig +++ b/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig @@ -9,4 +9,6 @@ io.swagger.codegen.languages.java.JavaJAXRSCXFCDIServerCodegen io.swagger.codegen.languages.java.JavaJAXRSSpecServerCodegen io.swagger.codegen.languages.java.JavaJerseyServerCodegen io.swagger.codegen.languages.java.JavaResteasyEapServerCodegen -io.swagger.codegen.languages.java.JavaResteasyServerCodegen \ No newline at end of file +io.swagger.codegen.languages.java.JavaResteasyServerCodegen +io.swagger.codegen.languages.kotlin.KotlinClientCodegen +io.swagger.codegen.languages.kotlin.KotlinServerCodegen diff --git a/src/main/resources/v2/kotlin-client/README.mustache b/src/main/resources/v2/kotlin-client/README.mustache new file mode 100644 index 0000000000..90b6f3e2db --- /dev/null +++ b/src/main/resources/v2/kotlin-client/README.mustache @@ -0,0 +1,85 @@ +# {{packageName}} - Kotlin client 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. +* Implementation of ApiClient is intended to reduce method counts, specifically to benefit Android targets. + +{{#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}} + +{{#is this 'apiKey'}}- **Type**: API key +- **API key parameter name**: {{keyParamName}} +- **Location**: {{#is this 'keyInQuery'}}URL query string{{/is}}{{#is this 'keyInHeader'}}HTTP header{{/is}} +{{/is}} +{{#is this 'basic'}}- **Type**: HTTP basic authentication +{{/is}} +{{#is this 'oauth'}}- **Type**: OAuth +- **Flow**: {{flow}} +- **Authorization URL**: {{authorizationUrl}} +- **Scopes**: {{^scopes}}N/A{{/scopes}} +{{#scopes}} - {{scope}}: {{description}} +{{/scopes}} +{{/is}} + +{{/authMethods}} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/api.mustache b/src/main/resources/v2/kotlin-client/api.mustache new file mode 100644 index 0000000000..4c2d0c3a67 --- /dev/null +++ b/src/main/resources/v2/kotlin-client/api.mustache @@ -0,0 +1,50 @@ +{{>licenseInfo}} +package {{apiPackage}} + +{{#imports}}import {{import}} +{{/imports}} + +import {{packageName}}.infrastructure.* +{{#threetenbp}} +import org.threeten.bp.LocalDateTime +{{/threetenbp}} + +{{#operations}} +class {{classname}}(basePath: kotlin.String = "{{{basePath}}}") : ApiClient(basePath) { + + {{#operation}} + /** + * {{summary}} + * {{notes}} + {{#allParams}}* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}} + {{/allParams}}* @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} + */{{#returnType}} + @Suppress("UNCHECKED_CAST"){{/returnType}} + fun {{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}{{#has this 'more'}}, {{/has}}{{/allParams}}) : {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}} { + val localVariableBody: kotlin.Any? = {{#hasBodyParam}}{{#bodyParams}}{{paramName}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}null{{/hasFormParams}}{{#hasFormParams}}mapOf({{#formParams}}"{{{baseName}}}" to "${{paramName}}"{{#has this 'more'}}, {{/has}}{{/formParams}}){{/hasFormParams}}{{/hasBodyParam}} + val localVariableQuery: MultiValueMap = {{^hasQueryParams}}mapOf(){{/hasQueryParams}}{{#hasQueryParams}}mapOf({{#queryParams}}"{{baseName}}" to {{#is this 'container'}}toMultiValue({{paramName}}.toList(), "{{collectionFormat}}"){{/is}}{{#isNot this 'container'}}listOf("${{paramName}}"){{/isNot}}{{#has this 'more'}}, {{/has}}{{/queryParams}}){{/hasQueryParams}} + val localVariableHeaders: kotlin.collections.Map = mapOf({{#hasFormParams}}"Content-Type" to "multipart/form-data"{{/hasFormParams}}{{^hasHeaderParams}}){{/hasHeaderParams}}{{#hasHeaderParams}}{{#hasFormParams}}, {{/hasFormParams}}{{#headerParams}}"{{baseName}}" to {{#is this 'container'}}{{paramName}}.joinToString(separator = collectionDelimiter("{{collectionFormat}}"){{/is}}{{#isNot this 'container'}}{{paramName}}{{/isNot}}{{#has this 'more'}}, {{/has}}{{/headerParams}}){{/hasHeaderParams}} + val localVariableConfig = RequestConfig( + RequestMethod.{{httpMethod}}, + "{{path}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", "${{paramName}}"){{/pathParams}}, + query = localVariableQuery, + headers = localVariableHeaders + ) + val response = request<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Any?{{/returnType}}>( + localVariableConfig, + localVariableBody + ) + + return when (response.responseType) { + ResponseType.Success -> {{#returnType}}(response as Success<*>).data as {{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}} + ResponseType.Informational -> TODO() + ResponseType.Redirection -> TODO() + ResponseType.ClientError -> throw ClientException((response as ClientError<*>).body as? String ?: "Client error") + ResponseType.ServerError -> throw ServerException((response as ServerError<*>).message ?: "Server error") + else -> throw kotlin.IllegalStateException("Undefined ResponseType.") + } + } + + {{/operation}} +} +{{/operations}} diff --git a/src/main/resources/v2/kotlin-client/api_doc.mustache b/src/main/resources/v2/kotlin-client/api_doc.mustache new file mode 100644 index 0000000000..b8a85201c5 --- /dev/null +++ b/src/main/resources/v2/kotlin-client/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}}}{{#has this 'more'}}, {{/has}}{{/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}}}{{#has this 'more'}}, {{/has}}{{/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}}** | {{#is this 'primitiveType'}}}**{{dataType}}**{{/is}}}{{#isNot this 'primitiveType'}}{{#is this 'file'}}**{{dataType}}**{{/is}}{{#isNot this 'file'}}{{#generateModelDocs}}[**{{dataType}}**]({{baseType}}.md){{/generateModelDocs}}{{^generateModelDocs}}**{{dataType}}**{{/generateModelDocs}}{{/isNot}}{{/isNot}}}| {{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}}}{{#has this 'more'}}, {{/has}}{{/consumes}}{{^consumes}}Not defined{{/consumes}} + - **Accept**: {{#produces}}{{{mediaType}}}{{#has this 'more'}}, {{/has}}{{/produces}}{{^produces}}Not defined{{/produces}} + +{{/operation}} +{{/operations}} diff --git a/src/main/resources/v2/kotlin-client/build.gradle.mustache b/src/main/resources/v2/kotlin-client/build.gradle.mustache new file mode 100644 index 0000000000..7d02942c6d --- /dev/null +++ b/src/main/resources/v2/kotlin-client/build.gradle.mustache @@ -0,0 +1,33 @@ +group '{{groupId}}' +version '{{artifactVersion}}' + +task wrapper(type: Wrapper) { + gradleVersion = '3.3' + distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip" +} + +buildscript { + ext.kotlin_version = '1.1.2' + + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'kotlin' + +repositories { + mavenCentral() +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "com.squareup.moshi:moshi-kotlin:1.5.0" + compile "com.squareup.moshi:moshi-adapters:1.5.0" + compile "com.squareup.okhttp3:okhttp:3.8.0" + compile "org.threeten:threetenbp:1.3.6" + testCompile "io.kotlintest:kotlintest:2.0.2" +} diff --git a/src/main/resources/v2/kotlin-client/class_doc.mustache b/src/main/resources/v2/kotlin-client/class_doc.mustache new file mode 100644 index 0000000000..e4c65efbea --- /dev/null +++ b/src/main/resources/v2/kotlin-client/class_doc.mustache @@ -0,0 +1,15 @@ +# {{classname}} + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +{{#vars}}**{{name}}** | {{#is this 'enum'}}[**inline**](#{{datatypeWithEnum}}){{/is}}{{#isNot this 'enum'}}{{#is this 'primitiveType'}}}**{{datatype}}**{{/is}}}{{#isNot this 'primitiveType'}}[**{{datatype}}**]({{complexType}}.md){{/is}}}{{/is}} | {{description}} | {{^required}} [optional]{{/required}}{{#readOnly}} [readonly]{{/readOnly}} +{{/vars}} +{{#vars}}{{#is this 'enum'}} + +{{!NOTE: see java's resources "pojo_doc.mustache" once enums are fully implemented}} +## Enum: {{baseName}} +Name | Value +---- | -----{{#allowableValues}} +{{name}} | {{#values}}{{.}}{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}} +{{/is}}{{/vars}} diff --git a/src/main/resources/v2/kotlin-client/data_class.mustache b/src/main/resources/v2/kotlin-client/data_class.mustache new file mode 100644 index 0000000000..c7d1897367 --- /dev/null +++ b/src/main/resources/v2/kotlin-client/data_class.mustache @@ -0,0 +1,25 @@ +/** + * {{{description}}} +{{~#vars}} + * @param {{name}} {{{description}}} +{{~/vars~}} + */ +data class {{classname}} ( +{{~#requiredVars~}} + {{>data_class_req_var}}{{^@last}},{{/@last}} +{{~/requiredVars~}} +{{#has this 'required'}}{{#has this 'optional'}},{{/has}}{{/has}} +{{~#optionalVars}}{{>data_class_opt_var}}{{^@last}},{{/@last}}{{/optionalVars}} +) { +{{#has this 'enums'}}{{#vars}}{{#is this 'enum'}} + /** + * {{{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}} + } +{{/is}}{{/vars}}{{/has}} +} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/data_class_opt_var.mustache b/src/main/resources/v2/kotlin-client/data_class_opt_var.mustache new file mode 100644 index 0000000000..2d950f5bab --- /dev/null +++ b/src/main/resources/v2/kotlin-client/data_class_opt_var.mustache @@ -0,0 +1,4 @@ +{{#description}} + /* {{{description}}} */ +{{~/description}} + val {{{name}}}: {{#is this 'enum'}}{{classname}}.{{nameInCamelCase}}{{/is}}{{#isNot this 'enum'}}{{{datatype}}}{{/isNot}}? = {{#defaultvalue}}{{defaultvalue}}{{/defaultvalue}}{{^defaultvalue}}null{{/defaultvalue}} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/data_class_req_var.mustache b/src/main/resources/v2/kotlin-client/data_class_req_var.mustache new file mode 100644 index 0000000000..659ceefc24 --- /dev/null +++ b/src/main/resources/v2/kotlin-client/data_class_req_var.mustache @@ -0,0 +1,4 @@ +{{#description}} + /* {{{description}}} */ +{{/description}} + val {{{name}}}: {{#is this 'enum'}}{{classname}}.{{nameInCamelCase}}{{/is}}{{#isNot this 'enum'}}{{{datatype}}}{{/isNot}} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/enum_class.mustache b/src/main/resources/v2/kotlin-client/enum_class.mustache new file mode 100644 index 0000000000..61324894fd --- /dev/null +++ b/src/main/resources/v2/kotlin-client/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/src/main/resources/v2/kotlin-client/enum_doc.mustache b/src/main/resources/v2/kotlin-client/enum_doc.mustache new file mode 100644 index 0000000000..fcb3d7e61a --- /dev/null +++ b/src/main/resources/v2/kotlin-client/enum_doc.mustache @@ -0,0 +1,7 @@ +# {{classname}} + +## Enum + +{{#allowableValues}}{{#enumVars}} + * `{{name}}` (value: `{{{value}}}`) +{{/enumVars}}{{/allowableValues}} diff --git a/src/main/resources/v2/kotlin-client/infrastructure/ApiAbstractions.kt.mustache b/src/main/resources/v2/kotlin-client/infrastructure/ApiAbstractions.kt.mustache new file mode 100644 index 0000000000..0a42ce534d --- /dev/null +++ b/src/main/resources/v2/kotlin-client/infrastructure/ApiAbstractions.kt.mustache @@ -0,0 +1,20 @@ +package {{packageName}}.infrastructure + +typealias MultiValueMap = Map> + +fun collectionDelimiter(collectionFormat: String) = when(collectionFormat) { + "csv" -> "," + "tsv" -> "\t" + "pipes" -> "|" + "ssv" -> " " + else -> "" +} + +val defaultMultiValueConverter: (item: Any?) -> String = { item -> "$item" } + +fun toMultiValue(items: List, collectionFormat: String, map: (item: Any?) -> String = defaultMultiValueConverter): List { + return when(collectionFormat) { + "multi" -> items.map(map) + else -> listOf(items.map(map).joinToString(separator = collectionDelimiter(collectionFormat))) + } +} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/infrastructure/ApiClient.kt.mustache b/src/main/resources/v2/kotlin-client/infrastructure/ApiClient.kt.mustache new file mode 100644 index 0000000000..e46256a356 --- /dev/null +++ b/src/main/resources/v2/kotlin-client/infrastructure/ApiClient.kt.mustache @@ -0,0 +1,128 @@ +package {{packageName}}.infrastructure + +import okhttp3.* +import java.io.File + +open class ApiClient(val baseUrl: String) { + companion object { + protected val ContentType = "Content-Type" + protected val Accept = "Accept" + protected val JsonMediaType = "application/json" + protected val FormDataMediaType = "multipart/form-data" + protected val XmlMediaType = "application/xml" + + @JvmStatic + val client : OkHttpClient = OkHttpClient() + + @JvmStatic + var defaultHeaders: Map by ApplicationDelegates.setOnce(mapOf(ContentType to JsonMediaType, Accept to JsonMediaType)) + + @JvmStatic + val jsonHeaders: Map = mapOf(ContentType to JsonMediaType, Accept to JsonMediaType) + } + + inline protected fun requestBody(content: T, mediaType: String = JsonMediaType): RequestBody { + if(content is File) { + return RequestBody.create( + MediaType.parse(mediaType), content + ) + } else if(mediaType == FormDataMediaType) { + var builder = FormBody.Builder() + // content's type *must* be Map + @Suppress("UNCHECKED_CAST") + (content as Map).forEach { key, value -> + builder = builder.add(key, value) + } + return builder.build() + } else if(mediaType == JsonMediaType) { + return RequestBody.create( + MediaType.parse(mediaType), Serializer.moshi.adapter(T::class.java).toJson(content) + ) + } else if (mediaType == XmlMediaType) { + TODO("xml not currently supported.") + } + + // TODO: this should be extended with other serializers + TODO("requestBody currently only supports JSON body and File body.") + } + + inline protected fun responseBody(body: ResponseBody?, mediaType: String = JsonMediaType): T? { + if(body == null) return null + return when(mediaType) { + JsonMediaType -> Serializer.moshi.adapter(T::class.java).fromJson(body.source()) + else -> TODO() + } + } + + inline protected fun request(requestConfig: RequestConfig, body : Any? = null): ApiInfrastructureResponse { + val httpUrl = HttpUrl.parse(baseUrl) ?: throw IllegalStateException("baseUrl is invalid.") + + var urlBuilder = httpUrl.newBuilder() + .addPathSegments(requestConfig.path.trimStart('/')) + + requestConfig.query.forEach { query -> + query.value.forEach { queryValue -> + urlBuilder = urlBuilder.addQueryParameter(query.key, queryValue) + } + } + + val url = urlBuilder.build() + val headers = requestConfig.headers + defaultHeaders + + if(headers[ContentType] ?: "" == "") { + throw kotlin.IllegalStateException("Missing Content-Type header. This is required.") + } + + if(headers[Accept] ?: "" == "") { + throw kotlin.IllegalStateException("Missing Accept header. This is required.") + } + + // TODO: support multiple contentType,accept options here. + val contentType = (headers[ContentType] as String).substringBefore(";").toLowerCase() + val accept = (headers[Accept] as String).substringBefore(";").toLowerCase() + + var request : Request.Builder = when (requestConfig.method) { + RequestMethod.DELETE -> Request.Builder().url(url).delete() + RequestMethod.GET -> Request.Builder().url(url) + RequestMethod.HEAD -> Request.Builder().url(url).head() + RequestMethod.PATCH -> Request.Builder().url(url).patch(requestBody(body, contentType)) + RequestMethod.PUT -> Request.Builder().url(url).put(requestBody(body, contentType)) + RequestMethod.POST -> Request.Builder().url(url).post(requestBody(body, contentType)) + RequestMethod.OPTIONS -> Request.Builder().url(url).method("OPTIONS", null) + } + + headers.forEach { header -> request = request.addHeader(header.key, header.value) } + + val realRequest = request.build() + val response = client.newCall(realRequest).execute() + + // TODO: handle specific mapping types. e.g. Map> + when { + response.isRedirect -> return Redirection( + response.code(), + response.headers().toMultimap() + ) + response.isInformational -> return Informational( + response.message(), + response.code(), + response.headers().toMultimap() + ) + response.isSuccessful -> return Success( + responseBody(response.body(), accept), + response.code(), + response.headers().toMultimap() + ) + response.isClientError -> return ClientError( + response.body()?.string(), + response.code(), + response.headers().toMultimap() + ) + else -> return ServerError( + null, + response.body()?.string(), + response.code(), + response.headers().toMultimap() + ) + } + } +} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/infrastructure/ApiInfrastructureResponse.kt.mustache b/src/main/resources/v2/kotlin-client/infrastructure/ApiInfrastructureResponse.kt.mustache new file mode 100644 index 0000000000..a4e0f0f424 --- /dev/null +++ b/src/main/resources/v2/kotlin-client/infrastructure/ApiInfrastructureResponse.kt.mustache @@ -0,0 +1,40 @@ +package {{packageName}}.infrastructure + +enum class ResponseType { + Success, Informational, Redirection, ClientError, ServerError +} + +abstract class ApiInfrastructureResponse(val responseType: ResponseType) { + abstract val statusCode: Int + abstract val headers: Map> +} + +class Success( + val data: T, + override val statusCode: Int = -1, + override val headers: Map> = mapOf() +): ApiInfrastructureResponse(ResponseType.Success) + +class Informational( + val statusText: String, + override val statusCode: Int = -1, + override val headers: Map> = mapOf() +) : ApiInfrastructureResponse(ResponseType.Informational) + +class Redirection( + override val statusCode: Int = -1, + override val headers: Map> = mapOf() +) : ApiInfrastructureResponse(ResponseType.Redirection) + +class ClientError( + val body: Any? = null, + override val statusCode: Int = -1, + override val headers: Map> = mapOf() +) : ApiInfrastructureResponse(ResponseType.ClientError) + +class ServerError( + val message: String? = null, + val body: Any? = null, + override val statusCode: Int = -1, + override val headers: Map> +): ApiInfrastructureResponse(ResponseType.ServerError) \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/infrastructure/ApplicationDelegates.kt.mustache b/src/main/resources/v2/kotlin-client/infrastructure/ApplicationDelegates.kt.mustache new file mode 100644 index 0000000000..bc4e1cd6a1 --- /dev/null +++ b/src/main/resources/v2/kotlin-client/infrastructure/ApplicationDelegates.kt.mustache @@ -0,0 +1,29 @@ +package {{packageName}}.infrastructure + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +object ApplicationDelegates { + /** + * Provides a property delegate, allowing the property to be set once and only once. + * + * If unset (no default value), a get on the property will throw [IllegalStateException]. + */ + fun setOnce(defaultValue: T? = null) : ReadWriteProperty = SetOnce(defaultValue) + + private class SetOnce(defaultValue: T? = null) : ReadWriteProperty { + private var isSet = false + private var value: T? = defaultValue + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return value ?: throw IllegalStateException("${property.name} not initialized") + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = synchronized(this) { + if (!isSet) { + this.value = value + isSet = true + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/infrastructure/Errors.kt.mustache b/src/main/resources/v2/kotlin-client/infrastructure/Errors.kt.mustache new file mode 100644 index 0000000000..1f6d106d94 --- /dev/null +++ b/src/main/resources/v2/kotlin-client/infrastructure/Errors.kt.mustache @@ -0,0 +1,42 @@ +@file:Suppress("unused") +package {{packageName}}.infrastructure + +import java.lang.RuntimeException + +open class ClientException : RuntimeException { + + /** + * Constructs an [ClientException] with no detail message. + */ + constructor() : super() + + /** + * Constructs an [ClientException] with the specified detail message. + + * @param message the detail message. + */ + constructor(message: kotlin.String) : super(message) + + companion object { + private const val serialVersionUID: Long = 123L + } +} + +open class ServerException : RuntimeException { + + /** + * Constructs an [ServerException] with no detail message. + */ + constructor() : super() + + /** + * Constructs an [ServerException] with the specified detail message. + + * @param message the detail message. + */ + constructor(message: kotlin.String) : super(message) + + companion object { + private const val serialVersionUID: Long = 456L + } +} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/infrastructure/RequestConfig.kt.mustache b/src/main/resources/v2/kotlin-client/infrastructure/RequestConfig.kt.mustache new file mode 100644 index 0000000000..58a3d605aa --- /dev/null +++ b/src/main/resources/v2/kotlin-client/infrastructure/RequestConfig.kt.mustache @@ -0,0 +1,15 @@ +package {{packageName}}.infrastructure + +/** + * Defines a config object for a given request. + * NOTE: This object doesn't include 'body' because it + * allows for caching of the constructed object + * for many request definitions. + * NOTE: Headers is a Map because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class RequestConfig( + val method: RequestMethod, + val path: String, + val headers: Map = mapOf(), + val query: Map> = mapOf()) \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/infrastructure/RequestMethod.kt.mustache b/src/main/resources/v2/kotlin-client/infrastructure/RequestMethod.kt.mustache new file mode 100644 index 0000000000..5774235985 --- /dev/null +++ b/src/main/resources/v2/kotlin-client/infrastructure/RequestMethod.kt.mustache @@ -0,0 +1,8 @@ +package {{packageName}}.infrastructure + +/** + * Provides enumerated HTTP verbs + */ +enum class RequestMethod { + GET, DELETE, HEAD, OPTIONS, PATCH, POST, PUT +} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/infrastructure/ResponseExtensions.kt.mustache b/src/main/resources/v2/kotlin-client/infrastructure/ResponseExtensions.kt.mustache new file mode 100644 index 0000000000..2e2a3478ff --- /dev/null +++ b/src/main/resources/v2/kotlin-client/infrastructure/ResponseExtensions.kt.mustache @@ -0,0 +1,23 @@ +package {{packageName}}.infrastructure + +import okhttp3.Response + +/** + * Provides an extension to evaluation whether the response is a 1xx code + */ +val Response.isInformational : Boolean get() = this.code() in 100..199 + +/** + * Provides an extension to evaluation whether the response is a 3xx code + */ +val Response.isRedirect : Boolean get() = this.code() in 300..399 + +/** + * Provides an extension to evaluation whether the response is a 4xx code + */ +val Response.isClientError : Boolean get() = this.code() in 400..499 + +/** + * Provides an extension to evaluation whether the response is a 5xx (Standard) through 999 (non-standard) code + */ +val Response.isServerError : Boolean get() = this.code() in 500..999 \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-client/infrastructure/Serializer.kt.mustache b/src/main/resources/v2/kotlin-client/infrastructure/Serializer.kt.mustache new file mode 100644 index 0000000000..71cb991ddb --- /dev/null +++ b/src/main/resources/v2/kotlin-client/infrastructure/Serializer.kt.mustache @@ -0,0 +1,14 @@ +package {{packageName}}.infrastructure + +import com.squareup.moshi.KotlinJsonAdapterFactory +import com.squareup.moshi.Moshi +import com.squareup.moshi.Rfc3339DateJsonAdapter +import java.util.* + +object Serializer { + @JvmStatic + val moshi: Moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .add(Date::class.java, Rfc3339DateJsonAdapter().nullSafe()) + .build() +} diff --git a/src/main/resources/v2/kotlin-client/licenseInfo.mustache b/src/main/resources/v2/kotlin-client/licenseInfo.mustache new file mode 100644 index 0000000000..aee680977d --- /dev/null +++ b/src/main/resources/v2/kotlin-client/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/src/main/resources/v2/kotlin-client/model.mustache b/src/main/resources/v2/kotlin-client/model.mustache new file mode 100644 index 0000000000..852d62cdbd --- /dev/null +++ b/src/main/resources/v2/kotlin-client/model.mustache @@ -0,0 +1,14 @@ +{{>licenseInfo}} +package {{modelPackage}} + +{{#imports}}import {{import}} +{{/imports}} +{{#threetenbp}} +import org.threeten.bp.LocalDateTime +{{/threetenbp}} + +{{#models}} +{{#model}} +{{#is this 'alias'}}typealias {{classname}} = {{dataType}}{{/is}}{{#isNot this 'alias'}}{{#is this 'enum'}}{{>enum_class}}{{/is}}{{#isNot this 'enum'}}{{>data_class}}{{/isNot}}{{/isNot}} +{{/model}} +{{/models}} diff --git a/src/main/resources/v2/kotlin-client/model_doc.mustache b/src/main/resources/v2/kotlin-client/model_doc.mustache new file mode 100644 index 0000000000..e11fac0390 --- /dev/null +++ b/src/main/resources/v2/kotlin-client/model_doc.mustache @@ -0,0 +1,3 @@ +{{#models}}{{#model}} +{{#is this 'enum'}}{{>enum_doc}}{{/is}}{{#isNot this 'enum'}}{{>class_doc}}{{/isNot}} +{{/model}}{{/models}} diff --git a/src/main/resources/v2/kotlin-client/settings.gradle.mustache b/src/main/resources/v2/kotlin-client/settings.gradle.mustache new file mode 100644 index 0000000000..448dc07602 --- /dev/null +++ b/src/main/resources/v2/kotlin-client/settings.gradle.mustache @@ -0,0 +1 @@ +rootProject.name = '{{artifactId}}' \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-server/README.mustache b/src/main/resources/v2/kotlin-server/README.mustache new file mode 100644 index 0000000000..de584f694f --- /dev/null +++ b/src/main/resources/v2/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}} + +{{#is this 'apiKey'}}- **Type**: API key +- **API key parameter name**: {{keyParamName}} +- **Location**: {{#is this 'keyInQuery'}}URL query string{{/is}}{{#is this 'keyInHeader'}}HTTP header{{/is}} +{{/is}} +{{#is this 'basic'}}- **Type**: HTTP basic authentication +{{/is}} +{{#is this 'oauth'}}- **Type**: OAuth +- **Flow**: {{flow}} +- **Authorization URL**: {{authorizationUrl}} +- **Scopes**: {{^scopes}}N/A{{/scopes}} +{{#scopes}} - {{scope}}: {{description}} +{{/scopes}} +{{/is}} + +{{/authMethods}} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-server/api_doc.mustache b/src/main/resources/v2/kotlin-server/api_doc.mustache new file mode 100644 index 0000000000..b8a85201c5 --- /dev/null +++ b/src/main/resources/v2/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}}}{{#has this 'more'}}, {{/has}}{{/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}}}{{#has this 'more'}}, {{/has}}{{/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}}** | {{#is this 'primitiveType'}}}**{{dataType}}**{{/is}}}{{#isNot this 'primitiveType'}}{{#is this 'file'}}**{{dataType}}**{{/is}}{{#isNot this 'file'}}{{#generateModelDocs}}[**{{dataType}}**]({{baseType}}.md){{/generateModelDocs}}{{^generateModelDocs}}**{{dataType}}**{{/generateModelDocs}}{{/isNot}}{{/isNot}}}| {{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}}}{{#has this 'more'}}, {{/has}}{{/consumes}}{{^consumes}}Not defined{{/consumes}} + - **Accept**: {{#produces}}{{{mediaType}}}{{#has this 'more'}}, {{/has}}{{/produces}}{{^produces}}Not defined{{/produces}} + +{{/operation}} +{{/operations}} diff --git a/src/main/resources/v2/kotlin-server/class_doc.mustache b/src/main/resources/v2/kotlin-server/class_doc.mustache new file mode 100644 index 0000000000..18812e09f9 --- /dev/null +++ b/src/main/resources/v2/kotlin-server/class_doc.mustache @@ -0,0 +1,15 @@ +# {{classname}} + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +{{#vars}}**{{name}}** | {{#is this 'enum'}}[**inline**](#{{datatypeWithEnum}}){{/is}}{{#isNot this 'enum'}}{{#is this 'primitiveType'}}}**{{datatype}}**{{/is}}}{{#isNot this 'primitiveType'}}[**{{datatype}}**]({{complexType}}.md){{/isNot}}}{{/isNot}} | {{description}} | {{^required}} [optional]{{/required}}{{#readOnly}} [readonly]{{/readOnly}} +{{/vars}} +{{#vars}}{{#is this 'enum'}} + +{{!NOTE: see java's resources "pojo_doc.mustache" once enums are fully implemented}} +## Enum: {{baseName}} +Name | Value +---- | -----{{#allowableValues}} +{{name}} | {{#values}}{{.}}{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}} +{{/is}}{{/vars}} diff --git a/src/main/resources/v2/kotlin-server/data_class.mustache b/src/main/resources/v2/kotlin-server/data_class.mustache new file mode 100644 index 0000000000..c7d1897367 --- /dev/null +++ b/src/main/resources/v2/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~}} +{{#has this 'required'}}{{#has this 'optional'}},{{/has}}{{/has}} +{{~#optionalVars}}{{>data_class_opt_var}}{{^@last}},{{/@last}}{{/optionalVars}} +) { +{{#has this 'enums'}}{{#vars}}{{#is this 'enum'}} + /** + * {{{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}} + } +{{/is}}{{/vars}}{{/has}} +} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-server/data_class_opt_var.mustache b/src/main/resources/v2/kotlin-server/data_class_opt_var.mustache new file mode 100644 index 0000000000..2d950f5bab --- /dev/null +++ b/src/main/resources/v2/kotlin-server/data_class_opt_var.mustache @@ -0,0 +1,4 @@ +{{#description}} + /* {{{description}}} */ +{{~/description}} + val {{{name}}}: {{#is this 'enum'}}{{classname}}.{{nameInCamelCase}}{{/is}}{{#isNot this 'enum'}}{{{datatype}}}{{/isNot}}? = {{#defaultvalue}}{{defaultvalue}}{{/defaultvalue}}{{^defaultvalue}}null{{/defaultvalue}} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-server/data_class_req_var.mustache b/src/main/resources/v2/kotlin-server/data_class_req_var.mustache new file mode 100644 index 0000000000..659ceefc24 --- /dev/null +++ b/src/main/resources/v2/kotlin-server/data_class_req_var.mustache @@ -0,0 +1,4 @@ +{{#description}} + /* {{{description}}} */ +{{/description}} + val {{{name}}}: {{#is this 'enum'}}{{classname}}.{{nameInCamelCase}}{{/is}}{{#isNot this 'enum'}}{{{datatype}}}{{/isNot}} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-server/enum_class.mustache b/src/main/resources/v2/kotlin-server/enum_class.mustache new file mode 100644 index 0000000000..61324894fd --- /dev/null +++ b/src/main/resources/v2/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/src/main/resources/v2/kotlin-server/enum_doc.mustache b/src/main/resources/v2/kotlin-server/enum_doc.mustache new file mode 100644 index 0000000000..fcb3d7e61a --- /dev/null +++ b/src/main/resources/v2/kotlin-server/enum_doc.mustache @@ -0,0 +1,7 @@ +# {{classname}} + +## Enum + +{{#allowableValues}}{{#enumVars}} + * `{{name}}` (value: `{{{value}}}`) +{{/enumVars}}{{/allowableValues}} diff --git a/src/main/resources/v2/kotlin-server/libraries/ktor/ApiKeyAuth.kt.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/ApiKeyAuth.kt.mustache new file mode 100644 index 0000000000..39a0ea7a84 --- /dev/null +++ b/src/main/resources/v2/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/src/main/resources/v2/kotlin-server/libraries/ktor/AppMain.kt.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/AppMain.kt.mustache new file mode 100644 index 0000000000..53d4a8ba39 --- /dev/null +++ b/src/main/resources/v2/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/src/main/resources/v2/kotlin-server/libraries/ktor/Configuration.kt.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/Configuration.kt.mustache new file mode 100644 index 0000000000..b1262c872d --- /dev/null +++ b/src/main/resources/v2/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}} + {{#is this 'oauth'}} + 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({{#each scopes}}"{{@key}}"{{#unless @last}}, {{/unless}}{{/each}}) + ){{#has this 'more'}},{{/has}} + {{/is}} +{{/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/src/main/resources/v2/kotlin-server/libraries/ktor/Dockerfile.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/Dockerfile.mustache new file mode 100644 index 0000000000..b9158ac26e --- /dev/null +++ b/src/main/resources/v2/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/src/main/resources/v2/kotlin-server/libraries/ktor/Paths.kt.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/Paths.kt.mustache new file mode 100644 index 0000000000..212c2245cd --- /dev/null +++ b/src/main/resources/v2/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}}}{{^required}}? = null{{/required}}{{#has this 'more'}}, {{/has}}{{/allParams}}) + + {{/bodyAllowed}} + {{/operation}} +{{/operations}} +{{/apis}} +} +{{/apiInfo}} diff --git a/src/main/resources/v2/kotlin-server/libraries/ktor/README.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/README.mustache new file mode 100644 index 0000000000..7782665eb3 --- /dev/null +++ b/src/main/resources/v2/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}} + +{{#is this 'api-key'}}- **Type**: API key +- **API key parameter name**: {{keyParamName}} +- **Location**: {{#is this 'key-in-query'}}URL query string{{/is}}{{#is this 'key-in-header'}}HTTP header{{/is}} +{{/is}} +{{#is this 'basic'}}- **Type**: HTTP basic authentication +{{/is}} +{{#is this 'oauth'}}- **Type**: OAuth +- **Flow**: {{flow}} +- **Authorization URL**: {{authorizationUrl}} +- **Scopes**: {{^scopes}}N/A{{/scopes}} +{{#scopes}} - {{scope}}: {{description}} +{{/scopes}} +{{/is}} + +{{/authMethods}} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-server/libraries/ktor/_api_body.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/_api_body.mustache new file mode 100644 index 0000000000..68fdc1434f --- /dev/null +++ b/src/main/resources/v2/kotlin-server/libraries/ktor/_api_body.mustache @@ -0,0 +1,26 @@ +{{#has this 'auth-methods'}} +val principals = mutableListOf() +{{>libraries/ktor/_principal}} +if (principals.any({ it != null })) { + call.respond(HttpStatusCode.Unauthorized) +} else { + {{#examples}} + {{#@first}} + {{>_response}} + {{/@first}} + {{/examples}} + {{^examples}} + call.respond(HttpStatusCode.NotImplemented) + {{/examples}} +} +{{/has}} +{{^notHas this 'auth-methods'}} +{{#examples}} +{{#@first}} +{{>libraries/ktor/_response}} +{{/@first}} +{{/examples}} +{{^examples}} +call.respond(HttpStatusCode.NotImplemented) +{{/examples}} +{{/notHas}} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-server/libraries/ktor/_principal.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/_principal.mustache new file mode 100644 index 0000000000..2b36b05d84 --- /dev/null +++ b/src/main/resources/v2/kotlin-server/libraries/ktor/_principal.mustache @@ -0,0 +1,11 @@ +{{#authMethods}} +{{#is this 'basic'}} + principals.add(call.authentication.principal()) +{{/is}} +{{#is this 'api-key'}} + principals.add(call.authentication.principal()) +{{/is}} +{{#is this 'oauth'}} + principals.add(call.authentication.principal()) +{{/is}} +{{/authMethods}} diff --git a/src/main/resources/v2/kotlin-server/libraries/ktor/_response.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/_response.mustache new file mode 100644 index 0000000000..fb26fb4a7f --- /dev/null +++ b/src/main/resources/v2/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/src/main/resources/v2/kotlin-server/libraries/ktor/api.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/api.mustache new file mode 100644 index 0000000000..9dfad2103c --- /dev/null +++ b/src/main/resources/v2/kotlin-server/libraries/ktor/api.mustache @@ -0,0 +1,115 @@ +{{>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.Principal +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}}") { + {{lower httpMethod}} { + {{>libraries/ktor/_api_body}} + } + } + {{/bodyAllowed}} + {{^bodyAllowed}} + + {{! NOTE: Locations can be used on routes without body parameters.}} + {{lower httpMethod}} { it: Paths.{{operationId}} -> + {{>libraries/ktor/_api_body}} + } + {{/bodyAllowed}} + {{! THis looks a little weird, but it's completely valid Kotlin code, and simplifies templated route logic above. }} + {{#has this 'auth-methods'}}.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}} + {{#is this 'basic'}} + 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 + } + } + {{/is}} + {{#is this 'api-key'}} + // "Implement API key auth ({{{name}}}) for parameter name '{{{keyParamName}}}'." + apiKeyAuth("{{{keyParamName}}}", {{#is this 'key-in-query'}}"query"{{/is}}{{#is this 'key-in-header'}}"header"{{/is}}) { + // TODO: "Verify key here , accessible as it.value" + if (it.value == "keyboardcat") { + ApiPrincipal(it) + } else { + null + } + } + {{/is}} + {{#is this 'oauth'}} + {{#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}} + {{/is}} + {{/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.") + } + } + {{/has}} + {{^notHas this 'auth-methods'}} + + {{/notHas}} +{{/operation}} +} +{{/operations}} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-server/libraries/ktor/application.conf.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/application.conf.mustache new file mode 100644 index 0000000000..d303443788 --- /dev/null +++ b/src/main/resources/v2/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}} +{{#is this 'oauth'}} + {{name}} { + clientId = "" + clientSecret = "" + } +{{/is}} +{{/authMethods}} + } +} \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-server/libraries/ktor/build.gradle.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/build.gradle.mustache new file mode 100644 index 0000000000..f053cf001f --- /dev/null +++ b/src/main/resources/v2/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' + 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/src/main/resources/v2/kotlin-server/libraries/ktor/gradle.properties b/src/main/resources/v2/kotlin-server/libraries/ktor/gradle.properties new file mode 100644 index 0000000000..5f1ed7bbe0 --- /dev/null +++ b/src/main/resources/v2/kotlin-server/libraries/ktor/gradle.properties @@ -0,0 +1 @@ +org.gradle.caching=true \ No newline at end of file diff --git a/src/main/resources/v2/kotlin-server/libraries/ktor/licenseInfo.mustache b/src/main/resources/v2/kotlin-server/libraries/ktor/licenseInfo.mustache new file mode 100644 index 0000000000..aee680977d --- /dev/null +++ b/src/main/resources/v2/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/src/main/resources/v2/kotlin-server/libraries/ktor/logback.xml b/src/main/resources/v2/kotlin-server/libraries/ktor/logback.xml new file mode 100644 index 0000000000..d0eaba8deb --- /dev/null +++ b/src/main/resources/v2/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/src/main/resources/v2/kotlin-server/licenseInfo.mustache b/src/main/resources/v2/kotlin-server/licenseInfo.mustache new file mode 100644 index 0000000000..aee680977d --- /dev/null +++ b/src/main/resources/v2/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/src/main/resources/v2/kotlin-server/model.mustache b/src/main/resources/v2/kotlin-server/model.mustache new file mode 100644 index 0000000000..6f0f576000 --- /dev/null +++ b/src/main/resources/v2/kotlin-server/model.mustache @@ -0,0 +1,14 @@ +{{>licenseInfo}} +package {{modelPackage}} + +{{#imports}}import {{import}} +{{/imports}} +{{#threetenbp}} +import org.threeten.bp.LocalDateTime +{{/threetenbp}} + +{{#models}} +{{#model}} +{{#is this 'alias'}}typealias {{classname}} = {{{dataType}}}{{/is}}{{#isNot this 'alias'}}{{#is this 'enum'}}{{>enum_class}}{{/is}}{{#isNot this 'enum'}}{{>data_class}}{{/isNot}}{{/isNot}} +{{/model}} +{{/models}} diff --git a/src/main/resources/v2/kotlin-server/model_doc.mustache b/src/main/resources/v2/kotlin-server/model_doc.mustache new file mode 100644 index 0000000000..e11fac0390 --- /dev/null +++ b/src/main/resources/v2/kotlin-server/model_doc.mustache @@ -0,0 +1,3 @@ +{{#models}}{{#model}} +{{#is this 'enum'}}{{>enum_doc}}{{/is}}{{#isNot this 'enum'}}{{>class_doc}}{{/isNot}} +{{/model}}{{/models}} diff --git a/src/main/resources/v2/kotlin-server/settings.gradle.mustache b/src/main/resources/v2/kotlin-server/settings.gradle.mustache new file mode 100644 index 0000000000..448dc07602 --- /dev/null +++ b/src/main/resources/v2/kotlin-server/settings.gradle.mustache @@ -0,0 +1 @@ +rootProject.name = '{{artifactId}}' \ No newline at end of file diff --git a/src/test/java/io/swagger/codegen/languages/kotlin/KotlinClientCodegenModelTest.java b/src/test/java/io/swagger/codegen/languages/kotlin/KotlinClientCodegenModelTest.java new file mode 100644 index 0000000000..3f1a2db798 --- /dev/null +++ b/src/test/java/io/swagger/codegen/languages/kotlin/KotlinClientCodegenModelTest.java @@ -0,0 +1,286 @@ +package io.swagger.codegen.languages.kotlin; + +import com.google.common.collect.Sets; + +import io.swagger.codegen.*; +import io.swagger.codegen.languages.kotlin.KotlinClientCodegen; +import io.swagger.codegen.languages.DefaultCodegenConfig; +import io.swagger.models.*; +import io.swagger.models.properties.*; + +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.BooleanSchema; +import io.swagger.v3.oas.models.media.ByteArraySchema; +import io.swagger.v3.oas.models.media.DateTimeSchema; +import io.swagger.v3.oas.models.media.IntegerSchema; +import io.swagger.v3.oas.models.media.MapSchema; +import io.swagger.v3.oas.models.media.NumberSchema; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.StringSchema; + +import io.swagger.v3.parser.util.SchemaTypeUtil; + +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import static io.swagger.codegen.languages.helpers.ExtensionHelper.getBooleanValue; + +@SuppressWarnings("static-method") +public class KotlinClientCodegenModelTest { + + protected static final Logger LOGGER = LoggerFactory.getLogger(KotlinClientCodegenModelTest.class); + + private Schema getArrayTestSchema() { + final Schema propertySchema = new ArraySchema() + .items(new StringSchema()) + .description("an array property"); + return new Schema() + .type("object") + .description("a sample model") + .addProperties("examples", propertySchema); + } + + private Schema getSimpleSchema() { + return new Schema() + .type("object") + .description("a sample model") + .addProperties("id", new IntegerSchema() + .format(SchemaTypeUtil.INTEGER64_FORMAT)) + .addProperties("first-name", new StringSchema() + .example("Tony")) + .addProperties("createdAt", new DateTimeSchema()) + .addRequiredItem("id") + .addRequiredItem("first-name"); + } + + private Schema getMapSchema() { + return new Schema() + .type("object") + .description("a sample model") + .addProperties("mapping", new MapSchema() + .additionalProperties(new StringSchema())) + .addRequiredItem("id"); + } + + private Schema getComplexSchema() { + return new Schema() + .description("a sample model") + .type("object") + .addProperties("child", new Schema().$ref("#/components/schemas/Child")); + } + + @Test(description = "convert a simple model") + public void simpleModelTest() { + final Schema schema = getSimpleSchema(); + final KotlinClientCodegen codegen = new KotlinClientCodegen(); + final CodegenModel cm = codegen.fromModel("sample", schema); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "a sample model"); + Assert.assertEquals(cm.vars.size(), 3); + + final CodegenProperty property1 = cm.vars.get(0); + Assert.assertEquals(property1.baseName, "id"); + Assert.assertEquals(property1.datatype, "kotlin.Long"); + Assert.assertEquals(property1.name, "id"); + Assert.assertEquals(property1.defaultValue, "null"); + Assert.assertEquals(property1.baseType, "kotlin.Long"); + Assert.assertTrue(getBooleanValue(property1, CodegenConstants.HAS_MORE_EXT_NAME)); + Assert.assertTrue(property1.required); + Assert.assertTrue(getBooleanValue(property1, CodegenConstants.IS_PRIMITIVE_TYPE_EXT_NAME)); + Assert.assertTrue(getBooleanValue(property1, CodegenConstants.IS_NOT_CONTAINER_EXT_NAME)); + + final CodegenProperty property2 = cm.vars.get(1); + Assert.assertEquals(property2.baseName, "first-name"); + Assert.assertEquals(property2.datatype, "kotlin.String"); + Assert.assertEquals(property2.name, "firstName"); + Assert.assertEquals(property2.defaultValue, "null"); + Assert.assertEquals(property2.baseType, "kotlin.String"); + Assert.assertTrue(getBooleanValue(property2, CodegenConstants.HAS_MORE_EXT_NAME)); + Assert.assertTrue(property2.required); + Assert.assertTrue(getBooleanValue(property2, CodegenConstants.IS_PRIMITIVE_TYPE_EXT_NAME)); + Assert.assertTrue(getBooleanValue(property2, CodegenConstants.IS_NOT_CONTAINER_EXT_NAME)); + + final CodegenProperty property3 = cm.vars.get(2); + Assert.assertEquals(property3.baseName, "createdAt"); + Assert.assertEquals(property3.datatype, "java.time.LocalDateTime"); + Assert.assertEquals(property3.name, "createdAt"); + Assert.assertEquals(property3.defaultValue, "null"); + Assert.assertEquals(property3.baseType, "java.time.LocalDateTime"); + Assert.assertFalse(getBooleanValue(property3, CodegenConstants.HAS_MORE_EXT_NAME)); + Assert.assertFalse(property3.required); + Assert.assertTrue(getBooleanValue(property3, CodegenConstants.IS_NOT_CONTAINER_EXT_NAME)); + } + + @Test(description = "convert a simple model: threetenbp") + public void selectDateLibraryAsThreetenbp() { + final Schema schema = getSimpleSchema(); + final KotlinClientCodegen codegen = new KotlinClientCodegen(); + codegen.setDateLibrary(KotlinClientCodegen.DateLibrary.THREETENBP.value); + codegen.processOpts(); + + final CodegenModel cm = codegen.fromModel("sample", schema); + + final CodegenProperty property3 = cm.vars.get(2); + Assert.assertEquals(property3.baseName, "createdAt"); + Assert.assertEquals(property3.datatype, "org.threeten.bp.LocalDateTime"); + Assert.assertEquals(property3.name, "createdAt"); + Assert.assertEquals(property3.defaultValue, "null"); + Assert.assertEquals(property3.baseType, "org.threeten.bp.LocalDateTime"); + Assert.assertFalse(getBooleanValue(property3, CodegenConstants.HAS_MORE_EXT_NAME)); + Assert.assertFalse(property3.required); + Assert.assertTrue(getBooleanValue(property3, CodegenConstants.IS_NOT_CONTAINER_EXT_NAME)); + } + + @Test(description = "convert a simple model: date string") + public void selectDateLibraryAsString() { + final Schema schema = getSimpleSchema(); + final KotlinClientCodegen codegen = new KotlinClientCodegen(); + codegen.setDateLibrary(KotlinClientCodegen.DateLibrary.STRING.value); + codegen.processOpts(); + + final CodegenModel cm = codegen.fromModel("sample", schema); + + final CodegenProperty property3 = cm.vars.get(2); + Assert.assertEquals(property3.baseName, "createdAt"); + Assert.assertEquals(property3.datatype, "kotlin.String"); + Assert.assertEquals(property3.name, "createdAt"); + Assert.assertEquals(property3.defaultValue, "null"); + Assert.assertEquals(property3.baseType, "kotlin.String"); + Assert.assertFalse(getBooleanValue(property3, CodegenConstants.HAS_MORE_EXT_NAME)); + Assert.assertFalse(property3.required); + Assert.assertTrue(getBooleanValue(property3, CodegenConstants.IS_NOT_CONTAINER_EXT_NAME)); + } + + @Test(description = "convert a simple model: date java8") + public void selectDateLibraryAsJava8() { + final Schema schema = getSimpleSchema(); + final KotlinClientCodegen codegen = new KotlinClientCodegen(); + codegen.setDateLibrary(KotlinClientCodegen.DateLibrary.JAVA8.value); + codegen.processOpts(); + + final CodegenModel cm = codegen.fromModel("sample", schema); + + final CodegenProperty property3 = cm.vars.get(2); + Assert.assertEquals(property3.baseName, "createdAt"); + Assert.assertEquals(property3.datatype, "java.time.LocalDateTime"); + Assert.assertEquals(property3.name, "createdAt"); + Assert.assertEquals(property3.defaultValue, "null"); + Assert.assertEquals(property3.baseType, "java.time.LocalDateTime"); + Assert.assertFalse(getBooleanValue(property3, CodegenConstants.HAS_MORE_EXT_NAME)); + Assert.assertFalse(property3.required); + Assert.assertTrue(getBooleanValue(property3, CodegenConstants.IS_NOT_CONTAINER_EXT_NAME)); + } + + @Test(description = "convert a model with array property to default kotlin.Array") + public void arrayPropertyTest() { + final Schema schema = getArrayTestSchema(); + + final CodegenConfig codegen = new KotlinClientCodegen(); + final CodegenModel generated = codegen.fromModel("sample", schema); + + Assert.assertEquals(generated.name, "sample"); + Assert.assertEquals(generated.classname, "Sample"); + Assert.assertEquals(generated.description, "a sample model"); + Assert.assertEquals(generated.vars.size(), 1); + + final CodegenProperty property = generated.vars.get(0); + Assert.assertEquals(property.baseName, "examples"); + Assert.assertEquals(property.getter, "getExamples"); + Assert.assertEquals(property.setter, "setExamples"); + Assert.assertEquals(property.datatype, "kotlin.Array"); + Assert.assertEquals(property.name, "examples"); + Assert.assertEquals(property.defaultValue, "null"); + Assert.assertEquals(property.baseType, "kotlin.Array"); + Assert.assertEquals(property.containerType, "array"); + Assert.assertFalse(property.required); + Assert.assertTrue(getBooleanValue(property, CodegenConstants.IS_CONTAINER_EXT_NAME)); + } + + @Test(description = "convert a model with a map property") + public void mapPropertyTest() { + final Schema schema = getMapSchema(); + final CodegenConfig codegen = new KotlinClientCodegen(); + final CodegenModel cm = codegen.fromModel("sample", schema); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "a sample model"); + Assert.assertEquals(cm.vars.size(), 1); + + final CodegenProperty property1 = cm.vars.get(0); + Assert.assertEquals(property1.baseName, "mapping"); + Assert.assertEquals(property1.datatype, "kotlin.collections.Map"); + Assert.assertEquals(property1.name, "mapping"); + Assert.assertEquals(property1.baseType, "kotlin.collections.Map"); + Assert.assertEquals(property1.containerType, "map"); + Assert.assertFalse(property1.required); + Assert.assertTrue(getBooleanValue(property1, CodegenConstants.IS_CONTAINER_EXT_NAME)); + Assert.assertTrue(getBooleanValue(property1, CodegenConstants.IS_PRIMITIVE_TYPE_EXT_NAME)); + } + + @Test(description = "convert a model with complex property") + public void complexPropertyTest() { + final Schema schema = getComplexSchema(); + final CodegenConfig codegen = new KotlinClientCodegen(); + final CodegenModel cm = codegen.fromModel("sample", schema); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "a sample model"); + Assert.assertEquals(cm.vars.size(), 1); + + final CodegenProperty property1 = cm.vars.get(0); + Assert.assertEquals(property1.baseName, "child"); + Assert.assertEquals(property1.datatype, "Child"); + Assert.assertEquals(property1.name, "child"); + Assert.assertEquals(property1.baseType, "Child"); + Assert.assertFalse(property1.required); + Assert.assertTrue(getBooleanValue(property1, CodegenConstants.IS_NOT_CONTAINER_EXT_NAME)); + } + + @DataProvider(name = "modelNames") + public static Object[][] modelNames() { + return new Object[][]{ + {"TestNs.TestClass", new ModelNameTest("TestNs.TestClass", "TestNsTestClass")}, + {"$", new ModelNameTest("$", "Dollar")}, + {"for", new ModelNameTest("`for`", "`for`")}, + {"One createOptions() { + ImmutableMap.Builder builder = new ImmutableMap.Builder(); + return builder + .put(CodegenConstants.PACKAGE_NAME, PACKAGE_NAME_VALUE) + .put(CodegenConstants.ARTIFACT_VERSION, ARTIFACT_VERSION_VALUE) + .put(CodegenConstants.ARTIFACT_ID, ARTIFACT_ID) + .put(CodegenConstants.GROUP_ID, GROUP_ID) + .put(CodegenConstants.SOURCE_FOLDER, SOURCE_FOLDER) + .put(CodegenConstants.ENUM_PROPERTY_NAMING, ENUM_PROPERTY_NAMING) + .put(KotlinClientCodegen.DATE_LIBRARY, DATE_LIBRARY) + .build(); + } + + @Override + public boolean isServer() { + return false; + } +} +