Skip to content

Commit 7f2ee85

Browse files
authored
Generators "scala-sttp" and "scala-sttp4" produce valid code when using APIKeyQuery, APIKeyHeader and APIKeyCookie #13474 (#21551)
* fix: using APIKeyQuery and APIKeyHeader generates invalid code (#13474) * fix: include test * fix: also fix auth generation for sttp4 * fix: maintain prev whitespaces & update generated APIs
1 parent 8862b96 commit 7f2ee85

File tree

16 files changed

+199
-56
lines changed

16 files changed

+199
-56
lines changed

modules/openapi-generator/src/main/resources/scala-sttp/api.mustache

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ class {{classname}}(baseUrl: String) {
2222
{{/javadocRenderer}}
2323
def {{operationId}}({{>methodParameters}}): Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}, Any] =
2424
basicRequest
25-
.method(Method.{{httpMethod.toUpperCase}}, uri"$baseUrl{{{path}}}{{#queryParams.0}}?{{#queryParams}}{{baseName}}=${ {{{paramName}}} }{{^-last}}&{{/-last}}{{/queryParams}}{{/queryParams.0}}{{#isApiKey}}{{#isKeyInQuery}}{{^queryParams.0}}?{{/queryParams.0}}{{#queryParams.0}}&{{/queryParams.0}}{{keyParamName}}=${apiKey.value}&{{/isKeyInQuery}}{{/isApiKey}}")
25+
.method(Method.{{httpMethod.toUpperCase}}, uri"$baseUrl{{{path}}}{{#queryParams.0}}?{{/queryParams.0}}{{#queryParams}}{{baseName}}=${ {{paramName}} }{{^-last}}&{{/-last}}{{/queryParams}}{{#authMethods}}{{#isApiKey}}{{#isKeyInQuery}}{{#queryParams.0}}&{{/queryParams.0}}{{^queryParams.0}}?{{/queryParams.0}}{{keyParamName}}=${apiKeyQuery}{{/isKeyInQuery}}{{/isApiKey}}{{/authMethods}}")
2626
.contentType({{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}){{#headerParams}}
2727
.header({{>paramCreation}}){{/headerParams}}{{#authMethods}}{{#isBasic}}{{#isBasicBasic}}
2828
.auth.basic(username, password){{/isBasicBasic}}{{#isBasicBearer}}
2929
.auth.bearer(bearerToken){{/isBasicBearer}}{{/isBasic}}{{#isApiKey}}{{#isKeyInHeader}}
30-
.header("{{keyParamName}}", apiKey){{/isKeyInHeader}}{{#isKeyInCookie}}
31-
.cookie("{{keyParamName}}", apiKey){{/isKeyInCookie}}{{/isApiKey}}{{/authMethods}}{{#formParams.0}}{{^isMultipart}}
30+
.header("{{keyParamName}}", apiKeyHeader){{/isKeyInHeader}}{{#isKeyInCookie}}
31+
.cookie("{{keyParamName}}", apiKeyCookie){{/isKeyInCookie}}{{/isApiKey}}{{/authMethods}}{{#formParams.0}}{{^isMultipart}}
3232
.body(Map({{#formParams}}
3333
{{>paramFormCreation}}{{^-last}},{{/-last}}{{/formParams}}
3434
)){{/isMultipart}}{{#isMultipart}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}apiKey: String{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}})({{/authMethods.0}}{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = None{{/isContainer}}{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}}
1+
{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}{{#isKeyInHeader}}apiKeyHeader: String{{/isKeyInHeader}}{{#isKeyInQuery}}apiKeyQuery: String{{/isKeyInQuery}}{{#isKeyInCookie}}apiKeyCookie: String{{/isKeyInCookie}}{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}})({{/authMethods.0}}{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = None{{/isContainer}}{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}}

modules/openapi-generator/src/main/resources/scala-sttp4/api.mustache

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ class {{classname}}(baseUrl: String) {
2222
{{/javadocRenderer}}
2323
def {{operationId}}({{>methodParameters}}): Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] =
2424
basicRequest
25-
.method(Method.{{httpMethod.toUpperCase}}, uri"$baseUrl{{{path}}}{{#queryParams.0}}?{{#queryParams}}{{baseName}}=${ {{{paramName}}} }{{^-last}}&{{/-last}}{{/queryParams}}{{/queryParams.0}}{{#isApiKey}}{{#isKeyInQuery}}{{^queryParams.0}}?{{/queryParams.0}}{{#queryParams.0}}&{{/queryParams.0}}{{keyParamName}}=${apiKey.value}&{{/isKeyInQuery}}{{/isApiKey}}")
25+
.method(Method.{{httpMethod.toUpperCase}}, uri"$baseUrl{{{path}}}{{#queryParams.0}}?{{/queryParams.0}}{{#queryParams}}{{baseName}}=${ {{paramName}} }{{^-last}}&{{/-last}}{{/queryParams}}{{#authMethods}}{{#isApiKey}}{{#isKeyInQuery}}{{#queryParams.0}}&{{/queryParams.0}}{{^queryParams.0}}?{{/queryParams.0}}{{keyParamName}}=${apiKeyQuery}{{/isKeyInQuery}}{{/isApiKey}}{{/authMethods}}")
2626
.contentType({{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}){{#headerParams}}
2727
.header({{>paramCreation}}){{/headerParams}}{{#authMethods}}{{#isBasic}}{{#isBasicBasic}}
2828
.auth.basic(username, password){{/isBasicBasic}}{{#isBasicBearer}}
2929
.auth.bearer(bearerToken){{/isBasicBearer}}{{/isBasic}}{{#isApiKey}}{{#isKeyInHeader}}
30-
.header("{{keyParamName}}", apiKey){{/isKeyInHeader}}{{#isKeyInCookie}}
31-
.cookie("{{keyParamName}}", apiKey){{/isKeyInCookie}}{{/isApiKey}}{{/authMethods}}{{#formParams.0}}{{^isMultipart}}
30+
.header("{{keyParamName}}", apiKeyHeader){{/isKeyInHeader}}{{#isKeyInCookie}}
31+
.cookie("{{keyParamName}}", apiKeyCookie){{/isKeyInCookie}}{{/isApiKey}}{{/authMethods}}{{#formParams.0}}{{^isMultipart}}
3232
.body(Map({{#formParams}}
3333
{{>paramFormCreation}}{{^-last}},{{/-last}}{{/formParams}}
3434
)){{/isMultipart}}{{#isMultipart}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}apiKey: String{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}})({{/authMethods.0}}{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = None{{/isContainer}}{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}}
1+
{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}{{#isKeyInHeader}}apiKeyHeader: String{{/isKeyInHeader}}{{#isKeyInQuery}}apiKeyQuery: String{{/isKeyInQuery}}{{#isKeyInCookie}}apiKeyCookie: String{{/isKeyInCookie}}{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}})({{/authMethods.0}}{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = None{{/isContainer}}{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.openapitools.codegen.scala;
2+
3+
import io.swagger.parser.OpenAPIParser;
4+
import io.swagger.v3.oas.models.OpenAPI;
5+
import io.swagger.v3.oas.models.media.Schema;
6+
import io.swagger.v3.parser.core.models.ParseOptions;
7+
import org.openapitools.codegen.ClientOptInput;
8+
import org.openapitools.codegen.CodegenConstants;
9+
import org.openapitools.codegen.DefaultGenerator;
10+
import org.openapitools.codegen.languages.ScalaSttp4ClientCodegen;
11+
import org.openapitools.codegen.languages.features.CXFServerFeatures;
12+
import org.testng.Assert;
13+
import org.testng.annotations.Test;
14+
15+
import java.io.File;
16+
import java.io.IOException;
17+
import java.nio.file.Files;
18+
import java.nio.file.Path;
19+
import java.nio.file.Paths;
20+
21+
import static org.openapitools.codegen.TestUtils.assertFileContains;
22+
23+
public class Sttp4CodegenTest {
24+
25+
@Test
26+
public void verifyApiKeyLocations() throws IOException {
27+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
28+
output.deleteOnExit();
29+
String outputPath = output.getAbsolutePath().replace('\\', '/');
30+
31+
OpenAPI openAPI = new OpenAPIParser()
32+
.readLocation("src/test/resources/bugs/issue_13474.json", null, new ParseOptions()).getOpenAPI();
33+
34+
ScalaSttp4ClientCodegen codegen = new ScalaSttp4ClientCodegen();
35+
codegen.setOutputDir(output.getAbsolutePath());
36+
codegen.additionalProperties().put(CXFServerFeatures.LOAD_TEST_DATA_FROM_FILE, "true");
37+
38+
ClientOptInput input = new ClientOptInput();
39+
input.openAPI(openAPI);
40+
input.config(codegen);
41+
42+
DefaultGenerator generator = new DefaultGenerator();
43+
44+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
45+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
46+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
47+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true");
48+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
49+
generator.opts(input).generate();
50+
51+
Path path = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/api/DefaultApi.scala");
52+
assertFileContains(path, ".method(Method.GET, uri\"$baseUrl/entities/?api_key=${apiKeyQuery}\")\n");
53+
assertFileContains(path, ".header(\"X-Api-Key\", apiKeyHeader)");
54+
assertFileContains(path, ".cookie(\"apikey\", apiKeyCookie)");
55+
}
56+
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,36 @@ public void verifyOperatorName() throws IOException {
7878
assertFileNotContains(path, "val X3D = Value(\"!=\")");
7979
}
8080

81+
@Test
82+
public void verifyApiKeyLocations() throws IOException {
83+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
84+
output.deleteOnExit();
85+
String outputPath = output.getAbsolutePath().replace('\\', '/');
86+
87+
OpenAPI openAPI = new OpenAPIParser()
88+
.readLocation("src/test/resources/bugs/issue_13474.json", null, new ParseOptions()).getOpenAPI();
89+
90+
ScalaSttpClientCodegen codegen = new ScalaSttpClientCodegen();
91+
codegen.setOutputDir(output.getAbsolutePath());
92+
codegen.additionalProperties().put(CXFServerFeatures.LOAD_TEST_DATA_FROM_FILE, "true");
93+
94+
ClientOptInput input = new ClientOptInput();
95+
input.openAPI(openAPI);
96+
input.config(codegen);
97+
98+
DefaultGenerator generator = new DefaultGenerator();
99+
100+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
101+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
102+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
103+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true");
104+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
105+
generator.opts(input).generate();
106+
107+
Path path = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/api/DefaultApi.scala");
108+
assertFileContains(path, ".method(Method.GET, uri\"$baseUrl/entities/?api_key=${apiKeyQuery}\")\n");
109+
assertFileContains(path, ".header(\"X-Api-Key\", apiKeyHeader)");
110+
assertFileContains(path, ".cookie(\"apikey\", apiKeyCookie)");
111+
}
112+
81113
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"openapi": "3.0.2",
3+
"info": {
4+
"title": "Sample ApPI",
5+
"version": "0.1.0"
6+
},
7+
"paths": {
8+
"/entities/": {
9+
"get": {
10+
"responses": {
11+
"200": {
12+
"description": "Successful Response",
13+
"content": {
14+
"text/plain" : {
15+
"schema": {
16+
"type": "string"
17+
}
18+
}
19+
}
20+
}
21+
},
22+
"security": [
23+
{
24+
"APIKeyHeader": []
25+
},
26+
{
27+
"APIKeyQuery": []
28+
},
29+
{
30+
"APIKeyCookie": []
31+
}
32+
]
33+
}
34+
}
35+
},
36+
"components": {
37+
"securitySchemes": {
38+
"APIKeyHeader": {
39+
"type": "apiKey",
40+
"in": "header",
41+
"name": "X-Api-Key"
42+
},
43+
"APIKeyQuery": {
44+
"type": "apiKey",
45+
"in": "query",
46+
"name": "api_key"
47+
},
48+
"APIKeyCookie": {
49+
"type": "apiKey",
50+
"in": "cookie",
51+
"name": "apikey"
52+
}
53+
}
54+
}
55+
}

samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/api/PetApi.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,12 @@ class PetApi(baseUrl: String) {
103103
*
104104
* @param petId ID of pet to return
105105
*/
106-
def getPetById(apiKey: String)(petId: Long
106+
def getPetById(apiKeyHeader: String)(petId: Long
107107
): Request[Either[ResponseException[String, Exception], Pet], Any] =
108108
basicRequest
109109
.method(Method.GET, uri"$baseUrl/pet/${petId}")
110110
.contentType("application/json")
111-
.header("api_key", apiKey)
111+
.header("api_key", apiKeyHeader)
112112
.response(asJson[Pet])
113113

114114
/**

samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/api/StoreApi.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ class StoreApi(baseUrl: String) {
4747
* Available security schemes:
4848
* api_key (apiKey)
4949
*/
50-
def getInventory(apiKey: String)(
50+
def getInventory(apiKeyHeader: String)(
5151
): Request[Either[ResponseException[String, Exception], Map[String, Int]], Any] =
5252
basicRequest
5353
.method(Method.GET, uri"$baseUrl/store/inventory")
5454
.contentType("application/json")
55-
.header("api_key", apiKey)
55+
.header("api_key", apiKeyHeader)
5656
.response(asJson[Map[String, Int]])
5757

5858
/**

samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/api/UserApi.scala

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ class UserApi(baseUrl: String) {
3434
*
3535
* @param user Created user object
3636
*/
37-
def createUser(apiKey: String)(user: User
37+
def createUser(apiKeyHeader: String)(user: User
3838
): Request[Either[ResponseException[String, Exception], Unit], Any] =
3939
basicRequest
4040
.method(Method.POST, uri"$baseUrl/user")
4141
.contentType("application/json")
42-
.header("api_key", apiKey)
42+
.header("api_key", apiKeyHeader)
4343
.body(user)
4444
.response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))))
4545

@@ -54,12 +54,12 @@ class UserApi(baseUrl: String) {
5454
*
5555
* @param user List of user object
5656
*/
57-
def createUsersWithArrayInput(apiKey: String)(user: Seq[User]
57+
def createUsersWithArrayInput(apiKeyHeader: String)(user: Seq[User]
5858
): Request[Either[ResponseException[String, Exception], Unit], Any] =
5959
basicRequest
6060
.method(Method.POST, uri"$baseUrl/user/createWithArray")
6161
.contentType("application/json")
62-
.header("api_key", apiKey)
62+
.header("api_key", apiKeyHeader)
6363
.body(user)
6464
.response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))))
6565

@@ -74,12 +74,12 @@ class UserApi(baseUrl: String) {
7474
*
7575
* @param user List of user object
7676
*/
77-
def createUsersWithListInput(apiKey: String)(user: Seq[User]
77+
def createUsersWithListInput(apiKeyHeader: String)(user: Seq[User]
7878
): Request[Either[ResponseException[String, Exception], Unit], Any] =
7979
basicRequest
8080
.method(Method.POST, uri"$baseUrl/user/createWithList")
8181
.contentType("application/json")
82-
.header("api_key", apiKey)
82+
.header("api_key", apiKeyHeader)
8383
.body(user)
8484
.response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))))
8585

@@ -95,12 +95,12 @@ class UserApi(baseUrl: String) {
9595
*
9696
* @param username The name that needs to be deleted
9797
*/
98-
def deleteUser(apiKey: String)(username: String
98+
def deleteUser(apiKeyHeader: String)(username: String
9999
): Request[Either[ResponseException[String, Exception], Unit], Any] =
100100
basicRequest
101101
.method(Method.DELETE, uri"$baseUrl/user/${username}")
102102
.contentType("application/json")
103-
.header("api_key", apiKey)
103+
.header("api_key", apiKeyHeader)
104104
.response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))))
105105

106106
/**
@@ -150,12 +150,12 @@ class UserApi(baseUrl: String) {
150150
* Available security schemes:
151151
* api_key (apiKey)
152152
*/
153-
def logoutUser(apiKey: String)(
153+
def logoutUser(apiKeyHeader: String)(
154154
): Request[Either[ResponseException[String, Exception], Unit], Any] =
155155
basicRequest
156156
.method(Method.GET, uri"$baseUrl/user/logout")
157157
.contentType("application/json")
158-
.header("api_key", apiKey)
158+
.header("api_key", apiKeyHeader)
159159
.response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))))
160160

161161
/**
@@ -171,12 +171,12 @@ class UserApi(baseUrl: String) {
171171
* @param username name that need to be deleted
172172
* @param user Updated user object
173173
*/
174-
def updateUser(apiKey: String)(username: String, user: User
174+
def updateUser(apiKeyHeader: String)(username: String, user: User
175175
): Request[Either[ResponseException[String, Exception], Unit], Any] =
176176
basicRequest
177177
.method(Method.PUT, uri"$baseUrl/user/${username}")
178178
.contentType("application/json")
179-
.header("api_key", apiKey)
179+
.header("api_key", apiKeyHeader)
180180
.body(user)
181181
.response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))))
182182

0 commit comments

Comments
 (0)