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 @@ -188,7 +188,12 @@ impl {{classname}} for {{classname}}Client {
{{/isArray}}
{{^isArray}}
{{^isNullable}}
{{#isMap}}
local_var_req_builder = local_var_req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(&{{{paramName}}})?)]);
{{/isMap}}
{{^isMap}}
local_var_req_builder = local_var_req_builder.query(&[("{{{baseName}}}", &{{{paramName}}}.to_string())]);
{{/isMap}}
{{/isNullable}}
{{#isNullable}}
{{#isDeepObject}}
Expand Down Expand Up @@ -228,9 +233,16 @@ impl {{classname}} for {{classname}}Client {
{{/isModel}}
{{^isObject}}
{{^isModel}}
{{#isMap}}
if let Some(ref param_value) = {{{paramName}}} {
local_var_req_builder = local_var_req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(param_value)?)]);
};
{{/isMap}}
{{^isMap}}
if let Some(ref param_value) = {{{paramName}}} {
local_var_req_builder = local_var_req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
};
{{/isMap}}
{{/isModel}}
{{/isObject}}
{{/isDeepObject}}
Expand Down Expand Up @@ -273,7 +285,12 @@ impl {{classname}} for {{classname}}Client {
{{/isModel}}
{{^isObject}}
{{^isModel}}
{{#isMap}}
local_var_req_builder = local_var_req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(param_value)?)]);
{{/isMap}}
{{^isMap}}
local_var_req_builder = local_var_req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
{{/isMap}}
{{/isModel}}
{{/isObject}}
{{/isDeepObject}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,12 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
{{/isArray}}
{{^isArray}}
{{^isNullable}}
{{#isMap}}
req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(&{{{vendorExtensions.x-rust-param-identifier}}})?)]);

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.

P3: A required, non-nullable map declared with style: deepObject falls into this {{#isMap}} branch and is serialized as a single JSON-encoded parameter (labels={...}) instead of the deep-object labels[key]=value form. The template only implements deepObject spreading inside the {{#isNullable}} branch, so required non-nullable deep-object maps bypass it. Previously this path emitted .to_string() and did not compile, so this is an incomplete fix, not a regression, but the wire format is now silently wrong for that style.

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/rust/reqwest/api.mustache, line 170:

<comment>A required, non-nullable map declared with `style: deepObject` falls into this `{{#isMap}}` branch and is serialized as a single JSON-encoded parameter (`labels={...}`) instead of the deep-object `labels[key]=value` form. The template only implements deepObject spreading inside the `{{#isNullable}}` branch, so required non-nullable deep-object maps bypass it. Previously this path emitted `.to_string()` and did not compile, so this is an incomplete fix, not a regression, but the wire format is now silently wrong for that style.</comment>

<file context>
@@ -166,7 +166,12 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
     {{^isArray}}
     {{^isNullable}}
+    {{#isMap}}
+    req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(&{{{vendorExtensions.x-rust-param-identifier}}})?)]);
+    {{/isMap}}
+    {{^isMap}}
</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.

Accurate, and worth separating from this PR's scope. That branch - required, non-nullable - has never had any isDeepObject handling at all: unlike the nullable and optional paths just below it, it goes straight to the scalar form for every type. So a required non-nullable style: deepObject map was not previously exploded either; it did not compile at all, which is what this PR fixes. The json encoding it now produces is the same shape the other libraries' non-primitive parameters use, so nothing regresses - but you are right that it is not the declared style.

The deepObject format work lives in #24899, which routes exploded deepObject maps through parse_deep_object (both the free-form and the typed-map shape). Extending that to the required non-nullable branch is a small addition and I am happy to push it - I would suggest doing it there rather than here, so all deepObject encoding stays in one PR and these two do not both edit the same template lines. Say which you prefer and I will add it.

{{/isMap}}
{{^isMap}}
req_builder = req_builder.query(&[("{{{baseName}}}", &{{{vendorExtensions.x-rust-param-identifier}}}.to_string())]);
{{/isMap}}
{{/isNullable}}
{{#isNullable}}
{{#isDeepObject}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,32 @@ public void testIntegerPropertyEnum() throws IOException {
TestUtils.assertFileNotContains(outputPath, linearize("#[serde(rename = \"0\")]"));
}

@Test
public void testMapQueryParamsSerializeAsJson() throws IOException {
// HashMap implements neither Display nor ToString, so the .to_string() the templates
// emitted for a map-typed query parameter did not compile (E0599) - for a required map
// in both libraries, and for optional/nullable maps in reqwest-trait too
for (String library : new String[] {"reqwest", "reqwest-trait"}) {
Path target = Files.createTempDirectory("test");
target.toFile().deleteOnExit();
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("rust")
.setLibrary(library)
.setInputSpec("src/test/resources/3_0/rust/map-query-params.yaml")
.setSkipOverwrite(false)
.setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
Path outputPath = Path.of(target.toString(), "/src/apis/default_api.rs");
TestUtils.assertFileExists(outputPath);
// the required and the optional map both serialize as one json-encoded parameter,
// like the other libraries' non-primitive parameters
TestUtils.assertFileContains(outputPath, "serde_json::to_string(&");
TestUtils.assertFileContains(outputPath, "(\"counts\", &serde_json::to_string(param_value)?)");
TestUtils.assertFileNotContains(outputPath, "labels.to_string()");
TestUtils.assertFileNotContains(outputPath, "param_value.to_string()");
}
}

@Test
public void testArrayWithObjectEnumValues() throws IOException {
Path target = Files.createTempDirectory("test");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
openapi: 3.0.3
info:
title: map query params
version: 1.0.0
paths:
/items:
get:
operationId: listItems
parameters:
- name: labels
in: query
required: true
schema:
type: object
additionalProperties:
type: string
- name: counts
in: query
schema:
type: object
additionalProperties:
type: integer
responses:
'200':
description: ok
Loading