From 23d5d5eddff626cb411d9b031f4f17c19ebdaa86 Mon Sep 17 00:00:00 2001 From: kroegerama <1519044+kroegerama@users.noreply.github.com> Date: Mon, 10 Jun 2019 12:41:23 +0200 Subject: [PATCH 01/11] add new generator: kotlin-retrofit --- .../languages/KotlinRetrofitCodegen.java | 209 ++++++++++++++++++ .../org.openapitools.codegen.CodegenConfig | 1 + .../AndroidManifest.xml.mustache | 5 + .../AuthInterceptors.kt.mustache | 57 +++++ .../resources/kotlin-retrofit/README.mustache | 26 +++ .../RetrofitHolder.kt.mustache | 76 +++++++ .../resources/kotlin-retrofit/api.mustache | 58 +++++ .../kotlin-retrofit/auth_method.mustache | 1 + .../kotlin-retrofit/bodyParams.mustache | 1 + .../kotlin-retrofit/build.gradle.mustache | 43 ++++ .../kotlin-retrofit/data_class.mustache | 40 ++++ .../data_class_opt_var.mustache | 1 + .../data_class_req_var.mustache | 1 + .../kotlin-retrofit/enum_class.mustache | 13 ++ .../kotlin-retrofit/formParams.mustache | 1 + .../kotlin-retrofit/gitignore.mustache | 1 + .../kotlin-retrofit/headerParams.mustache | 1 + .../kotlin-retrofit/licenseInfo.mustache | 11 + .../resources/kotlin-retrofit/model.mustache | 11 + .../kotlin-retrofit/pathParams.mustache | 1 + .../proguard-rules.pro.mustache | 21 ++ .../kotlin-retrofit/queryParams.mustache | 1 + 22 files changed, 580 insertions(+) create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/AndroidManifest.xml.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/AuthInterceptors.kt.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/README.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/api.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/auth_method.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/bodyParams.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/formParams.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/gitignore.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/headerParams.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/licenseInfo.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/model.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/pathParams.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/proguard-rules.pro.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/queryParams.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java new file mode 100644 index 000000000000..75aa36ba7910 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java @@ -0,0 +1,209 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * Copyright 2018 SmartBear Software + * Copyright 2019 kroegerama + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.languages; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; +import org.apache.commons.lang3.StringUtils; +import org.openapitools.codegen.*; +import org.openapitools.codegen.templating.mustache.CamelCaseLambda; + +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.openapitools.codegen.utils.StringUtils.camelize; + +public class KotlinRetrofitCodegen extends AbstractKotlinCodegen { + + private class EnumCaseLambda implements Mustache.Lambda { + @Override + public void execute(Template.Fragment fragment, Writer writer) throws IOException { + String text = fragment.execute(); + text = camelize(text); + text = toVarName(text); + if (text.length() > 0) { + text = text.substring(0, 1).toUpperCase(Locale.ROOT) + text.substring(1); + } + writer.write(text); + } + } + + public static final String DATE_LIBRARY = "dateLibrary"; + public static final String COLLECTION_TYPE = "collectionType"; + + protected String dateLibrary = DateLibrary.STRING.value; + protected String collectionType = CollectionType.ARRAY.value; + + public enum DateLibrary { + STRING("string"), + THREETENBP("threetenbp"), + JAVA8("java8"); + + public final String value; + + DateLibrary(String value) { + this.value = value; + } + } + + public enum CollectionType { + ARRAY("array"), + LIST("list"); + + public final String value; + + CollectionType(String value) { + this.value = value; + } + } + + /** + * Constructs an instance of `KotlinRetrofitCodegen`. + */ + public KotlinRetrofitCodegen() { + super(); + + artifactId = "kotlin-retrofit"; + packageName = "org.openapitools.client"; + + sourceFolder = "src/main/java"; + + // cliOptions default redefinition need to be updated + updateOption(CodegenConstants.ARTIFACT_ID, this.artifactId); + updateOption(CodegenConstants.PACKAGE_NAME, this.packageName); + + outputFolder = "generated-code" + File.separator + "kotlin-retrofit"; + modelTemplateFiles.put("model.mustache", ".kt"); + apiTemplateFiles.put("api.mustache", ".kt"); + embeddedTemplateDir = templateDir = "kotlin-retrofit"; + 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); + dateLibrary.setDefault(this.dateLibrary); + cliOptions.add(dateLibrary); + + CliOption collectionType = new CliOption(COLLECTION_TYPE, "Option. Collection type to use"); + Map collectionOptions = new HashMap<>(); + collectionOptions.put(CollectionType.ARRAY.value, "kotlin.Array"); + collectionOptions.put(CollectionType.LIST.value, "kotlin.collections.List"); + collectionType.setEnum(collectionOptions); + collectionType.setDefault(this.collectionType); + cliOptions.add(collectionType); + + additionalProperties.put("enumcase", new EnumCaseLambda()); + CamelCaseLambda camelCase = new CamelCaseLambda(); + camelCase.generator(this); + camelCase.escapeAsParamName(true); + additionalProperties.put("camelcase", camelCase); + } + + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + public String getName() { + return "kotlin-retrofit"; + } + + public String getHelp() { + return "Generates an android library module. Uses Kotlin, Retrofit and Coroutines."; + } + + public void setDateLibrary(String library) { + this.dateLibrary = library; + } + + public void setCollectionType(String collectionType) { + this.collectionType = collectionType; + } + + @Override + public Map postProcessOperationsWithModels(Map objs, List allModels) { + super.postProcessOperationsWithModels(objs, allModels); + Map operations = (Map) objs.get("operations"); + if (operations == null) return objs; + + List ops = (List) operations.get("operation"); + for (CodegenOperation operation : ops) { + if (StringUtils.isNotEmpty(operation.path) && operation.path.startsWith("/")) { + operation.path = operation.path.substring(1); + } + } + return objs; + } + + @Override + public void processOpts() { + super.processOpts(); + + 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.LocalDate"); + 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); + } + + if (additionalProperties.containsKey(COLLECTION_TYPE)) { + setCollectionType(additionalProperties.get(COLLECTION_TYPE).toString()); + } + + if (CollectionType.LIST.value.equals(collectionType)) { + typeMapping.put("array", "kotlin.collections.List"); + typeMapping.put("list", "kotlin.collections.List"); + } + + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + supportingFiles.add(new SupportingFile("build.gradle.mustache", "", "build.gradle")); + + final String infrastructureFolder = (sourceFolder + File.separator + packageName).replace(".", "/"); + + supportingFiles.add(new SupportingFile("AuthInterceptors.kt.mustache", infrastructureFolder, "AuthInterceptors.kt")); + supportingFiles.add(new SupportingFile("RetrofitHolder.kt.mustache", infrastructureFolder, "RetrofitHolder.kt")); + + supportingFiles.add(new SupportingFile("proguard-rules.pro.mustache", "", "proguard-rules.pro")); + supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore")); + + supportingFiles.add(new SupportingFile("AndroidManifest.xml.mustache", "src/main", "AndroidManifest.xml")); + } +} diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 3f21ea57ec09..302ff4d27930 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -34,6 +34,7 @@ org.openapitools.codegen.languages.GraphQLSchemaCodegen org.openapitools.codegen.languages.GraphQLNodeJSExpressServerCodegen org.openapitools.codegen.languages.GroovyClientCodegen org.openapitools.codegen.languages.KotlinClientCodegen +org.openapitools.codegen.languages.KotlinRetrofitCodegen org.openapitools.codegen.languages.KotlinServerCodegen org.openapitools.codegen.languages.KotlinSpringServerCodegen org.openapitools.codegen.languages.HaskellHttpClientCodegen diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/AndroidManifest.xml.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/AndroidManifest.xml.mustache new file mode 100644 index 000000000000..4df27691129e --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/AndroidManifest.xml.mustache @@ -0,0 +1,5 @@ + + + + diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/AuthInterceptors.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/AuthInterceptors.kt.mustache new file mode 100644 index 000000000000..577192b1adb7 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/AuthInterceptors.kt.mustache @@ -0,0 +1,57 @@ +package {{packageName}} + +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response + +typealias AuthGeneratorFun = (authName: String, Request) -> String? + +sealed class AuthInterceptor( + private val authName: String, + private val generator: AuthGeneratorFun +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val apiKey = generator(authName, chain.request()) ?: return chain.proceed(chain.request()) + return handleApiKey(chain, apiKey) + } + + protected abstract fun handleApiKey(chain: Interceptor.Chain, apiKey: String): Response +} + +class HeaderParamInterceptor( + authName: String, + private val paramName: String, + generator: AuthGeneratorFun +) : AuthInterceptor(authName, generator) { + + override fun handleApiKey(chain: Interceptor.Chain, apiKey: String): Response { + val newRequest = chain.request() + .newBuilder() + .addHeader(paramName, apiKey) + .build() + + return chain.proceed(newRequest) + } +} + +class QueryParamInterceptor( + authName: String, + private val paramName: String, + generator: AuthGeneratorFun +) : AuthInterceptor(authName, generator) { + + override fun handleApiKey(chain: Interceptor.Chain, apiKey: String): Response { + val newUrl = chain.request().url() + .newBuilder() + .addQueryParameter(paramName, apiKey) + .build() + + val newRequest = chain.request() + .newBuilder() + .url(newUrl) + .build() + + return chain.proceed(newRequest) + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/README.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/README.mustache new file mode 100644 index 000000000000..8d527ec691fc --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/README.mustache @@ -0,0 +1,26 @@ +# {{packageName}} - Android library module for {{appName}} + +## Dependencies + +* Kotlin +* Gradle +* Coroutines +* Moshi +* OkHttp +* Retrofit + +## Usage + +Just add the generated folder to your Android Studio project. + +#### Generator State: **pre-alpha** + +## Todo + +* fix api.mustache: double @Header generation possible +* add better support for json schema inheritance (allOf, anyOf, etc.) +* add OAuth functionality +* update proguard/p8 rules for okhttp/moshi +* option to switch coroutine generation on/off +* add documentation +* add tests \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache new file mode 100644 index 000000000000..0c7cfdb75dc5 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache @@ -0,0 +1,76 @@ +package {{packageName}} + +import com.squareup.moshi.Moshi +import okhttp3.Credentials +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory + +object RetrofitHolder { + + const val BASE_URL = "{{basePath}}/" + + val clientBuilder: OkHttpClient.Builder by lazy { + OkHttpClient().newBuilder(){{#authMethods}}{{#-first}} + .addNetworkInterceptor { chain -> + val newRequest = chain.request().newBuilder() + .removeHeader(AUTH_NAME_HEADER) + .build() + chain.proceed(newRequest) + }{{/-first}}{{/authMethods}} + .addNetworkInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.HEADERS + }){{#authMethods}} + {{>auth_method}}{{/authMethods}} + } + + val retrofitBuilder: Retrofit.Builder by lazy { + val moshi = Moshi.Builder() + //.add(OffsetDateTimeAdapter()) + .build() + + Retrofit.Builder() + .client(clientBuilder.build()) + .baseUrl(BASE_URL) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + } + + val retrofit: Retrofit by lazy { retrofitBuilder.build() } + + private val securityDefinitions = HashMap() + +{{#authMethods}}{{#-first}} + fun setApiKey(authMethod: AuthMethod, apiKey: String) { + securityDefinitions[authMethod.authName] = apiKey + } + + fun setBasicAuth(authMethod: AuthMethod, username: String, password: String) { + securityDefinitions[authMethod.authName] = Credentials.basic(username, password) + } + + fun setBearerAuth(authMethod: AuthMethod, bearer: String) { + securityDefinitions[authMethod.authName] = "Bearer $bearer" + } + + private fun apiKeyGenerator(authName: String, request: Request): String? = + if (requestHasAuth(request, authName)) + securityDefinitions[authName] + else + null + + + private fun requestHasAuth(request: Request, authName: String): Boolean { + val headers = request.headers(AUTH_NAME_HEADER) ?: return false + return headers.contains(authName) + } + + const val AUTH_NAME_HEADER = "X-Auth-Name" +{{/-first}}{{/authMethods}} +} + +{{#authMethods}}{{#-first}}enum class AuthMethod(internal val authName: String) { {{/-first}} + {{#enumcase}}{{{name}}}{{/enumcase}}("{{{name}}}"){{^-last}},{{/-last}}{{#-last}} +}{{/-last}} +{{/authMethods}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/api.mustache new file mode 100644 index 000000000000..316d5e6e7e6c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/api.mustache @@ -0,0 +1,58 @@ +{{>licenseInfo}} +package {{apiPackage}} + +{{#imports}}import {{import}} +{{/imports}} + +import {{packageName}}.RetrofitHolder +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.create +import retrofit2.http.* +import okhttp3.* +import retrofit2.http.Headers + +{{#operations}} +interface {{classname}} { + + {{#operation}} + /** + * {{summary}} + * {{notes}} + * Responses:{{#responses}} + * - {{code}}: {{{message}}}{{/responses}} + * + {{#allParams}}* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}} + {{/allParams}}* @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} + */ + @{{httpMethod}}("{{{path}}}") + {{#isDeprecated}} + @Deprecated("") + {{/isDeprecated}} + {{#formParams}} + {{#-first}} + {{#isMultipart}}@Multipart{{/isMultipart}}{{^isMultipart}}@FormUrlEncoded{{/isMultipart}} + {{/-first}} + {{/formParams}} + {{^formParams}} + {{#prioritizedContentTypes}} + {{#-first}} + @Headers( + "Content-Type:{{{mediaType}}}" + ) + {{/-first}} + {{/prioritizedContentTypes}} + {{/formParams}} + {{#authMethods}}{{#-first}}@Headers({{/-first}}RetrofitHolder.AUTH_NAME_HEADER + ": {{{name}}}"{{^-last}}, {{/-last}}{{#-last}}){{/-last}}{{/authMethods}} + suspend fun {{operationId}}( + {{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{#hasMore}}, + {{/hasMore}}{{/allParams}} + ): Response<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}> + + {{/operation}} + + companion object { + fun create(retrofit: Retrofit = RetrofitHolder.retrofit): {{classname}} = retrofit.create() + } +} +{{/operations}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/auth_method.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/auth_method.mustache new file mode 100644 index 000000000000..306f01a9b076 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/auth_method.mustache @@ -0,0 +1 @@ +{{#isKeyInHeader}}.addInterceptor(HeaderParamInterceptor("{{{name}}}", "{{{keyParamName}}}", this::apiKeyGenerator)){{/isKeyInHeader}}{{#isKeyInQuery}}.addInterceptor(QueryParamInterceptor("{{{name}}}", "{{{keyParamName}}}", this::apiKeyGenerator)){{/isKeyInQuery}}{{#isBasicBasic}}.addInterceptor(HeaderParamInterceptor("{{{name}}}", "Authorization", this::apiKeyGenerator)){{/isBasicBasic}}{{#isBasicBearer}}.addInterceptor(HeaderParamInterceptor("{{{name}}}", "Authorization", this::apiKeyGenerator)){{/isBasicBearer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/bodyParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/bodyParams.mustache new file mode 100644 index 000000000000..079fbb1169b5 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/bodyParams.mustache @@ -0,0 +1 @@ +{{#isBodyParam}}@Body {{paramName}}: {{{dataType}}}{{/isBodyParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache new file mode 100644 index 000000000000..1a857be85486 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache @@ -0,0 +1,43 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } + compileOptions { + targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_8 + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation 'com.squareup.okhttp3:logging-interceptor:3.14.2' + + api 'com.squareup.retrofit2:retrofit:2.6.0' + {{#threetenbp}} + api 'com.jakewharton.threetenabp:threetenabp:1.2.1' + {{/threetenbp}} + + implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' + api 'com.squareup.moshi:moshi:1.8.0' + kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.8.0' + + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' + +} diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache new file mode 100644 index 000000000000..fa62ee50495e --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache @@ -0,0 +1,40 @@ +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +{{#parcelizeModels}} +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize +{{/parcelizeModels}} + +/** + * {{{description}}} +{{#vars}} + * @param {{name}} {{{description}}} +{{/vars}} + */ +{{#parcelizeModels}} +@Parcelize +{{/parcelizeModels}} +@JsonClass(generateAdapter = true) +data class {{classname}} ( +{{#requiredVars}} +{{>data_class_req_var}}{{^-last}}, + +{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, + +{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>data_class_opt_var}}{{^-last}}, + +{{/-last}}{{/optionalVars}} +){{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{#hasEnums}} { +{{#vars}}{{#isEnum}} + /** + * {{{description}}} + * Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} + */ + enum class {{nameInCamelCase}}(val value: {{dataType}}){ + {{#allowableValues}}{{#enumVars}} + @Json(name = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) + {{^isString}}{{nameInCamelCase}}{{/isString}}{{#enumcase}}{{&name}}{{/enumcase}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} + {{/enumVars}}{{/allowableValues}} + } +{{/isEnum}}{{/vars}} +}{{/hasEnums}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache new file mode 100644 index 000000000000..09e74d90c394 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache @@ -0,0 +1 @@ + @Json(name = "{{{baseName}}}") val {{#title}}{{#camelcase}}{{title}}{{/camelcase}}{{/title}}{{^title}}{{#camelcase}}{{name}}{{/camelcase}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{#defaultValue}}{{#isEnum}}{{classname}}.{{nameInCamelCase}}.{{#enumcase}}{{{defaultValue}}}{{/enumcase}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache new file mode 100644 index 000000000000..41e57e7df7c3 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache @@ -0,0 +1 @@ + @Json(name = "{{{baseName}}}") val {{#title}}{{#camelcase}}{{title}}{{/camelcase}}{{/title}}{{^title}}{{#camelcase}}{{name}}{{/camelcase}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#defaultValue}} = {{#isEnum}}{{classname}}.{{nameInCamelCase}}.{{#enumcase}}{{{defaultValue}}}{{/enumcase}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache new file mode 100644 index 000000000000..a6b311fa2b7f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache @@ -0,0 +1,13 @@ +import com.squareup.moshi.Json + +/** +* {{{description}}} +* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} +*/ +enum class {{classname}}(val value: {{dataType}}){ +{{#allowableValues}}{{#enumVars}} + @Json(name = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) + {{^isString}}{{nameInCamelCase}}{{/isString}}{{#enumcase}}{{&name}}{{/enumcase}}({{{value}}}){{^-last}}, + {{/-last}}{{#-last}};{{/-last}} +{{/enumVars}}{{/allowableValues}} +} diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/formParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/formParams.mustache new file mode 100644 index 000000000000..d3c86c3e8870 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/formParams.mustache @@ -0,0 +1 @@ +{{#isFormParam}}{{^isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field{{/isMultipart}}("{{baseName}}") {{paramName}}: {{{dataType}}}{{/isFile}}{{#isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field("{{baseName}}"){{/isMultipart}} {{paramName}}: MultipartBody.Part{{/isFile}}{{/isFormParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/gitignore.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/gitignore.mustache new file mode 100644 index 000000000000..796b96d1c402 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/gitignore.mustache @@ -0,0 +1 @@ +/build diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/headerParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/headerParams.mustache new file mode 100644 index 000000000000..2b26c446678c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/headerParams.mustache @@ -0,0 +1 @@ +{{#isHeaderParam}}@Header("{{baseName}}") {{paramName}}: {{{dataType}}}{{/isHeaderParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/licenseInfo.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/licenseInfo.mustache new file mode 100644 index 000000000000..3a547de74bb7 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/licenseInfo.mustache @@ -0,0 +1,11 @@ +/** +* {{{appName}}} +* {{{appDescription}}} +* +* {{#version}}The version of the OpenAPI document: {{{version}}}{{/version}} +* {{#infoEmail}}Contact: {{{infoEmail}}}{{/infoEmail}} +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/model.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/model.mustache new file mode 100644 index 000000000000..b9514dad4d7b --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/model.mustache @@ -0,0 +1,11 @@ +{{>licenseInfo}} +package {{modelPackage}} + +{{#imports}}import {{import}} +{{/imports}} + +{{#models}} +{{#model}} +{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{#isAlias}}typealias {{classname}} = {{dataType}}{{/isAlias}}{{^isAlias}}{{>data_class}}{{/isAlias}}{{/isEnum}} +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/pathParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/pathParams.mustache new file mode 100644 index 000000000000..996a45473a95 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/pathParams.mustache @@ -0,0 +1 @@ +{{#isPathParam}}@Path("{{baseName}}") {{paramName}}: {{{dataType}}}{{/isPathParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/proguard-rules.pro.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/proguard-rules.pro.mustache new file mode 100644 index 000000000000..201ce968d17f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/proguard-rules.pro.mustache @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class paramName to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file paramName. +#-renamesourcefileattribute SourceFile diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/queryParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/queryParams.mustache new file mode 100644 index 000000000000..14c40f05e588 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/queryParams.mustache @@ -0,0 +1 @@ +{{#isQueryParam}}@Query("{{baseName}}") {{paramName}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}{{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{{dataType}}}{{/collectionFormat}}{{/isQueryParam}} \ No newline at end of file From 9f4993581804dab9bfe87cd388f4353ae5bec8bd Mon Sep 17 00:00:00 2001 From: kroegerama <1519044+kroegerama@users.noreply.github.com> Date: Sun, 16 Jun 2019 17:26:16 +0200 Subject: [PATCH 02/11] improve generated code quality * KotlinRetrofitCodegen no more uses AbstractKotlinCodegen (has wrong reserved word list) * support for default values for Api parameters --- .../languages/KotlinRetrofitCodegen.java | 839 ++++++++++++++++-- .../RetrofitHolder.kt.mustache | 2 +- .../resources/kotlin-retrofit/api.mustache | 4 +- .../kotlin-retrofit/bodyParams.mustache | 2 +- .../kotlin-retrofit/data_class.mustache | 5 +- .../data_class_opt_var.mustache | 2 +- .../data_class_req_var.mustache | 2 +- .../kotlin-retrofit/enum_class.mustache | 5 +- .../kotlin-retrofit/formParams.mustache | 2 +- .../kotlin-retrofit/headerParams.mustache | 2 +- .../kotlin-retrofit/pathParams.mustache | 2 +- .../kotlin-retrofit/queryParams.mustache | 2 +- 12 files changed, 799 insertions(+), 70 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java index 75aa36ba7910..904463c5cb67 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java @@ -18,42 +18,55 @@ package org.openapitools.codegen.languages; -import com.samskivert.mustache.Mustache; -import com.samskivert.mustache.Template; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Schema; +import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.openapitools.codegen.*; -import org.openapitools.codegen.templating.mustache.CamelCaseLambda; +import org.openapitools.codegen.utils.ModelUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; -import java.io.IOException; -import java.io.Writer; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import static org.openapitools.codegen.utils.StringUtils.camelize; - -public class KotlinRetrofitCodegen extends AbstractKotlinCodegen { - - private class EnumCaseLambda implements Mustache.Lambda { - @Override - public void execute(Template.Fragment fragment, Writer writer) throws IOException { - String text = fragment.execute(); - text = camelize(text); - text = toVarName(text); - if (text.length() > 0) { - text = text.substring(0, 1).toUpperCase(Locale.ROOT) + text.substring(1); - } - writer.write(text); - } - } +import java.util.*; + +import static org.openapitools.codegen.utils.StringUtils.*; + +public class KotlinRetrofitCodegen extends DefaultCodegen implements CodegenConfig { + private static final Logger LOGGER = LoggerFactory.getLogger(KotlinRetrofitCodegen.class); + +// private class EnumCaseLambda implements Mustache.Lambda { +// @Override +// public void execute(Template.Fragment fragment, Writer writer) throws IOException { +// String text = fragment.execute(); +// text = camelize(text); +// text = toVarName(text); +// if (text.length() > 0) { +// text = text.substring(0, 1).toUpperCase(Locale.ROOT) + text.substring(1); +// } +// writer.write(text); +// } +// } public static final String DATE_LIBRARY = "dateLibrary"; public static final String COLLECTION_TYPE = "collectionType"; + protected String artifactId = "kotlin-retrofit"; + protected String artifactVersion = "1.0.0"; + protected String groupId = "org.openapitools"; + protected String packageName = "org.openapitools.client"; + protected String apiSuffix = "Api"; + + protected String sourceFolder = "src/main/java"; + protected String testFolder = "src/test/java"; + + protected String apiDocPath = "docs/"; + protected String modelDocPath = "docs/"; + + protected boolean parcelizeModels = false; protected String dateLibrary = DateLibrary.STRING.value; protected String collectionType = CollectionType.ARRAY.value; + protected CodegenConstants.ENUM_PROPERTY_NAMING_TYPE enumPropertyNaming = CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.PascalCase; public enum DateLibrary { STRING("string"), @@ -82,24 +95,166 @@ public enum CollectionType { * Constructs an instance of `KotlinRetrofitCodegen`. */ public KotlinRetrofitCodegen() { - super(); - - artifactId = "kotlin-retrofit"; - packageName = "org.openapitools.client"; + supportsInheritance = true; + specialCharReplacements.put(";", "Semicolon"); - sourceFolder = "src/main/java"; + initPrimitives(); + initReservedWords(); + initTypeMapping(); + initInstantiationTypes(); + initImportMapping(); // cliOptions default redefinition need to be updated updateOption(CodegenConstants.ARTIFACT_ID, this.artifactId); updateOption(CodegenConstants.PACKAGE_NAME, this.packageName); outputFolder = "generated-code" + File.separator + "kotlin-retrofit"; + embeddedTemplateDir = templateDir = "kotlin-retrofit"; modelTemplateFiles.put("model.mustache", ".kt"); apiTemplateFiles.put("api.mustache", ".kt"); - embeddedTemplateDir = templateDir = "kotlin-retrofit"; apiPackage = packageName + ".apis"; modelPackage = packageName + ".models"; +// additionalProperties.put("enumcase", new EnumCaseLambda()); +// CamelCaseLambda camelCase = new CamelCaseLambda(); +// camelCase.generator(this); +// camelCase.escapeAsParamName(true); +// additionalProperties.put("camelcase", camelCase); + + initCliOptions(); + } + + private void initPrimitives() { + languageSpecificPrimitives = new HashSet<>(Arrays.asList( + "kotlin.Byte", + "kotlin.ByteArray", + "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" + )); + defaultIncludes = new HashSet<>(Arrays.asList( + "kotlin.Byte", + "kotlin.ByteArray", + "kotlin.Short", + "kotlin.Int", + "kotlin.Long", + "kotlin.Float", + "kotlin.Double", + "kotlin.Boolean", + "kotlin.Char", + "kotlin.String", + "kotlin.Array", + "kotlin.collections.List", + "kotlin.collections.Set", + "kotlin.collections.Map" + )); + } + + private void initTypeMapping() { + typeMapping = new HashMap<>(); + typeMapping.put("object", "kotlin.Any"); + + typeMapping.put("string", "kotlin.String"); + typeMapping.put("boolean", "kotlin.Boolean"); + typeMapping.put("integer", "kotlin.Int"); + typeMapping.put("long", "kotlin.Long"); + typeMapping.put("float", "kotlin.Float"); + typeMapping.put("double", "kotlin.Double"); + typeMapping.put("number", "java.math.BigDecimal"); + + typeMapping.put("array", "kotlin.Array"); + typeMapping.put("list", "kotlin.collections.List"); + typeMapping.put("map", "kotlin.collections.Map"); + + typeMapping.put("ByteArray", "kotlin.ByteArray"); + typeMapping.put("file", "java.io.File"); + typeMapping.put("binary", "kotlin.ByteArray"); + + typeMapping.put("date-time", "java.time.LocalDateTime"); + typeMapping.put("date", "java.time.LocalDate"); + typeMapping.put("Date", "java.time.LocalDate"); + typeMapping.put("DateTime", "java.time.LocalDateTime"); + } + + private void initInstantiationTypes() { + instantiationTypes.put("array", "kotlin.arrayOf"); + instantiationTypes.put("list", "kotlin.arrayOf"); + instantiationTypes.put("map", "kotlin.mapOf"); + } + + private void initImportMapping() { + importMapping = new HashMap<>(); + importMapping.put("BigDecimal", "java.math.BigDecimal"); + importMapping.put("UUID", "java.util.UUID"); + importMapping.put("URI", "java.net.URI"); + 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"); + } + + private void initReservedWords() { + // this includes hard reserved words defined by https://github.com/JetBrains/kotlin/blob/master/core/descriptors/src/org/jetbrains/kotlin/renderer/KeywordStringsGenerated.java + // DOES NOT contain keywords from https://kotlinlang.org/docs/reference/keyword-reference.html + // because 'val data: String' is valid, but not 'val fun: String' + // also valid: 'val override: Int = 2' + reservedWords = new HashSet<>(Arrays.asList( + "package", + "as", + "typealias", + "class", + "this", + "super", + "val", + "var", + "fun", + "for", + "null", + "true", + "false", + "is", + "in", + "throw", + "return", + "break", + "continue", + "object", + "if", + "try", + "else", + "while", + "do", + "when", + "interface", + "typeof" + )); + } + + private void initCliOptions() { + cliOptions.clear(); + addOption(CodegenConstants.SOURCE_FOLDER, CodegenConstants.SOURCE_FOLDER_DESC, sourceFolder); + addOption(CodegenConstants.PACKAGE_NAME, "Generated artifact package name.", packageName); + addOption(CodegenConstants.API_SUFFIX, CodegenConstants.API_SUFFIX_DESC, apiSuffix); + 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())); + cliOptions.add(new CliOption(CodegenConstants.PARCELIZE_MODELS, CodegenConstants.PARCELIZE_MODELS_DESC)); + CliOption dateLibrary = new CliOption(DATE_LIBRARY, "Option. Date library to use"); Map dateOptions = new HashMap<>(); dateOptions.put(DateLibrary.THREETENBP.value, "Threetenbp"); @@ -116,53 +271,126 @@ public KotlinRetrofitCodegen() { collectionType.setEnum(collectionOptions); collectionType.setDefault(this.collectionType); cliOptions.add(collectionType); - - additionalProperties.put("enumcase", new EnumCaseLambda()); - CamelCaseLambda camelCase = new CamelCaseLambda(); - camelCase.generator(this); - camelCase.escapeAsParamName(true); - additionalProperties.put("camelcase", camelCase); } + @Override public CodegenType getTag() { return CodegenType.CLIENT; } + @Override public String getName() { return "kotlin-retrofit"; } + @Override public String getHelp() { return "Generates an android library module. Uses Kotlin, Retrofit and Coroutines."; } - public void setDateLibrary(String library) { - this.dateLibrary = library; + @Override + public String apiFileFolder() { + return (outputFolder + File.separator + sourceFolder + File.separator + apiPackage().replace('.', File.separatorChar)).replace('/', File.separatorChar); } - public void setCollectionType(String collectionType) { - this.collectionType = collectionType; + @Override + public String modelFileFolder() { + return outputFolder + File.separator + sourceFolder + File.separator + modelPackage().replace('.', File.separatorChar); } @Override - public Map postProcessOperationsWithModels(Map objs, List allModels) { - super.postProcessOperationsWithModels(objs, allModels); - Map operations = (Map) objs.get("operations"); - if (operations == null) return objs; + public String escapeQuotationMark(String input) { + return input.replace("\"", "\\\""); + } - List ops = (List) operations.get("operation"); - for (CodegenOperation operation : ops) { - if (StringUtils.isNotEmpty(operation.path) && operation.path.startsWith("/")) { - operation.path = operation.path.substring(1); - } - } - return objs; + @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(Locale.ROOT, "`%s`", name); + } + + @Override + public String escapeUnsafeCharacters(String input) { + return input.replace("*/", "*_/").replace("/*", "/_*"); } + // + @Override public void processOpts() { super.processOpts(); + if (StringUtils.isEmpty(System.getenv("KOTLIN_POST_PROCESS_FILE"))) { + LOGGER.info("Environment variable KOTLIN_POST_PROCESS_FILE not defined so the Kotlin code may not be properly formatted. To define it, try 'export KOTLIN_POST_PROCESS_FILE=\"/usr/local/bin/ktlint -F\"' (Linux/Mac)"); + LOGGER.info("NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI)."); + } + + 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.API_SUFFIX)) { + this.setApiSuffix((String) additionalProperties.get(CodegenConstants.API_SUFFIX)); + } + + 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 + "."); + } + + if (additionalProperties.containsKey(CodegenConstants.PARCELIZE_MODELS)) { + this.setParcelizeModels(Boolean.valueOf((String) additionalProperties.get(CodegenConstants.PARCELIZE_MODELS))); + LOGGER.info(CodegenConstants.PARCELIZE_MODELS + " depends on the android framework and " + + "experimental parcelize feature. Make sure your build applies the android plugin:\n" + + "apply plugin: 'com.android.library' OR apply plugin: 'com.android.application'.\n" + + "and enables the experimental features:\n" + + "androidExtensions {\n" + + " experimental = true\n" + + "}" + ); + } else { + additionalProperties.put(CodegenConstants.PARCELIZE_MODELS, parcelizeModels); + } + + additionalProperties.put(CodegenConstants.API_PACKAGE, apiPackage()); + additionalProperties.put(CodegenConstants.MODEL_PACKAGE, modelPackage()); + + additionalProperties.put("apiDocPath", apiDocPath); + additionalProperties.put("modelDocPath", modelDocPath); + if (additionalProperties.containsKey(DATE_LIBRARY)) { setDateLibrary(additionalProperties.get(DATE_LIBRARY).toString()); } @@ -206,4 +434,509 @@ public void processOpts() { supportingFiles.add(new SupportingFile("AndroidManifest.xml.mustache", "src/main", "AndroidManifest.xml")); } + + // + + // + + private String titleCase(final String input) { + return input.substring(0, 1).toUpperCase(Locale.ROOT) + input.substring(1); + } + + /** + * Sanitize against Kotlin specific naming conventions, which may differ from those required by {@link DefaultCodegen#sanitizeName}. + * + * @param name string to be sanitize + * @return sanitized string + */ + private String sanitizeKotlinSpecificNames(final String name) { + String word = name; + for (Map.Entry specialCharacters : specialCharReplacements.entrySet()) { + word = replaceSpecialCharacters(word, specialCharacters); + } + + // 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; + } + + @Override + public String toInstantiationType(Schema p) { + if (ModelUtils.isArraySchema(p)) { + return getArrayTypeDeclaration((ArraySchema) p); + } + return super.toInstantiationType(p); + } + + @Override + public String toApiName(String name) { + if (name.length() == 0) { + return "DefaultApi"; + } + return (this.apiSuffix.isEmpty() ? camelize(name) : camelize(name) + this.apiSuffix); + } + + @Override + public String toModelImport(String name) { + // toModelImport is called while processing operations, but DefaultCodegen doesn't + // define imports correctly with fully qualified primitives and models as defined in this generator. + if (needToImport(name)) { + return super.toModelImport(name); + } + + return name; + } + + @Override + public 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("\\.", ""); + + String nameWithPrefixSuffix = sanitizeKotlinSpecificNames(modifiedName); + if (!StringUtils.isEmpty(modelNamePrefix)) { + // add '_' so that model name can be camelized correctly + nameWithPrefixSuffix = modelNamePrefix + "_" + nameWithPrefixSuffix; + } + + if (!StringUtils.isEmpty(modelNameSuffix)) { + // add '_' so that model name can be camelized correctly + nameWithPrefixSuffix = nameWithPrefixSuffix + "_" + modelNameSuffix; + } + + // Camelize name of nested properties + modifiedName = camelize(nameWithPrefixSuffix); + + // model name cannot use reserved keyword, e.g. return + if (isReservedWord(modifiedName)) { + final String modelName = "Model" + modifiedName; + LOGGER.warn(modifiedName + " (reserved word) cannot be used as model name. Renamed to " + modelName); + return modelName; + } + + // model name starts with number + if (modifiedName.matches("^\\d.*")) { + final String modelName = "Model" + modifiedName; // e.g. 200Response => Model200Response (after camelize) + LOGGER.warn(name + " (model name starts with number) cannot be used as model name. Renamed to " + modelName); + return modelName; + } + + return titleCase(modifiedName); + } + + @Override + public String toOperationId(String operationId) { + // throw exception if method name is empty + if (StringUtils.isEmpty(operationId)) + throw new RuntimeException("Empty method/operation name (operationId) not allowed"); + + operationId = camelize(sanitizeName(operationId), true); + + // method name cannot use reserved keyword, e.g. return + if (isReservedWord(operationId)) { + String newOperationId = camelize("call_" + operationId, true); + LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + newOperationId); + return newOperationId; + } + + // operationId starts with a number + if (operationId.matches("^\\d.*")) { + LOGGER.warn(operationId + " (starting with a number) cannot be used as method name. Renamed to " + camelize("call_" + operationId), true); + operationId = camelize("call_" + operationId, true); + } + + return operationId; + } + + @Override + public String toModelFilename(String name) { + // Should be the same as the model name + return toModelName(name); + } + + @Override + public String toEnumValue(String value, String datatype) { + if ("kotlin.Int".equals(datatype) || "kotlin.Long".equals(datatype)) { + return value; + } else if ("kotlin.Double".equals(datatype)) { + if (value.contains(".")) { + return value; + } else { + return value + ".0"; // Float and double must have .0 + } + } else if ("kotlin.Float".equals(datatype)) { + return value + "f"; + } else { + return "\"" + escapeText(value) + "\""; + } + } + + @Override + public String toEnumVarName(String value, String datatype) { + String modified; + if (value.length() == 0) { + modified = "EMPTY"; + } else { + modified = value; + modified = sanitizeKotlinSpecificNames(modified); + } + + switch (enumPropertyNaming) { + 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(Locale.ROOT); + break; + } + + if (reservedWords.contains(modified)) { + return escapeReservedWord(modified); + } + // NOTE: another sanitize because camelize can create an invalid name + return sanitizeKotlinSpecificNames(modified); + } + + @Override + public String toParamName(String name) { + // to avoid conflicts with 'callback' parameter for async call + if ("callback".equals(name)) { + return "paramCallback"; + } + + // should be the same as variable name + return toVarName(name); + } + + @Override + public String toVarName(String name) { + // sanitize name + name = sanitizeKotlinSpecificNames(name); + name = sanitizeName(name, "\\W-[\\$]"); + + if (name.toLowerCase(Locale.ROOT).matches("^_*class$")) { + return "propertyClass"; + } + + if ("_".equals(name)) { + name = "_u"; + } + + // if it's all uppper case, do nothing + if (name.matches("^[A-Z0-9_]*$")) { + return name; + } + + if (startsWithTwoUppercaseLetters(name)) { + name = name.substring(0, 2).toLowerCase(Locale.ROOT) + name.substring(2); + } + + // If name contains special chars -> replace them. + if ((name.chars().anyMatch(character -> specialCharReplacements.keySet().contains("" + ((char) character))))) { + List allowedCharacters = new ArrayList<>(); + allowedCharacters.add("_"); + allowedCharacters.add("$"); + name = escape(name, specialCharReplacements, allowedCharacters, "_"); + } + + // camelize (lower first character) the variable name + // pet_id => petId + name = camelize(name, true); + + // for reserved word or word starting with number or containing dollar symbol, escape it + if (isReservedWord(name) || name.matches("(^\\d.*)|(.*[$].*)")) { + name = escapeReservedWord(name); + } + + return name; + } + + @Override + public String toRegularExpression(String pattern) { + return escapeText(pattern); + } + + @Override + public String toDefaultValue(Schema p) { + if (ModelUtils.isBooleanSchema(p)) { + if (p.getDefault() != null) { + return p.getDefault().toString(); + } + } else if (ModelUtils.isDateSchema(p)) { + // TODO + } else if (ModelUtils.isDateTimeSchema(p)) { + // TODO + } else if (ModelUtils.isNumberSchema(p)) { + if (p.getDefault() != null) { + return p.getDefault().toString(); + } + } else if (ModelUtils.isIntegerSchema(p)) { + if (p.getDefault() != null) { + return p.getDefault().toString(); + } + } else if (ModelUtils.isStringSchema(p)) { + if (p.getDefault() != null) { + return "\"" + p.getDefault() + "\""; + } + } + + return null; + } + + // + + // + + @Override + public String getSchemaType(Schema p) { + String openAPIType = super.getSchemaType(p); + String type; + // This maps, for example, long -> kotlin.Long based on hashes in this type's constructor + if (typeMapping.containsKey(openAPIType)) { + type = typeMapping.get(openAPIType); + if (languageSpecificPrimitives.contains(type)) { + return toModelName(type); + } + } else { + type = openAPIType; + } + return toModelName(type); + } + + @Override + public String getTypeDeclaration(Schema p) { + if (ModelUtils.isArraySchema(p)) { + return getArrayTypeDeclaration((ArraySchema) p); + } else if (ModelUtils.isMapSchema(p)) { + Schema inner = ModelUtils.getAdditionalProperties(p); + + // Maps will be keyed only by primitive Kotlin string + return getSchemaType(p) + ""; + } + return super.getTypeDeclaration(p); + } + + /** + * Provides a strongly typed declaration for simple arrays of some type and arrays of arrays of some type. + * + * @param arr Array schema + * @return type declaration of array + */ + private String getArrayTypeDeclaration(ArraySchema arr) { + // TODO: collection type here should be fully qualified namespace to avoid model conflicts + // This supports arrays of arrays. + String arrayType = typeMapping.get("array"); + StringBuilder instantiationType = new StringBuilder(arrayType); + Schema items = arr.getItems(); + String nestedType = getTypeDeclaration(items); + // TODO: We may want to differentiate here between generics and primitive arrays. + instantiationType.append("<").append(nestedType).append(">"); + return instantiationType.toString(); + } + + // + + // + + 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 setApiSuffix(String apiSuffix) { + this.apiSuffix = apiSuffix; + } + + public void setSourceFolder(String sourceFolder) { + this.sourceFolder = sourceFolder; + } + + public void setParcelizeModels(Boolean parcelizeModels) { + this.parcelizeModels = parcelizeModels; + } + + /** + * Sets the naming convention for Kotlin enum properties + * + * @param enumPropertyNamingType The string representation of the naming convention, as defined by {@link org.openapitools.codegen.CodegenConstants.ENUM_PROPERTY_NAMING_TYPE} + */ + public void setEnumPropertyNaming(final String enumPropertyNamingType) { + try { + this.enumPropertyNaming = CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.valueOf(enumPropertyNamingType); + } catch (IllegalArgumentException ex) { + StringBuilder sb = new StringBuilder(enumPropertyNamingType + " is an invalid enum property naming option. Please choose from:"); + for (CodegenConstants.ENUM_PROPERTY_NAMING_TYPE t : CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.values()) { + sb.append("\n ").append(t.name()); + } + throw new RuntimeException(sb.toString()); + } + } + + public void setDateLibrary(String library) { + dateLibrary = library; + } + + public void setCollectionType(String collectionType) { + this.collectionType = collectionType; + } + + // + + // + + @Override + public boolean isDataTypeString(final String dataType) { + return "String".equals(dataType) || "kotlin.String".equals(dataType); + } + + @Override + protected boolean isReservedWord(String word) { + // We want case-sensitive escaping, to avoid unnecessary backtick-escaping. + return reservedWords.contains(word); + } + + @Override + protected boolean needToImport(String type) { + // provides extra protection against improperly trying to import language primitives and java types + return !type.startsWith("kotlin.") && + !type.startsWith("java.") && + !defaultIncludes.contains(type) && + !languageSpecificPrimitives.contains(type); + } + + private boolean startsWithTwoUppercaseLetters(String name) { + boolean startsWithTwoUppercaseLetters = false; + if (name.length() > 1) { + startsWithTwoUppercaseLetters = name.substring(0, 2).equals(name.substring(0, 2).toUpperCase(Locale.ROOT)); + } + return startsWithTwoUppercaseLetters; + } + + private String recurseOnEndOfWord(String word, String oldValue, String newValue, int lastReplacedValue) { + String end = word.substring(lastReplacedValue + 1); + if (!end.isEmpty()) { + end = titleCase(end); + end = replaceCharacters(end, oldValue, newValue); + } + return end; + } + + private String replaceCharacters(String word, String oldValue, String newValue) { + if (!word.contains(oldValue)) { + return word; + } + if (word.equals(oldValue)) { + return newValue; + } + int i = word.indexOf(oldValue); + String start = word.substring(0, i); + String end = recurseOnEndOfWord(word, oldValue, newValue, i); + return start + newValue + end; + } + + private String replaceSpecialCharacters(String word, Map.Entry specialCharacters) { + String specialChar = specialCharacters.getKey(); + String replacementChar = specialCharacters.getValue(); + // Underscore is the only special character we'll allow + if (!specialChar.equals("_") && word.contains(specialChar)) { + return replaceCharacters(word, specialChar, replacementChar); + } + return word; + } + + // + + @Override + public void postProcessFile(File file, String fileType) { + if (file == null) { + return; + } + + String kotlinPostProcessFile = System.getenv("KOTLIN_POST_PROCESS_FILE"); + if (StringUtils.isEmpty(kotlinPostProcessFile)) { + return; // skip if KOTLIN_POST_PROCESS_FILE env variable is not defined + } + + // only process files with kt extension + if ("kt".equals(FilenameUtils.getExtension(file.toString()))) { + String command = kotlinPostProcessFile + " " + file.toString(); + try { + Process p = Runtime.getRuntime().exec(command); + p.waitFor(); + int exitValue = p.exitValue(); + if (exitValue != 0) { + LOGGER.error("Error running the command ({}). Exit value: {}", command, exitValue); + } else { + LOGGER.info("Successfully executed: " + command); + } + } catch (Exception e) { + LOGGER.error("Error running the command ({}). Exception: {}", command, e.getMessage()); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public Map postProcessOperationsWithModels(Map objs, List allModels) { + super.postProcessOperationsWithModels(objs, allModels); + Map operations = (Map) objs.get("operations"); + if (operations == null) return objs; + + List ops = (List) operations.get("operation"); + for (CodegenOperation operation : ops) { + if (StringUtils.isNotEmpty(operation.path) && operation.path.startsWith("/")) { + operation.path = operation.path.substring(1); + } + } + return objs; + } + + @Override + public Map postProcessModels(Map objs) { + Map map = super.postProcessModels(objs); + postProcessModelsEnum(map); + return map; + } } diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache index 0c7cfdb75dc5..d3f92464de1f 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache @@ -39,9 +39,9 @@ object RetrofitHolder { val retrofit: Retrofit by lazy { retrofitBuilder.build() } +{{#authMethods}}{{#-first}} private val securityDefinitions = HashMap() -{{#authMethods}}{{#-first}} fun setApiKey(authMethod: AuthMethod, apiKey: String) { securityDefinitions[authMethod.authName] = apiKey } diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/api.mustache index 316d5e6e7e6c..f99d554416ac 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/api.mustache @@ -23,7 +23,7 @@ interface {{classname}} { * - {{code}}: {{{message}}}{{/responses}} * {{#allParams}}* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}} - {{/allParams}}* @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} + {{/allParams}}* @return {{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}void{{/returnType}} */ @{{httpMethod}}("{{{path}}}") {{#isDeprecated}} @@ -47,7 +47,7 @@ interface {{classname}} { suspend fun {{operationId}}( {{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{#hasMore}}, {{/hasMore}}{{/allParams}} - ): Response<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}> + ): Response<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}> {{/operation}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/bodyParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/bodyParams.mustache index 079fbb1169b5..1d67aed02097 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/bodyParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/bodyParams.mustache @@ -1 +1 @@ -{{#isBodyParam}}@Body {{paramName}}: {{{dataType}}}{{/isBodyParam}} \ No newline at end of file +{{#isBodyParam}}@Body {{paramName}}: {{{dataType}}}{{#required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isBodyParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache index fa62ee50495e..0a1c8260ca37 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache @@ -18,11 +18,8 @@ import kotlinx.android.parcel.Parcelize data class {{classname}} ( {{#requiredVars}} {{>data_class_req_var}}{{^-last}}, - {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, - {{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>data_class_opt_var}}{{^-last}}, - {{/-last}}{{/optionalVars}} ){{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{#hasEnums}} { {{#vars}}{{#isEnum}} @@ -33,7 +30,7 @@ data class {{classname}} ( enum class {{nameInCamelCase}}(val value: {{dataType}}){ {{#allowableValues}}{{#enumVars}} @Json(name = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) - {{^isString}}{{nameInCamelCase}}{{/isString}}{{#enumcase}}{{&name}}{{/enumcase}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} + {{^isString}}{{nameInCamelCase}}{{/isString}}{{&name}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} {{/enumVars}}{{/allowableValues}} } {{/isEnum}}{{/vars}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache index 09e74d90c394..03fbf03af294 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache @@ -1 +1 @@ - @Json(name = "{{{baseName}}}") val {{#title}}{{#camelcase}}{{title}}{{/camelcase}}{{/title}}{{^title}}{{#camelcase}}{{name}}{{/camelcase}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{#defaultValue}}{{#isEnum}}{{classname}}.{{nameInCamelCase}}.{{#enumcase}}{{{defaultValue}}}{{/enumcase}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}} \ No newline at end of file + @Json(name = "{{{baseName}}}") val {{#title}}{{.}}{{/title}}{{^title}}{{name}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{#defaultValue}}{{#isEnum}}{{classname}}.{{nameInCamelCase}}.{{{defaultValue}}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache index 41e57e7df7c3..876e58cfc54a 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache @@ -1 +1 @@ - @Json(name = "{{{baseName}}}") val {{#title}}{{#camelcase}}{{title}}{{/camelcase}}{{/title}}{{^title}}{{#camelcase}}{{name}}{{/camelcase}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#defaultValue}} = {{#isEnum}}{{classname}}.{{nameInCamelCase}}.{{#enumcase}}{{{defaultValue}}}{{/enumcase}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}} \ No newline at end of file + @Json(name = "{{{baseName}}}") val {{#title}}{{.}}{{/title}}{{^title}}{{name}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#defaultValue}} = {{#isEnum}}{{classname}}.{{nameInCamelCase}}.{{{defaultValue}}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache index a6b311fa2b7f..315251a55901 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache @@ -7,7 +7,6 @@ import com.squareup.moshi.Json enum class {{classname}}(val value: {{dataType}}){ {{#allowableValues}}{{#enumVars}} @Json(name = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) - {{^isString}}{{nameInCamelCase}}{{/isString}}{{#enumcase}}{{&name}}{{/enumcase}}({{{value}}}){{^-last}}, - {{/-last}}{{#-last}};{{/-last}} -{{/enumVars}}{{/allowableValues}} + {{^isString}}{{nameInCamelCase}}{{/isString}}{{&name}}({{{value}}}){{^-last}}, + {{/-last}}{{#-last}};{{/-last}}{{/enumVars}}{{/allowableValues}} } diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/formParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/formParams.mustache index d3c86c3e8870..f2ca0d380155 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/formParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/formParams.mustache @@ -1 +1 @@ -{{#isFormParam}}{{^isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field{{/isMultipart}}("{{baseName}}") {{paramName}}: {{{dataType}}}{{/isFile}}{{#isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field("{{baseName}}"){{/isMultipart}} {{paramName}}: MultipartBody.Part{{/isFile}}{{/isFormParam}} \ No newline at end of file +{{#isFormParam}}{{^isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field{{/isMultipart}}("{{baseName}}") {{paramName}}: {{{dataType}}}{{/isFile}}{{#isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field("{{baseName}}"){{/isMultipart}} {{paramName}}: MultipartBody.Part{{/isFile}}{{#required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isFormParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/headerParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/headerParams.mustache index 2b26c446678c..d4b64844142a 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/headerParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/headerParams.mustache @@ -1 +1 @@ -{{#isHeaderParam}}@Header("{{baseName}}") {{paramName}}: {{{dataType}}}{{/isHeaderParam}} \ No newline at end of file +{{#isHeaderParam}}@Header("{{baseName}}") {{paramName}}: {{{dataType}}}{{#required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isHeaderParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/pathParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/pathParams.mustache index 996a45473a95..db9bacb3a792 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/pathParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/pathParams.mustache @@ -1 +1 @@ -{{#isPathParam}}@Path("{{baseName}}") {{paramName}}: {{{dataType}}}{{/isPathParam}} \ No newline at end of file +{{#isPathParam}}@Path("{{baseName}}") {{paramName}}: {{{dataType}}}{{#required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isPathParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/queryParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/queryParams.mustache index 14c40f05e588..9f8f490adaf6 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/queryParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/queryParams.mustache @@ -1 +1 @@ -{{#isQueryParam}}@Query("{{baseName}}") {{paramName}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}{{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{{dataType}}}{{/collectionFormat}}{{/isQueryParam}} \ No newline at end of file +{{#isQueryParam}}@Query("{{baseName}}") {{paramName}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}{{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{{dataType}}}{{/collectionFormat}}{{#required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isQueryParam}} \ No newline at end of file From 1e36f85a63ea67c90d6d908362749e1a804c823b Mon Sep 17 00:00:00 2001 From: kroegerama <1519044+kroegerama@users.noreply.github.com> Date: Sun, 16 Jun 2019 22:14:27 +0200 Subject: [PATCH 03/11] add support for allOf (inheritance) --- .../languages/KotlinRetrofitCodegen.java | 37 +++++++++++++++++++ .../resources/kotlin-retrofit/README.mustache | 1 + .../kotlin-retrofit/data_class.mustache | 2 +- .../data_class_opt_var.mustache | 3 +- .../data_class_req_var.mustache | 3 +- .../kotlin-retrofit/iface_class.mustache | 23 ++++++++++++ .../resources/kotlin-retrofit/model.mustache | 3 +- 7 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/iface_class.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java index 904463c5cb67..560d9bf51a96 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java @@ -628,6 +628,11 @@ public String toEnumVarName(String value, String datatype) { return sanitizeKotlinSpecificNames(modified); } + @Override + public String toEnumDefaultValue(String value, String datatype) { + return value; + } + @Override public String toParamName(String name) { // to avoid conflicts with 'callback' parameter for async call @@ -933,6 +938,38 @@ public Map postProcessOperationsWithModels(Map o return objs; } + @SuppressWarnings("unchecked") + @Override + public Map postProcessAllModels(Map objs) { + Map result = super.postProcessAllModels(objs); + Set ifaces = new HashSet<>(); + + for (Map.Entry entry : result.entrySet()) { + Map map = (Map) entry.getValue(); + List> models = (List>) map.get("models"); + for (Map model : models) { + CodegenModel cm = (CodegenModel) model.get("model"); + if (cm.interfaces != null && cm.interfaces.size() > 0) { + ifaces.addAll(cm.interfaces); + model.put("hasInterfaces", true); + } + } + } + + for (Map.Entry entry : result.entrySet()) { + Map map = (Map) entry.getValue(); + List> models = (List>) map.get("models"); + for (Map model : models) { + CodegenModel cm = (CodegenModel) model.get("model"); + if (ifaces.contains(cm.classFilename)) { + model.put("isInterface", true); + } + } + } + + return result; + } + @Override public Map postProcessModels(Map objs) { Map map = super.postProcessModels(objs); diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/README.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/README.mustache index 8d527ec691fc..61b9040a3f37 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/README.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/README.mustache @@ -19,6 +19,7 @@ Just add the generated folder to your Android Studio project. * fix api.mustache: double @Header generation possible * add better support for json schema inheritance (allOf, anyOf, etc.) +** allOf: done * add OAuth functionality * update proguard/p8 rules for okhttp/moshi * option to switch coroutine generation on/off diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache index 0a1c8260ca37..caa6b1a66409 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache @@ -21,7 +21,7 @@ data class {{classname}} ( {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, {{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>data_class_opt_var}}{{^-last}}, {{/-last}}{{/optionalVars}} -){{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{#hasEnums}} { +){{#parcelizeModels}} : Parcelable{{#hasInterfaces}}, {{/hasInterfaces}}{{/parcelizeModels}}{{#interfaces}}{{#-first}}{{^parcelizeModels}} : {{/parcelizeModels}}{{/-first}}{{{.}}}{{^-last}}, {{/-last}}{{/interfaces}}{{#hasEnums}} { {{#vars}}{{#isEnum}} /** * {{{description}}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache index 03fbf03af294..be27ac95b840 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_opt_var.mustache @@ -1 +1,2 @@ - @Json(name = "{{{baseName}}}") val {{#title}}{{.}}{{/title}}{{^title}}{{name}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{#defaultValue}}{{#isEnum}}{{classname}}.{{nameInCamelCase}}.{{{defaultValue}}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}} \ No newline at end of file + @Json(name = "{{{baseName}}}") + {{#hasInterfaces}}override {{/hasInterfaces}}val {{#title}}{{.}}{{/title}}{{^title}}{{name}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{#defaultValue}}{{#isEnum}}{{classname}}.{{nameInCamelCase}}.{{{defaultValue}}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache index 876e58cfc54a..2148c2f9d3a1 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class_req_var.mustache @@ -1 +1,2 @@ - @Json(name = "{{{baseName}}}") val {{#title}}{{.}}{{/title}}{{^title}}{{name}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#defaultValue}} = {{#isEnum}}{{classname}}.{{nameInCamelCase}}.{{{defaultValue}}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}} \ No newline at end of file + @Json(name = "{{{baseName}}}") + {{#hasInterfaces}}override {{/hasInterfaces}}val {{#title}}{{.}}{{/title}}{{^title}}{{name}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#defaultValue}} = {{#isEnum}}{{classname}}.{{nameInCamelCase}}.{{{defaultValue}}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/iface_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/iface_class.mustache new file mode 100644 index 000000000000..155f2d99847f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/iface_class.mustache @@ -0,0 +1,23 @@ +/** + * {{{description}}} +{{#vars}} + * @param {{name}} {{{description}}} +{{/vars}} + */ +interface {{classname}} { + {{#requiredVars}}val {{#title}}{{.}}{{/title}}{{^title}}{{name}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^-last}} + {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}} + {{/hasOptional}}{{/hasRequired}}{{#optionalVars}}val {{#title}}{{.}}{{/title}}{{^title}}{{name}}{{/title}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}?{{^-last}} + {{/-last}}{{/optionalVars}} + + {{#hasEnums}} { + {{#vars}}{{#isEnum}} + enum class {{nameInCamelCase}}(val value: {{dataType}}){ + {{#allowableValues}}{{#enumVars}} + @Json(name = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) + {{^isString}}{{nameInCamelCase}}{{/isString}}{{&name}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} + {{/enumVars}}{{/allowableValues}} + } + {{/isEnum}}{{/vars}} + }{{/hasEnums}} +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/model.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/model.mustache index b9514dad4d7b..0196e0e65b90 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/model.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/model.mustache @@ -6,6 +6,7 @@ package {{modelPackage}} {{#models}} {{#model}} -{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{#isAlias}}typealias {{classname}} = {{dataType}}{{/isAlias}}{{^isAlias}}{{>data_class}}{{/isAlias}}{{/isEnum}} +{{#isEnum}}{{>enum_class}}{{/isEnum}} +{{^isEnum}}{{#isAlias}}typealias {{classname}} = {{dataType}}{{/isAlias}}{{^isAlias}}{{#isInterface}}{{>iface_class}}{{/isInterface}}{{^isInterface}}{{>data_class}}{{/isInterface}}{{/isAlias}}{{/isEnum}} {{/model}} {{/models}} From f265fac971d2e735fbf1e2c959307e825a37f3b9 Mon Sep 17 00:00:00 2001 From: kroegerama <1519044+kroegerama@users.noreply.github.com> Date: Sat, 29 Jun 2019 22:31:32 +0200 Subject: [PATCH 04/11] feedback from @wing328 --- .../languages/KotlinRetrofitCodegen.java | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java index 560d9bf51a96..eb0c2913a800 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java @@ -1,7 +1,5 @@ /* * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) - * Copyright 2018 SmartBear Software - * Copyright 2019 kroegerama * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,19 +33,6 @@ public class KotlinRetrofitCodegen extends DefaultCodegen implements CodegenConfig { private static final Logger LOGGER = LoggerFactory.getLogger(KotlinRetrofitCodegen.class); -// private class EnumCaseLambda implements Mustache.Lambda { -// @Override -// public void execute(Template.Fragment fragment, Writer writer) throws IOException { -// String text = fragment.execute(); -// text = camelize(text); -// text = toVarName(text); -// if (text.length() > 0) { -// text = text.substring(0, 1).toUpperCase(Locale.ROOT) + text.substring(1); -// } -// writer.write(text); -// } -// } - public static final String DATE_LIBRARY = "dateLibrary"; public static final String COLLECTION_TYPE = "collectionType"; @@ -115,12 +100,6 @@ public KotlinRetrofitCodegen() { apiPackage = packageName + ".apis"; modelPackage = packageName + ".models"; -// additionalProperties.put("enumcase", new EnumCaseLambda()); -// CamelCaseLambda camelCase = new CamelCaseLambda(); -// camelCase.generator(this); -// camelCase.escapeAsParamName(true); -// additionalProperties.put("camelcase", camelCase); - initCliOptions(); } From 76ad145f198467ed160e55950abcf6f6596b2372 Mon Sep 17 00:00:00 2001 From: kroegerama <1519044+kroegerama@users.noreply.github.com> Date: Fri, 5 Jul 2019 14:34:31 +0200 Subject: [PATCH 05/11] fix RetrofitHolder AuthMethods enum --- .../resources/kotlin-retrofit/RetrofitHolder.kt.mustache | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache index d3f92464de1f..c43a37961b2d 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache @@ -46,6 +46,10 @@ object RetrofitHolder { securityDefinitions[authMethod.authName] = apiKey } + fun removeAuthInfo(authMethod: AuthMethod) { + securityDefinitions -= authMethod.authName + } + fun setBasicAuth(authMethod: AuthMethod, username: String, password: String) { securityDefinitions[authMethod.authName] = Credentials.basic(username, password) } @@ -71,6 +75,6 @@ object RetrofitHolder { } {{#authMethods}}{{#-first}}enum class AuthMethod(internal val authName: String) { {{/-first}} - {{#enumcase}}{{{name}}}{{/enumcase}}("{{{name}}}"){{^-last}},{{/-last}}{{#-last}} + {{{name}}}("{{{name}}}"){{^-last}},{{/-last}}{{#-last}} }{{/-last}} {{/authMethods}} \ No newline at end of file From 11888b8257f799bab6c159115104e5f355755e74 Mon Sep 17 00:00:00 2001 From: kroegerama <1519044+kroegerama@users.noreply.github.com> Date: Tue, 1 Oct 2019 15:02:20 +0200 Subject: [PATCH 06/11] update retrofit/okhttp version, improve build.gradle --- .../AuthInterceptors.kt.mustache | 2 +- .../RetrofitHolder.kt.mustache | 2 +- .../kotlin-retrofit/build.gradle.mustache | 29 ++++++++++++------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/AuthInterceptors.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/AuthInterceptors.kt.mustache index 577192b1adb7..e41ed1203002 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/AuthInterceptors.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/AuthInterceptors.kt.mustache @@ -42,7 +42,7 @@ class QueryParamInterceptor( ) : AuthInterceptor(authName, generator) { override fun handleApiKey(chain: Interceptor.Chain, apiKey: String): Response { - val newUrl = chain.request().url() + val newUrl = chain.request().url .newBuilder() .addQueryParameter(paramName, apiKey) .build() diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache index c43a37961b2d..fb9b82c44cd2 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache @@ -20,7 +20,7 @@ object RetrofitHolder { .build() chain.proceed(newRequest) }{{/-first}}{{/authMethods}} - .addNetworkInterceptor(HttpLoggingInterceptor().apply { + .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.HEADERS }){{#authMethods}} {{>auth_method}}{{/authMethods}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache index 1a857be85486..90456c1e262b 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache @@ -4,18 +4,18 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 21 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" } compileOptions { targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = "1.8" + } buildTypes { release { minifyEnabled false @@ -24,20 +24,27 @@ android { } } +ext { + retrofitVersion = "2.6.2" + moshiVersion = "1.8.0" + coroutinesVersion = "1.3.2" + okHttpVersion = "4.2.0" +} + dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'com.squareup.okhttp3:logging-interceptor:3.14.2' + implementation "com.squareup.okhttp3:logging-interceptor:$okHttpVersion" - api 'com.squareup.retrofit2:retrofit:2.6.0' + api "com.squareup.retrofit2:retrofit:$retrofitVersion" {{#threetenbp}} api 'com.jakewharton.threetenabp:threetenabp:1.2.1' {{/threetenbp}} - implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' - api 'com.squareup.moshi:moshi:1.8.0' - kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.8.0' + implementation "com.squareup.retrofit2:converter-moshi:$retrofitVersion" + api "com.squareup.moshi:moshi:$moshiVersion" + kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" } From 1d7567af34d4e3cf08e7cbdfc9f6db5aec4b9525 Mon Sep 17 00:00:00 2001 From: kroegerama <1519044+kroegerama@users.noreply.github.com> Date: Mon, 14 Oct 2019 18:04:09 +0200 Subject: [PATCH 07/11] improved enum handling (serialize/deserialize arbitrary values) --- .../languages/KotlinRetrofitCodegen.java | 3 +- .../kotlin-retrofit/EnumUtils.kt.mustache | 54 +++++++++++++++++++ .../RetrofitHolder.kt.mustache | 10 ++-- .../kotlin-retrofit/data_class.mustache | 2 +- .../kotlin-retrofit/enum_class.mustache | 2 +- 5 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/kotlin-retrofit/EnumUtils.kt.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java index eb0c2913a800..574bbe838b55 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinRetrofitCodegen.java @@ -50,7 +50,7 @@ public class KotlinRetrofitCodegen extends DefaultCodegen implements CodegenConf protected boolean parcelizeModels = false; protected String dateLibrary = DateLibrary.STRING.value; - protected String collectionType = CollectionType.ARRAY.value; + protected String collectionType = CollectionType.LIST.value; protected CodegenConstants.ENUM_PROPERTY_NAMING_TYPE enumPropertyNaming = CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.PascalCase; public enum DateLibrary { @@ -407,6 +407,7 @@ public void processOpts() { supportingFiles.add(new SupportingFile("AuthInterceptors.kt.mustache", infrastructureFolder, "AuthInterceptors.kt")); supportingFiles.add(new SupportingFile("RetrofitHolder.kt.mustache", infrastructureFolder, "RetrofitHolder.kt")); + supportingFiles.add(new SupportingFile("EnumUtils.kt.mustache", infrastructureFolder, "EnumUtils.kt")); supportingFiles.add(new SupportingFile("proguard-rules.pro.mustache", "", "proguard-rules.pro")); supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore")); diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/EnumUtils.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/EnumUtils.kt.mustache new file mode 100644 index 000000000000..126eb407283d --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/EnumUtils.kt.mustache @@ -0,0 +1,54 @@ +package {{packageName}} + +import com.squareup.moshi.* +import retrofit2.Converter +import retrofit2.Retrofit +import java.lang.reflect.Type + +interface EnumWithValue { + val value: T +} + +object EnumJsonAdapterFactory : JsonAdapter.Factory { + + override fun create(type: Type, annotations: MutableSet, moshi: Moshi): JsonAdapter<*>? { + if (type !is Class<*> || !type.isEnum) { + return null + } + + val constants = type.enumConstants?.mapNotNull { it as? EnumWithValue<*> } ?: return null + val first = constants.firstOrNull()?.value ?: return null + val valueAdapter = moshi.adapter(first::class.java) + + return object : JsonAdapter>() { + override fun fromJson(reader: JsonReader): EnumWithValue<*>? { + if (reader.peek() == JsonReader.Token.NULL) return reader.nextNull() + val value = valueAdapter.fromJson(reader) + return constants.firstOrNull { it.value == value } + ?: throw JsonDataException("Expected one of ${constants.map { it.value }} but was $value at path ${reader.path}") + } + + override fun toJson(writer: JsonWriter, value: EnumWithValue<*>?) { + if (value == null) { + writer.nullValue() + } else { + val innerValue = value.value + valueAdapter.toJson(writer, innerValue) + } + } + + } + } + +} + +object EnumRetrofitConverterFactory : Converter.Factory() { + override fun stringConverter(type: Type, annotations: Array, retrofit: Retrofit): Converter<*, String>? { + if (type is Class<*> && type.isEnum) { + return Converter, String> { value -> + value.javaClass.getField(value.name).getAnnotation(Json::class.java)?.name ?: value.name + } + } + return null + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache index fb9b82c44cd2..32fad1082a41 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache @@ -20,20 +20,22 @@ object RetrofitHolder { .build() chain.proceed(newRequest) }{{/-first}}{{/authMethods}} + {{#authMethods}} + {{>auth_method}}{{/authMethods}} .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.HEADERS - }){{#authMethods}} - {{>auth_method}}{{/authMethods}} + }) } val retrofitBuilder: Retrofit.Builder by lazy { val moshi = Moshi.Builder() - //.add(OffsetDateTimeAdapter()) + .add(EnumJsonAdapterFactory) .build() Retrofit.Builder() .client(clientBuilder.build()) .baseUrl(BASE_URL) + .addConverterFactory(EnumRetrofitConverterFactory) .addConverterFactory(MoshiConverterFactory.create(moshi)) } @@ -66,7 +68,7 @@ object RetrofitHolder { private fun requestHasAuth(request: Request, authName: String): Boolean { - val headers = request.headers(AUTH_NAME_HEADER) ?: return false + val headers = request.headers(AUTH_NAME_HEADER) return headers.contains(authName) } diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache index caa6b1a66409..d66d7850a267 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/data_class.mustache @@ -27,7 +27,7 @@ data class {{classname}} ( * {{{description}}} * Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} */ - enum class {{nameInCamelCase}}(val value: {{dataType}}){ + enum class {{nameInCamelCase}}(override val value: {{dataType}}) : {{packageName}}.EnumWithValue<{{dataType}}> { {{#allowableValues}}{{#enumVars}} @Json(name = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) {{^isString}}{{nameInCamelCase}}{{/isString}}{{&name}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache index 315251a55901..12c75ca28b77 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/enum_class.mustache @@ -4,7 +4,7 @@ import com.squareup.moshi.Json * {{{description}}} * Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} */ -enum class {{classname}}(val value: {{dataType}}){ +enum class {{classname}}(override val value: {{dataType}}) : {{packageName}}.EnumWithValue<{{dataType}}> { {{#allowableValues}}{{#enumVars}} @Json(name = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) {{^isString}}{{nameInCamelCase}}{{/isString}}{{&name}}({{{value}}}){{^-last}}, From 4db020631a669db8b134965f8c873289f8d2de5b Mon Sep 17 00:00:00 2001 From: kroegerama <1519044+kroegerama@users.noreply.github.com> Date: Thu, 7 Nov 2019 14:49:09 +0100 Subject: [PATCH 08/11] improve performance, hide debug info * add retrofit scalars converter for simple types * only add HttpLoggingInterceptor in Debug builds * initialize OkHttpClient lazy on non-main thread (see https://www.zacsweers.dev/dagger-party-tricks-deferred-okhttp-init/) --- .../RetrofitHolder.kt.mustache | 20 +++++++++++++++---- .../kotlin-retrofit/auth_method.mustache | 2 +- .../kotlin-retrofit/build.gradle.mustache | 1 + 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache index 32fad1082a41..79e1411a836b 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache @@ -1,12 +1,14 @@ package {{packageName}} import com.squareup.moshi.Moshi +import okhttp3.Call import okhttp3.Credentials import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.converter.scalars.ScalarsConverterFactory object RetrofitHolder { @@ -22,9 +24,13 @@ object RetrofitHolder { }{{/-first}}{{/authMethods}} {{#authMethods}} {{>auth_method}}{{/authMethods}} - .addInterceptor(HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.HEADERS - }) + .apply { + if (BuildConfig.DEBUG) { + addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.HEADERS + }) + } + } } val retrofitBuilder: Retrofit.Builder by lazy { @@ -33,8 +39,14 @@ object RetrofitHolder { .build() Retrofit.Builder() - .client(clientBuilder.build()) + .callFactory(object : Call.Factory { + //create client lazy on demand in background thread + //see https://www.zacsweers.dev/dagger-party-tricks-deferred-okhttp-init/ + private val client by lazy { clientBuilder.build() } + override fun newCall(request: Request): Call = client.newCall(request) + }) .baseUrl(BASE_URL) + .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(EnumRetrofitConverterFactory) .addConverterFactory(MoshiConverterFactory.create(moshi)) } diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/auth_method.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/auth_method.mustache index 306f01a9b076..a68519aaf09c 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/auth_method.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/auth_method.mustache @@ -1 +1 @@ -{{#isKeyInHeader}}.addInterceptor(HeaderParamInterceptor("{{{name}}}", "{{{keyParamName}}}", this::apiKeyGenerator)){{/isKeyInHeader}}{{#isKeyInQuery}}.addInterceptor(QueryParamInterceptor("{{{name}}}", "{{{keyParamName}}}", this::apiKeyGenerator)){{/isKeyInQuery}}{{#isBasicBasic}}.addInterceptor(HeaderParamInterceptor("{{{name}}}", "Authorization", this::apiKeyGenerator)){{/isBasicBasic}}{{#isBasicBearer}}.addInterceptor(HeaderParamInterceptor("{{{name}}}", "Authorization", this::apiKeyGenerator)){{/isBasicBearer}} \ No newline at end of file +{{#isKeyInHeader}}.addInterceptor(HeaderParamInterceptor("{{{name}}}", "{{{keyParamName}}}", this::apiKeyGenerator)){{/isKeyInHeader}}{{#isKeyInQuery}}.addInterceptor(QueryParamInterceptor("{{{name}}}", "{{{keyParamName}}}", this::apiKeyGenerator)){{/isKeyInQuery}}{{#isBasicBasic}}.addInterceptor(HeaderParamInterceptor("{{{name}}}", "Authorization", this::apiKeyGenerator)){{/isBasicBasic}}{{#isBasicBearer}}.addInterceptor(HeaderParamInterceptor("{{{name}}}", "Authorization", this::apiKeyGenerator)){{/isBasicBearer}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache index 90456c1e262b..93dfe71993c2 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache @@ -42,6 +42,7 @@ dependencies { {{/threetenbp}} implementation "com.squareup.retrofit2:converter-moshi:$retrofitVersion" + implementation "com.squareup.retrofit2:converter-scalars:$retrofitVersion" api "com.squareup.moshi:moshi:$moshiVersion" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion" From 735ed1818f9aa91d9d491189781da6542784ece4 Mon Sep 17 00:00:00 2001 From: kroegerama <1519044+kroegerama@users.noreply.github.com> Date: Thu, 7 Nov 2019 15:17:24 +0100 Subject: [PATCH 09/11] update okHttp version --- .../src/main/resources/kotlin-retrofit/build.gradle.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache index 93dfe71993c2..2249954649ed 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache @@ -28,7 +28,7 @@ ext { retrofitVersion = "2.6.2" moshiVersion = "1.8.0" coroutinesVersion = "1.3.2" - okHttpVersion = "4.2.0" + okHttpVersion = "4.2.2" } dependencies { From 5cc00638de7c5c607a2a4f77a054c6cdb28a2173 Mon Sep 17 00:00:00 2001 From: kroegerama <1519044+kroegerama@users.noreply.github.com> Date: Tue, 12 Nov 2019 10:59:52 +0100 Subject: [PATCH 10/11] add api version constant to RetrofitHolder --- .../main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache index 79e1411a836b..b30bd007942b 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/RetrofitHolder.kt.mustache @@ -12,6 +12,7 @@ import retrofit2.converter.scalars.ScalarsConverterFactory object RetrofitHolder { + {{#version}}const val API_VERSION = "{{{version}}}"{{/version}} const val BASE_URL = "{{basePath}}/" val clientBuilder: OkHttpClient.Builder by lazy { From 55b443dd51e865dfb0f17b038a518c28fe94a915 Mon Sep 17 00:00:00 2001 From: kroegerama <1519044+kroegerama@users.noreply.github.com> Date: Thu, 14 Nov 2019 10:05:23 +0100 Subject: [PATCH 11/11] add versionName to build.gradle --- .../src/main/resources/kotlin-retrofit/build.gradle.mustache | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache b/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache index 2249954649ed..a2146825c679 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-retrofit/build.gradle.mustache @@ -8,6 +8,7 @@ android { defaultConfig { minSdkVersion 21 + {{#version}}versionName "{{{version}}}"{{/version}} } compileOptions { targetCompatibility = JavaVersion.VERSION_1_8