Skip to content
Merged
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
16 changes: 16 additions & 0 deletions modules/openapi-generator/src/main/resources/dart2/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,23 @@ class {{{classname}}} {
{{^required}}
if ({{{paramName}}} != null) {
{{/required}}
{{#isMap}}
{{#isExplode}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When explode is omitted for an object query parameter, this template does not enter the new per-entry branch. Update codegen metadata to represent the OpenAPI form-object default as exploded, while preserving explicit explode: false.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/dart2/api.mustache, line 71:

<comment>When `explode` is omitted for an object query parameter, this template does not enter the new per-entry branch. Update codegen metadata to represent the OpenAPI form-object default as exploded, while preserving explicit `explode: false`.</comment>

<file context>
@@ -67,7 +67,24 @@ class {{{classname}}} {
     if ({{{paramName}}} != null) {
           {{/required}}
+        {{#isMap}}
+          {{#isExplode}}
+            {{^isDeepObject}}
+      // form style explodes an object into one query parameter per entry, keyed by the property name alone;
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default is already applied upstream of this template: swagger-parser fills in explode for form style (OpenAPIDeserializer sets explode = true when the style is form and the spec leaves it out), so isExplode is true by the time the template runs.

Checked with a jar built from this branch: a parameter with neither style nor explode, and one with only style: form, both generate the per-entry forEach branch. Explicit explode: false stays a single parameter.

{{^isDeepObject}}
// form style, explode: one query parameter per entry, keyed by the property name; a collection repeats the key per non-null element
{{#isFreeFormObject}}({{{paramName}}} as Map){{/isFreeFormObject}}{{^isFreeFormObject}}{{{paramName}}}{{/isFreeFormObject}}.forEach((entryKey, dynamic entryValue) => queryParams.addAll(_queryParams('multi', entryKey.toString(), entryValue is Iterable ? entryValue.where((e) => e != null).toList() : entryValue)));
{{/isDeepObject}}
{{#isDeepObject}}
queryParams.addAll(_queryParams('{{{collectionFormat}}}', '{{{baseName}}}', {{{paramName}}}));
{{/isDeepObject}}
{{/isExplode}}
{{^isExplode}}
queryParams.addAll(_queryParams('{{{collectionFormat}}}', '{{{baseName}}}', {{{paramName}}}));
{{/isExplode}}
{{/isMap}}
{{^isMap}}
queryParams.addAll(_queryParams('{{{collectionFormat}}}', '{{{baseName}}}', {{{paramName}}}));
{{/isMap}}
{{^required}}
}
{{/required}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,34 @@ public void testObjectArrayDoesNotUseListFromJson() throws Exception {
TestUtils.assertFileContains(modelFile.toPath(), "cast<Object>");
}

@Test(description = "Verify a form style, exploded map query parameter goes on the wire one entry per parameter")
public void testExplodedObjectQueryParameter() throws Exception {
List<File> files = generateDartNativeFromSpec(
"src/test/resources/3_0/exploded-object-query-param.yaml");

File apiFile = files.stream()
.filter(f -> f.getName().equals("default_api.dart"))
.findFirst()
.orElseThrow(() -> new AssertionError("default_api.dart not found in generated files"));

// form style with explode - the default - puts every entry on the wire under its own
// property name. Handing the whole map to _queryParams stringifies it with
// Map.toString(), which is what used to happen. A collection value repeats the key per
// non-null element ('multi').
TestUtils.assertFileContains(apiFile.toPath(),
"(filter as Map).forEach((entryKey, dynamic entryValue) => queryParams.addAll(_queryParams('multi', entryKey.toString(), entryValue is Iterable ? entryValue.where((e) => e != null).toList() : entryValue)));");
TestUtils.assertFileNotContains(apiFile.toPath(), "_queryParams('', 'filter', filter)");

// a declared map behaves the same way, and needs no cast
TestUtils.assertFileContains(apiFile.toPath(),
"typedFilter.forEach((entryKey, dynamic entryValue) => queryParams.addAll(_queryParams('multi', entryKey.toString(), entryValue is Iterable ? entryValue.where((e) => e != null).toList() : entryValue)));");

// deepObject and form without explode both keep a single parameter
TestUtils.assertFileContains(apiFile.toPath(),
"_queryParams('', 'deepFilter', deepFilter)",
"_queryParams('', 'flatFilter', flatFilter)");
}

@Test(description = "Enum properties with defaults should emit enum constructor, not string literal")
public void testEnumDefaultUsesEnumConstructor() throws Exception {
List<File> files = generateDartNativeFromSpec(
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
Original file line number Diff line number Diff line change
Expand Up @@ -1382,7 +1382,8 @@ class FakeApi {
queryParams.addAll(_queryParams('csv', 'url', url));
queryParams.addAll(_queryParams('multi', 'context', context));
if (language != null) {
queryParams.addAll(_queryParams('', 'language', language));
// form style, explode: one query parameter per entry, keyed by the property name; a collection repeats the key per non-null element
language.forEach((entryKey, dynamic entryValue) => queryParams.addAll(_queryParams('multi', entryKey.toString(), entryValue is Iterable ? entryValue.where((e) => e != null).toList() : entryValue)));
}
queryParams.addAll(_queryParams('', 'allowEmpty', allowEmpty));

Expand Down
Loading