Skip to content

Commit 2128f28

Browse files
authored
Enhancement/kotlin/apiclient and auth (#6585)
* Add multiple auth methods to ApiClient. ApiClient more configurable * Fixed missing close tag for isMultipart in api.mustache * Generated samples ./bin/kotlin-client-all.sh * Generated samples./bin/openapi3/kotlin-client-petstore-all.sh * Use of better way to add supporting files based on conditions * Fixed missing check for retrofit generating auth files * Generated samples Co-authored-by: Alexander Karkossa <[email protected]>
1 parent 3b9cb14 commit 2128f28

File tree

131 files changed

+5872
-2544
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

131 files changed

+5872
-2544
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package org.openapitools.codegen.languages;
1919

20+
import org.apache.commons.lang3.StringUtils;
2021
import org.openapitools.codegen.CliOption;
2122
import org.openapitools.codegen.CodegenConstants;
2223
import org.openapitools.codegen.CodegenModel;
@@ -28,6 +29,7 @@
2829
import org.openapitools.codegen.meta.features.*;
2930
import org.slf4j.Logger;
3031
import org.slf4j.LoggerFactory;
32+
import org.openapitools.codegen.utils.ProcessUtils;
3133

3234
import java.io.File;
3335
import java.util.HashMap;
@@ -68,6 +70,8 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen {
6870
// (mustache does not allow for boolean operators so we need this extra field)
6971
protected boolean doNotUseRxAndCoroutines = true;
7072

73+
protected String authFolder;
74+
7175
public enum DateLibrary {
7276
STRING("string"),
7377
THREETENBP("threetenbp"),
@@ -300,6 +304,7 @@ public void processOpts() {
300304

301305
// infrastructure destination folder
302306
final String infrastructureFolder = (sourceFolder + File.separator + packageName + File.separator + "infrastructure").replace(".", "/");
307+
authFolder = (sourceFolder + File.separator + packageName + File.separator + "auth").replace(".", "/");
303308

304309
// additional properties
305310
if (additionalProperties.containsKey(DATE_LIBRARY)) {
@@ -339,6 +344,23 @@ public void processOpts() {
339344
typeMapping.put("list", "kotlin.collections.List");
340345
additionalProperties.put("isList", true);
341346
}
347+
348+
if(usesRetrofit2Library()) {
349+
if (ProcessUtils.hasOAuthMethods(openAPI)) {
350+
supportingFiles.add(new SupportingFile("auth/ApiKeyAuth.kt.mustache", authFolder, "ApiKeyAuth.kt"));
351+
supportingFiles.add(new SupportingFile("auth/OAuth.kt.mustache", authFolder, "OAuth.kt"));
352+
supportingFiles.add(new SupportingFile("auth/OAuthFlow.kt.mustache", authFolder, "OAuthFlow.kt"));
353+
supportingFiles.add(new SupportingFile("auth/OAuthOkHttpClient.kt.mustache", authFolder, "OAuthOkHttpClient.kt"));
354+
}
355+
356+
if(ProcessUtils.hasHttpBearerMethods(openAPI)) {
357+
supportingFiles.add(new SupportingFile("auth/HttpBearerAuth.kt.mustache", authFolder, "HttpBearerAuth.kt"));
358+
}
359+
360+
if(ProcessUtils.hasHttpBasicMethods(openAPI)) {
361+
supportingFiles.add(new SupportingFile("auth/HttpBasicAuth.kt.mustache", authFolder, "HttpBasicAuth.kt"));
362+
}
363+
}
342364
}
343365

344366
private void processDateLibrary() {
@@ -403,6 +425,7 @@ private void processJVMRetrofit2Library(String infrastructureFolder) {
403425
additionalProperties.put(JVM, true);
404426
additionalProperties.put(JVM_RETROFIT2, true);
405427
supportingFiles.add(new SupportingFile("infrastructure/ApiClient.kt.mustache", infrastructureFolder, "ApiClient.kt"));
428+
supportingFiles.add(new SupportingFile("infrastructure/ResponseExt.kt.mustache", infrastructureFolder, "ResponseExt.kt"));
406429
supportingFiles.add(new SupportingFile("infrastructure/CollectionFormats.kt.mustache", infrastructureFolder, "CollectionFormats.kt"));
407430
addSupportingSerializerAdapters(infrastructureFolder);
408431
}
@@ -494,7 +517,6 @@ private void processMultiplatformLibrary(final String infrastructureFolder) {
494517
supportingFiles.add(new SupportingFile("infrastructure/OctetByteArray.kt.mustache", infrastructureFolder, "OctetByteArray.kt"));
495518

496519
// multiplatform specific auth
497-
final String authFolder = (sourceFolder + File.separator + packageName + File.separator + "auth").replace(".", "/");
498520
supportingFiles.add(new SupportingFile("auth/ApiKeyAuth.kt.mustache", authFolder, "ApiKeyAuth.kt"));
499521
supportingFiles.add(new SupportingFile("auth/Authentication.kt.mustache", authFolder, "Authentication.kt"));
500522
supportingFiles.add(new SupportingFile("auth/HttpBasicAuth.kt.mustache", authFolder, "HttpBasicAuth.kt"));
@@ -558,6 +580,10 @@ public Map<String, Object> postProcessModels(Map<String, Object> objs) {
558580
return objects;
559581
}
560582

583+
private boolean usesRetrofit2Library() {
584+
return getLibrary() != null && getLibrary().contains(JVM_RETROFIT2);
585+
}
586+
561587
@Override
562588
@SuppressWarnings("unchecked")
563589
public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> objs, List<Object> allModels) {
@@ -574,6 +600,10 @@ public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> o
574600
}
575601
}
576602

603+
if (usesRetrofit2Library() && StringUtils.isNotEmpty(operation.path) && operation.path.startsWith("/")) {
604+
operation.path = operation.path.substring(1);
605+
}
606+
577607
// modify the data type of binary form parameters to a more friendly type for multiplatform builds
578608
if (MULTIPLATFORM.equals(getLibrary()) && operation.allParams != null) {
579609
for (CodegenParameter param : operation.allParams) {

modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ProcessUtils.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.swagger.v3.oas.models.OpenAPI;
44
import io.swagger.v3.oas.models.security.SecurityScheme;
55
import org.openapitools.codegen.CodegenModel;
6+
import org.openapitools.codegen.CodegenOperation;
67
import org.openapitools.codegen.CodegenProperty;
78
import org.openapitools.codegen.CodegenSecurity;
89

@@ -81,7 +82,7 @@ public static boolean hasApiKeyMethods(List<CodegenSecurity> authMethods) {
8182
}
8283

8384
/**
84-
* Returns true if the specified OAS model has at least one operation with the HTTP signature
85+
* Returns true if the specified OAS model has at least one operation with the HTTP basic
8586
* security scheme.
8687
* The HTTP signature scheme is defined in https://datatracker.ietf.org/doc/draft-cavage-http-signatures/
8788
*

modules/openapi-generator/src/main/resources/kotlin-client/README.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Requires
44

55
{{#jvm}}
6-
* Kotlin 1.3.41
6+
* Kotlin 1.3.61
77
* Gradle 4.9
88
{{/jvm}}
99
{{#multiplatform}}

modules/openapi-generator/src/main/resources/kotlin-client/build.gradle.mustache

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ test {
4141

4242
dependencies {
4343
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
44+
{{#hasOAuthMethods}}
45+
compile "org.apache.oltu.oauth2:org.apache.oltu.oauth2.client:1.0.0"
46+
{{/hasOAuthMethods}}
4447
{{#moshi}}
4548
{{^moshiCodeGen}}
4649
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
@@ -81,6 +84,7 @@ dependencies {
8184
{{/modeCodeGen}}
8285
{{/moshi}}
8386
compile "com.squareup.okhttp3:okhttp:4.2.2"
87+
compile "com.squareup.okhttp3:logging-interceptor:4.4.0"
8488
{{/jvm-okhttp4}}
8589
{{#threetenbp}}
8690
compile "org.threeten:threetenbp:1.4.0"

modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/api.mustache

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@ import retrofit2.http.*
55
{{#doNotUseRxAndCoroutines}}
66
import retrofit2.Call
77
{{/doNotUseRxAndCoroutines}}
8+
{{^doNotUseRxAndCoroutines}}
9+
{{#useCoroutines}}
10+
import retrofit2.Response
11+
{{/useCoroutines}}
12+
{{/doNotUseRxAndCoroutines}}
813
import okhttp3.RequestBody
14+
{{#isResponseFile}}
915
import okhttp3.ResponseBody
16+
{{/isResponseFile}}
17+
{{#isMultipart}}
1018
import okhttp3.MultipartBody
19+
{{/isMultipart}}
1120
{{^doNotUseRxAndCoroutines}}
1221
{{#useRxJava}}
1322
import rx.Observable
@@ -28,6 +37,17 @@ import io.reactivex.Completable
2837
{{#operations}}
2938
interface {{classname}} {
3039
{{#operation}}
40+
/**
41+
* {{summary}}
42+
* {{notes}}
43+
* Responses:{{#responses}}
44+
* - {{code}}: {{{message}}}{{/responses}}
45+
*
46+
{{#allParams}}
47+
* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}
48+
{{/allParams}}
49+
* @return {{^useCoroutines}}[Call]<{{/useCoroutines}}{{#isResponseFile}}[ResponseBody]{{/isResponseFile}}{{^isResponseFile}}{{#returnType}}[{{{returnType}}}]{{/returnType}}{{^returnType}}[Unit]{{/returnType}}{{/isResponseFile}}{{^useCoroutines}}>{{/useCoroutines}}
50+
*/
3151
{{#isDeprecated}}
3252
@Deprecated("This api was deprecated")
3353
{{/isDeprecated}}
@@ -46,7 +66,7 @@ interface {{classname}} {
4666
{{/prioritizedContentTypes}}
4767
{{/formParams}}
4868
@{{httpMethod}}("{{{path}}}")
49-
{{^doNotUseRxAndCoroutines}}{{#useCoroutines}}suspend {{/useCoroutines}}{{/doNotUseRxAndCoroutines}}fun {{operationId}}({{^allParams}}){{/allParams}}{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{#hasMore}}, {{/hasMore}}{{^hasMore}}){{/hasMore}}{{/allParams}}: {{^doNotUseRxAndCoroutines}}{{#useRxJava}}Observable<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}>{{/useRxJava}}{{#useRxJava2}}{{#returnType}}Single<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{/isResponseFile}}>{{/returnType}}{{^returnType}}Completable{{/returnType}}{{/useRxJava2}}{{#useCoroutines}}{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}{{/useCoroutines}}{{/doNotUseRxAndCoroutines}}{{#doNotUseRxAndCoroutines}}Call<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}>{{/doNotUseRxAndCoroutines}}
69+
{{^doNotUseRxAndCoroutines}}{{#useCoroutines}}suspend {{/useCoroutines}}{{/doNotUseRxAndCoroutines}}fun {{operationId}}({{^allParams}}){{/allParams}}{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{#hasMore}}, {{/hasMore}}{{^hasMore}}){{/hasMore}}{{/allParams}}: {{^doNotUseRxAndCoroutines}}{{#useRxJava}}Observable<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}>{{/useRxJava}}{{#useRxJava2}}{{#returnType}}Single<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{/isResponseFile}}>{{/returnType}}{{^returnType}}Completable{{/returnType}}{{/useRxJava2}}{{#useCoroutines}}Response<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}>{{/useCoroutines}}{{/doNotUseRxAndCoroutines}}{{#doNotUseRxAndCoroutines}}Call<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}>{{/doNotUseRxAndCoroutines}}
5070

5171
{{/operation}}
5272
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# {{classname}}{{#description}}
2+
{{description}}{{/description}}
3+
4+
All URIs are relative to *{{basePath}}*
5+
6+
Method | HTTP request | Description
7+
------------- | ------------- | -------------
8+
{{#operations}}{{#operation}}[**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{#summary}}{{summary}}{{/summary}}
9+
{{/operation}}{{/operations}}
10+
11+
{{#operations}}
12+
{{#operation}}
13+
14+
{{summary}}{{#notes}}
15+
16+
{{notes}}{{/notes}}
17+
18+
### Example
19+
```kotlin
20+
// Import classes:
21+
//import {{{packageName}}}.*
22+
//import {{{packageName}}}.infrastructure.*
23+
//import {{{modelPackage}}}.*
24+
25+
val apiClient = ApiClient()
26+
{{#authMethods}}
27+
{{#isBasic}}
28+
{{#isBasicBasic}}
29+
apiClient.setCredentials("USERNAME", "PASSWORD")
30+
{{/isBasicBasic}}
31+
{{#isBasicBearer}}
32+
apiClient.setBearerToken("TOKEN")
33+
{{/isBasicBearer}}
34+
{{/isBasic}}
35+
{{/authMethods}}
36+
val webService = apiClient.createWebservice({{{classname}}}::class.java)
37+
{{#allParams}}
38+
val {{{paramName}}} : {{{dataType}}} = {{{example}}} // {{{dataType}}} | {{{description}}}
39+
{{/allParams}}
40+
41+
{{#useCoroutines}}
42+
launch(Dispatchers.IO) {
43+
{{/useCoroutines}}
44+
{{#useCoroutines}} {{/useCoroutines}}{{#returnType}}val result : {{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}} = {{/returnType}}webService.{{{operationId}}}({{#allParams}}{{{paramName}}}{{#hasMore}}, {{/hasMore}}{{/allParams}})
45+
{{#useCoroutines}}
46+
}
47+
{{/useCoroutines}}
48+
```
49+
50+
### Parameters
51+
{{^allParams}}This endpoint does not need any parameter.{{/allParams}}{{#allParams}}{{#-last}}
52+
Name | Type | Description | Notes
53+
------------- | ------------- | ------------- | -------------{{/-last}}{{/allParams}}
54+
{{#allParams}} **{{paramName}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isFile}}**{{dataType}}**{{/isFile}}{{^isFile}}{{#generateModelDocs}}[**{{dataType}}**]({{baseType}}.md){{/generateModelDocs}}{{^generateModelDocs}}**{{dataType}}**{{/generateModelDocs}}{{/isFile}}{{/isPrimitiveType}}| {{description}} |{{^required}} [optional]{{/required}}{{#defaultValue}} [default to {{defaultValue}}]{{/defaultValue}}{{#allowableValues}} [enum: {{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}]{{/allowableValues}}
55+
{{/allParams}}
56+
57+
### Return type
58+
59+
{{#returnType}}{{#returnTypeIsPrimitive}}**{{returnType}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}{{#generateModelDocs}}[**{{returnType}}**]({{returnBaseType}}.md){{/generateModelDocs}}{{^generateModelDocs}}**{{returnType}}**{{/generateModelDocs}}{{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}}null (empty response body){{/returnType}}
60+
61+
### Authorization
62+
63+
{{^authMethods}}No authorization required{{/authMethods}}
64+
{{#authMethods}}
65+
{{#isBasic}}
66+
{{#isBasicBasic}}
67+
Configure {{name}}:
68+
ApiClient().setCredentials("USERNAME", "PASSWORD")
69+
{{/isBasicBasic}}
70+
{{#isBasicBearer}}
71+
Configure {{name}}:
72+
ApiClient().setBearerToken("TOKEN")
73+
{{/isBasicBearer}}
74+
{{/isBasic}}
75+
{{/authMethods}}
76+
77+
### HTTP request headers
78+
79+
- **Content-Type**: {{#consumes}}{{{mediaType}}}{{#hasMore}}, {{/hasMore}}{{/consumes}}{{^consumes}}Not defined{{/consumes}}
80+
- **Accept**: {{#produces}}{{{mediaType}}}{{#hasMore}}, {{/hasMore}}{{/produces}}{{^produces}}Not defined{{/produces}}
81+
82+
{{/operation}}
83+
{{/operations}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package {{packageName}}.auth
2+
3+
import java.io.IOException
4+
import java.net.URI
5+
import java.net.URISyntaxException
6+
7+
import okhttp3.Interceptor
8+
import okhttp3.Response
9+
10+
class ApiKeyAuth(
11+
private val location: String = "",
12+
private val paramName: String = "",
13+
private var apiKey: String = ""
14+
) : Interceptor {
15+
16+
@Throws(IOException::class)
17+
override fun intercept(chain: Interceptor.Chain): Response {
18+
var request = chain.request()
19+
20+
if ("query" == location) {
21+
var newQuery = request.url.toUri().query
22+
val paramValue = "$paramName=$apiKey"
23+
if (newQuery == null) {
24+
newQuery = paramValue
25+
} else {
26+
newQuery += "&$paramValue"
27+
}
28+
29+
val newUri: URI
30+
try {
31+
val oldUri = request.url.toUri()
32+
newUri = URI(oldUri.scheme, oldUri.authority,
33+
oldUri.path, newQuery, oldUri.fragment)
34+
} catch (e: URISyntaxException) {
35+
throw IOException(e)
36+
}
37+
38+
request = request.newBuilder().url(newUri.toURL()).build()
39+
} else if ("header" == location) {
40+
request = request.newBuilder()
41+
.addHeader(paramName, apiKey)
42+
.build()
43+
} else if ("cookie" == location) {
44+
request = request.newBuilder()
45+
.addHeader("Cookie", "$paramName=$apiKey")
46+
.build()
47+
}
48+
return chain.proceed(request)
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package {{packageName}}.auth
2+
3+
import java.io.IOException
4+
5+
import kotlin.jvm.Throws
6+
import okhttp3.Interceptor
7+
import okhttp3.Interceptor.Chain
8+
import okhttp3.Response
9+
import okhttp3.Credentials
10+
11+
class HttpBasicAuth(
12+
private var username: String = "",
13+
private var password: String = ""
14+
) : Interceptor {
15+
16+
fun setCredentials(username: String, password: String) {
17+
this.username = username
18+
this.password = password
19+
}
20+
21+
@Throws(IOException::class)
22+
override fun intercept(chain: Chain): Response {
23+
var request = chain.request()
24+
25+
// If the request already have an authorization (eg. Basic auth), do nothing
26+
if (request.header("Authorization") == null && username.isNotBlank() && password.isNotBlank()) {
27+
request = request.newBuilder()
28+
.addHeader("Authorization", Credentials.basic(username, password))
29+
.build()
30+
}
31+
return chain.proceed(request)
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package {{packageName}}.auth
2+
3+
import java.io.IOException
4+
5+
import okhttp3.Interceptor
6+
import okhttp3.Interceptor.Chain
7+
import okhttp3.Response
8+
9+
class HttpBearerAuth(
10+
private var schema: String = "",
11+
var bearerToken: String = ""
12+
) : Interceptor {
13+
14+
@Throws(IOException::class)
15+
override fun intercept(chain: Chain): Response {
16+
var request = chain.request()
17+
18+
// If the request already have an authorization (eg. Basic auth), do nothing
19+
if (request.header("Authorization") == null && bearerToken.isNotBlank()) {
20+
request = request.newBuilder()
21+
.addHeader("Authorization", headerValue())
22+
.build()
23+
}
24+
return chain.proceed(request)
25+
}
26+
27+
private fun headerValue(): String {
28+
return if (schema.isNotBlank()) {
29+
"${upperCaseBearer()} $bearerToken"
30+
} else {
31+
bearerToken
32+
}
33+
}
34+
35+
private fun upperCaseBearer(): String {
36+
return if (schema.toLowerCase().equals("bearer")) "Bearer" else schema
37+
}
38+
39+
}

0 commit comments

Comments
 (0)