Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,14 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
.filter(isSerializable)
.collect(Collectors.toList());
operation.hasProduces = operation.produces != null && !operation.produces.isEmpty();

// form style with explode puts a map-typed query parameter on the wire as one parameter
// per entry; api.mustache adds those after the declared query parameters
for (CodegenParameter param : operation.queryParams) {
if (param.isMap && param.isExplode && !param.isDeepObject) {
param.vendorExtensions.put("x-kotlin-explode-form-object", true);
}
}
}

// set multipart against all relevant operations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ import {{packageName}}.infrastructure.Serializer
{{/hasQueryParams}}{{#hasQueryParams}}mutableMapOf<kotlin.String, kotlin.collections.List<kotlin.String>>()
.apply {
{{#queryParams}}
{{^vendorExtensions.x-kotlin-explode-form-object}}
{{^required}}
if ({{{paramName}}} != null) {
{{#isModel}}
Expand Down Expand Up @@ -251,6 +252,23 @@ import {{packageName}}.infrastructure.Serializer
{{/isModel}}
{{/isNullable}}
{{/required}}
{{/vendorExtensions.x-kotlin-explode-form-object}}
{{/queryParams}}
{{#queryParams}}
{{#vendorExtensions.x-kotlin-explode-form-object}}
// form style, explode: one query parameter per entry, keyed by the property name, appended after the declared ones; null is left out, a collection repeats the key
({{{paramName}}} as? kotlin.collections.Map<*, *>)?.forEach { (key, value) ->
val values = when (value) {
is kotlin.collections.Iterable<*> -> value.toList()
is kotlin.Array<*> -> value.toList()
else -> listOf(value)
}.filterNotNull().map { parameterToString(it) }
if (key != null && values.isNotEmpty()) {
val name = key.toString()
put(name, getOrElse(name) { emptyList() } + values)
}
}
{{/vendorExtensions.x-kotlin-explode-form-object}}
{{/queryParams}}
}
{{/hasQueryParams}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
Expand Down Expand Up @@ -229,6 +230,47 @@ public void testJvmKtorQueryParamWithTypeObject() throws IOException {
assertFileNotContains(defaultApi.toPath(), "mapDeep?.apply {");
}

@Test(description = "Verify a form style, exploded map query parameter goes on the wire one entry per parameter")
public void testExplodedObjectQueryParameterJvmOkhttp() throws IOException {
OpenAPI openAPI = readOpenAPI("src/test/resources/3_0/exploded-object-query-param.yaml");

KotlinClientCodegen codegen = createCodegen(ClientLibrary.JVM_OKHTTP4);
DefaultGenerator generator = new DefaultGenerator();
enableOnlyApiGeneration(generator);

List<File> files = generator.opts(createClientOptInput(openAPI, codegen)).generate();
File defaultApi = files.stream().filter(file -> file.getName().equals("DefaultApi.kt")).findAny().orElseThrow();

String content = new String(Files.readAllBytes(defaultApi.toPath()), StandardCharsets.UTF_8);

// form style with explode - the default - puts every entry on the wire under its own
// property name. Serializing the whole map with toString() is what used to happen.
assertFileContains(defaultApi.toPath(), "(filter as? kotlin.collections.Map<*, *>)?.forEach { (key, value) ->");
assertFileNotContains(defaultApi.toPath(), "put(\"filter\", listOf(filter.toString()))");

// a declared map behaves the same way
assertFileContains(defaultApi.toPath(), "(typedFilter as? kotlin.collections.Map<*, *>)?.forEach { (key, value) ->");
assertFileNotContains(defaultApi.toPath(), "put(\"typedFilter\"");

// a null entry is left out rather than sent as "null", a collection repeats the key
// once per element rather than going out as its toString(), and an entry is appended
// rather than replacing a query parameter of the same name
assertFileContains(defaultApi.toPath(),
"is kotlin.collections.Iterable<*> -> value.toList()",
"}.filterNotNull().map { parameterToString(it) }",
"put(name, getOrElse(name) { emptyList() } + values)");

// deepObject and form without explode both keep a single parameter
assertFileContains(defaultApi.toPath(),
"put(\"deepFilter\", listOf(deepFilter.toString()))",
"put(\"flatFilter\", listOf(flatFilter.toString()))");

// the exploded entries are added after every declared parameter, so a declared
// parameter's put cannot overwrite an entry that happens to share its name
Assert.assertTrue(content.indexOf("(filter as? kotlin.collections.Map") > content.indexOf("put(\"flatFilter\""),
"exploded entries must be added after the declared query parameters");
}

private static void assertFileContainsLine(List<String> lines, String line) {
Assert.assertListContains(lines, s -> s.equals(line), line);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
openapi: 3.0.3
info:
title: Exploded object query parameters
description: >
Object typed query parameters under form/explode (as a free-form object and as a typed map), deepObject, and form without explode. The free-form variant matters because it is flagged isMap but not isContainer.
version: 1.0.0
servers:
- url: localhost:8080
paths:
/items:
get:
operationId: listItems
parameters:
# style and explode both left out, so the form/true defaults apply: every entry
# becomes its own parameter, keyed by the property name alone.
- in: query
name: filter
schema:
type: object
# the same, but declared as a map rather than as a free-form object
- in: query
name: typedFilter
schema:
type: object
additionalProperties:
type: string
# deepObject nests each entry under the parameter name: deepFilter[key]=value
- in: query
name: deepFilter
style: deepObject
explode: true
schema:
type: object
# form without explode keeps a single parameter carrying the whole object
- in: query
name: flatFilter
style: form
explode: false
schema:
type: object
responses:
'200':
description: a list of items
content:
application/json:
schema:
type: array
items:
type: string
Loading