From 213694870712b99826cd0dcec54084468fef684d Mon Sep 17 00:00:00 2001 From: George Holderness Date: Mon, 20 Jul 2026 17:52:56 +0100 Subject: [PATCH] fix(rust): serialize form enums with `Display` --- .../codegen/languages/RustServerCodegen.java | 8 +++-- .../example-server-operation.mustache | 2 +- .../codegen/rust/RustServerCodegenTest.java | 31 +++++++++++++++++ .../3_0/rust-server/form-enum-array.yaml | 34 +++++++++++++++++++ .../multipart-v3/examples/server/server.rs | 2 +- .../openapi-v3/examples/server/server.rs | 14 ++++---- .../output/openapi-v3/src/client/mod.rs | 2 +- .../examples/server/server.rs | 28 +++++++-------- .../src/client/mod.rs | 2 +- .../examples/server/server.rs | 4 +-- 10 files changed, 97 insertions(+), 30 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/rust-server/form-enum-array.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java index 125bcb5088c6..39df2e6ac3f2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java @@ -1753,7 +1753,8 @@ private void processParam(CodegenParameter param, CodegenOperation op) { } } } else if (param.isArray) { - param.vendorExtensions.put("x-format-string", "{:?}"); + boolean itemsAreEnum = param.items != null && param.items.getIsEnumOrRef(); + param.vendorExtensions.put("x-format-string", itemsAreEnum ? "{}" : "{:?}"); if (param.items.isString) { // We iterate through the list of string and ensure they end up in the format vec!["example".to_string()] example = (param.example != null) @@ -1765,7 +1766,7 @@ private void processParam(CodegenParameter param, CodegenOperation op) { example = (param.example != null) ? param.example : "&Vec::new()"; } } else { - param.vendorExtensions.put("x-format-string", "{:?}"); + param.vendorExtensions.put("x-format-string", param.getIsEnumOrRef() ? "{}" : "{:?}"); // Check if this is a model-type enum (allowableValues with values list) if (param.allowableValues != null && param.allowableValues.containsKey("values")) { List values = (List) param.allowableValues.get("values"); @@ -1798,7 +1799,8 @@ private void processParam(CodegenParameter param, CodegenOperation op) { param.vendorExtensions.put("x-example", "None"); } else { // Not required, so override the format string and example - param.vendorExtensions.put("x-format-string", "{:?}"); + boolean itemsAreEnum = param.isArray && param.items != null && param.items.getIsEnumOrRef(); + param.vendorExtensions.put("x-format-string", (param.getIsEnumOrRef() || itemsAreEnum) ? "{}" : "{:?}"); String exampleString = (example != null) ? "Some(" + example + ")" : "None"; param.vendorExtensions.put("x-example", exampleString); } diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-server-operation.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-server-operation.mustache index dfe154824b82..bb736d2136f0 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/example-server-operation.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/example-server-operation.mustache @@ -13,6 +13,6 @@ {{/allParams}} context: &C) -> Result<{{{operationId}}}Response, ApiError> { - info!("{{#exts}}{{{x-operation-id}}}{{/exts}}({{#allParams}}{{#exts}}{{{x-format-string}}}{{/exts}}{{^-last}}, {{/-last}}{{/allParams}}) - X-Span-ID: {:?}"{{#allParams}}, {{{paramName}}}{{/allParams}}, context.get().0.clone()); + info!("{{#exts}}{{{x-operation-id}}}{{/exts}}({{#allParams}}{:?}{{^-last}}, {{/-last}}{{/allParams}}) - X-Span-ID: {:?}"{{#allParams}}, {{{paramName}}}{{/allParams}}, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustServerCodegenTest.java index 0b8c5eea2383..5f73054cbe1b 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustServerCodegenTest.java @@ -155,6 +155,37 @@ public void testMultipleResponseContentTypes() throws IOException { target.toFile().deleteOnExit(); } + /** + * Test that enum items inside an array-typed form parameter are serialized using their + * Display impl (i.e. "{}") rather than Debug (i.e. "{:?}"). Both required and optional + * array-of-enum form params must use Display so the wire value matches the OpenAPI enum + * string, not the Rust variant debug representation. + */ + @Test + public void testFormEnumArrayUsesDisplay() throws IOException { + Path target = Files.createTempDirectory("test"); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("rust-server") + .setInputSpec("src/test/resources/3_0/rust-server/form-enum-array.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/")); + List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path clientModPath = Path.of(target.toString(), "/src/client/mod.rs"); + TestUtils.assertFileExists(clientModPath); + + // Both the required and optional array-of-enum params must use "{}" (Display), + // not "{:?}" (Debug), so the serialized string matches the OpenAPI enum value. + TestUtils.assertFileContains(clientModPath, "format!(\"{}\", param_required_enum_array)"); + TestUtils.assertFileContains(clientModPath, "format!(\"{}\", param_optional_enum_array)"); + TestUtils.assertFileNotContains(clientModPath, "format!(\"{:?}\", param_required_enum_array)"); + TestUtils.assertFileNotContains(clientModPath, "format!(\"{:?}\", param_optional_enum_array)"); + + // Clean up + target.toFile().deleteOnExit(); + } + /** * Test that binary/byte request bodies are passed through as raw bytes rather than being * coerced to UTF-8, which panics on any non-UTF-8 payload (see issue #24094). diff --git a/modules/openapi-generator/src/test/resources/3_0/rust-server/form-enum-array.yaml b/modules/openapi-generator/src/test/resources/3_0/rust-server/form-enum-array.yaml new file mode 100644 index 000000000000..4eea21a832e4 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/rust-server/form-enum-array.yaml @@ -0,0 +1,34 @@ +openapi: "3.0.3" +info: + title: Form Enum Array Test + version: "1.0.0" +paths: + /form-enum-array: + post: + operationId: FormEnumArray + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + required_enum_array: + type: array + items: + type: string + enum: + - value_one + - value_two + optional_enum_array: + type: array + items: + type: string + enum: + - value_one + - value_two + required: + - required_enum_array + responses: + '200': + description: OK diff --git a/samples/server/petstore/rust-server/output/multipart-v3/examples/server/server.rs b/samples/server/petstore/rust-server/output/multipart-v3/examples/server/server.rs index 29377f99bde5..984b9851c942 100644 --- a/samples/server/petstore/rust-server/output/multipart-v3/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/multipart-v3/examples/server/server.rs @@ -167,7 +167,7 @@ impl Api for Server where C: Has + Send + Sync object_field: Option, context: &C) -> Result { - info!("multipart_request_post(\"{}\", {:?}, {:?}, {:?}) - X-Span-ID: {:?}", string_field, binary_field, optional_string_field, object_field, context.get().0.clone()); + info!("multipart_request_post({:?}, {:?}, {:?}, {:?}) - X-Span-ID: {:?}", string_field, binary_field, optional_string_field, object_field, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } diff --git a/samples/server/petstore/rust-server/output/openapi-v3/examples/server/server.rs b/samples/server/petstore/rust-server/output/openapi-v3/examples/server/server.rs index c702e7a5d829..3dfc4542ada5 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/openapi-v3/examples/server/server.rs @@ -193,7 +193,7 @@ impl Api for Server where C: Has + Send + Sync url: String, context: &C) -> Result { - info!("callback_with_header_post(\"{}\") - X-Span-ID: {:?}", url, context.get().0.clone()); + info!("callback_with_header_post({:?}) - X-Span-ID: {:?}", url, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -232,7 +232,7 @@ impl Api for Server where C: Has + Send + Sync iambool: bool, context: &C) -> Result { - info!("get_with_boolean_parameter({}) - X-Span-ID: {:?}", iambool, context.get().0.clone()); + info!("get_with_boolean_parameter({:?}) - X-Span-ID: {:?}", iambool, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -250,7 +250,7 @@ impl Api for Server where C: Has + Send + Sync x_header: String, context: &C) -> Result { - info!("mandatory_request_header_get(\"{}\") - X-Span-ID: {:?}", x_header, context.get().0.clone()); + info!("mandatory_request_header_get({:?}) - X-Span-ID: {:?}", x_header, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -324,7 +324,7 @@ impl Api for Server where C: Has + Send + Sync required_with_example: i32, context: &C) -> Result { - info!("query_example_get(\"{}\", {}) - X-Span-ID: {:?}", required_no_example, required_with_example, context.get().0.clone()); + info!("query_example_get({:?}, {:?}) - X-Span-ID: {:?}", required_no_example, required_with_example, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -341,7 +341,7 @@ impl Api for Server where C: Has + Send + Sync url: String, context: &C) -> Result { - info!("register_callback_post(\"{}\") - X-Span-ID: {:?}", url, context.get().0.clone()); + info!("register_callback_post({:?}) - X-Span-ID: {:?}", url, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -467,7 +467,7 @@ impl Api for Server where C: Has + Send + Sync path_param_b: String, context: &C) -> Result { - info!("multiple_path_params_with_very_long_path_to_test_formatting_path_param_a_path_param_b_get(\"{}\", \"{}\") - X-Span-ID: {:?}", path_param_a, path_param_b, context.get().0.clone()); + info!("multiple_path_params_with_very_long_path_to_test_formatting_path_param_a_path_param_b_get({:?}, {:?}) - X-Span-ID: {:?}", path_param_a, path_param_b, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -485,7 +485,7 @@ impl Api for Server where C: Has + Send + Sync repo_id: String, context: &C) -> Result { - info!("get_repo_info(\"{}\") - X-Span-ID: {:?}", repo_id, context.get().0.clone()); + info!("get_repo_info({:?}) - X-Span-ID: {:?}", repo_id, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } diff --git a/samples/server/petstore/rust-server/output/openapi-v3/src/client/mod.rs b/samples/server/petstore/rust-server/output/openapi-v3/src/client/mod.rs index 34a31884e836..3dab1dd28afb 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/src/client/mod.rs +++ b/samples/server/petstore/rust-server/output/openapi-v3/src/client/mod.rs @@ -871,7 +871,7 @@ impl Api for Client where } #[allow(clippy::uninlined_format_args)] params.push(("enum_field", - format!("{:?}", param_enum_field) + format!("{}", param_enum_field) )); let body = serde_urlencoded::to_string(params).expect("impossible to fail to serialize"); diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs index 98ed4ce9894a..d60357b56d59 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs @@ -248,7 +248,7 @@ impl Api for Server where C: Has + Send + Sync body: models::User, context: &C) -> Result { - info!("test_body_with_query_params(\"{}\", {:?}) - X-Span-ID: {:?}", query, body, context.get().0.clone()); + info!("test_body_with_query_params({:?}, {:?}) - X-Span-ID: {:?}", query, body, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -281,7 +281,7 @@ impl Api for Server where C: Has + Send + Sync callback: Option, context: &C) -> Result { - info!("test_endpoint_parameters({}, {}, \"{}\", {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}) - X-Span-ID: {:?}", number, double, pattern_without_delimiter, byte, integer, int32, int64, float, string, binary, date, date_time, password, callback, context.get().0.clone()); + info!("test_endpoint_parameters({:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}) - X-Span-ID: {:?}", number, double, pattern_without_delimiter, byte, integer, int32, int64, float, string, binary, date, date_time, password, callback, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -318,7 +318,7 @@ impl Api for Server where C: Has + Send + Sync param2: String, context: &C) -> Result { - info!("test_json_form_data(\"{}\", \"{}\") - X-Span-ID: {:?}", param, param2, context.get().0.clone()); + info!("test_json_form_data({:?}, {:?}) - X-Span-ID: {:?}", param, param2, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -327,7 +327,7 @@ impl Api for Server where C: Has + Send + Sync hyphen_param: String, context: &C) -> Result { - info!("hyphen_param(\"{}\") - X-Span-ID: {:?}", hyphen_param, context.get().0.clone()); + info!("hyphen_param({:?}) - X-Span-ID: {:?}", hyphen_param, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -388,7 +388,7 @@ impl Api for Server where C: Has + Send + Sync api_key: Option, context: &C) -> Result { - info!("delete_pet({}, {:?}) - X-Span-ID: {:?}", pet_id, api_key, context.get().0.clone()); + info!("delete_pet({:?}, {:?}) - X-Span-ID: {:?}", pet_id, api_key, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -398,7 +398,7 @@ impl Api for Server where C: Has + Send + Sync pet_id: i64, context: &C) -> Result { - info!("get_pet_by_id({}) - X-Span-ID: {:?}", pet_id, context.get().0.clone()); + info!("get_pet_by_id({:?}) - X-Span-ID: {:?}", pet_id, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -410,7 +410,7 @@ impl Api for Server where C: Has + Send + Sync status: Option, context: &C) -> Result { - info!("update_pet_with_form({}, {:?}, {:?}) - X-Span-ID: {:?}", pet_id, name, status, context.get().0.clone()); + info!("update_pet_with_form({:?}, {:?}, {:?}) - X-Span-ID: {:?}", pet_id, name, status, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -422,7 +422,7 @@ impl Api for Server where C: Has + Send + Sync file: Option, context: &C) -> Result { - info!("upload_file({}, {:?}, {:?}) - X-Span-ID: {:?}", pet_id, additional_metadata, file, context.get().0.clone()); + info!("upload_file({:?}, {:?}, {:?}) - X-Span-ID: {:?}", pet_id, additional_metadata, file, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -451,7 +451,7 @@ impl Api for Server where C: Has + Send + Sync order_id: String, context: &C) -> Result { - info!("delete_order(\"{}\") - X-Span-ID: {:?}", order_id, context.get().0.clone()); + info!("delete_order({:?}) - X-Span-ID: {:?}", order_id, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -461,7 +461,7 @@ impl Api for Server where C: Has + Send + Sync order_id: u64, context: &C) -> Result { - info!("get_order_by_id({}) - X-Span-ID: {:?}", order_id, context.get().0.clone()); + info!("get_order_by_id({:?}) - X-Span-ID: {:?}", order_id, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -502,7 +502,7 @@ impl Api for Server where C: Has + Send + Sync password: String, context: &C) -> Result { - info!("login_user(\"{}\", \"{}\") - X-Span-ID: {:?}", username, password, context.get().0.clone()); + info!("login_user({:?}, {:?}) - X-Span-ID: {:?}", username, password, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -521,7 +521,7 @@ impl Api for Server where C: Has + Send + Sync username: String, context: &C) -> Result { - info!("delete_user(\"{}\") - X-Span-ID: {:?}", username, context.get().0.clone()); + info!("delete_user({:?}) - X-Span-ID: {:?}", username, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -531,7 +531,7 @@ impl Api for Server where C: Has + Send + Sync username: String, context: &C) -> Result { - info!("get_user_by_name(\"{}\") - X-Span-ID: {:?}", username, context.get().0.clone()); + info!("get_user_by_name({:?}) - X-Span-ID: {:?}", username, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -542,7 +542,7 @@ impl Api for Server where C: Has + Send + Sync body: models::User, context: &C) -> Result { - info!("update_user(\"{}\", {:?}) - X-Span-ID: {:?}", username, body, context.get().0.clone()); + info!("update_user({:?}, {:?}) - X-Span-ID: {:?}", username, body, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/client/mod.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/client/mod.rs index eb008a933adb..3ae7f539974e 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/client/mod.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/client/mod.rs @@ -1484,7 +1484,7 @@ impl Api for Client where if let Some(param_enum_form_string) = param_enum_form_string { #[allow(clippy::uninlined_format_args)] params.push(("enum_form_string", - format!("{:?}", param_enum_form_string) + format!("{}", param_enum_form_string) )); } diff --git a/samples/server/petstore/rust-server/output/rust-server-test/examples/server/server.rs b/samples/server/petstore/rust-server/output/rust-server-test/examples/server/server.rs index 144228beed8e..588aaa08b89f 100644 --- a/samples/server/petstore/rust-server/output/rust-server-test/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/rust-server-test/examples/server/server.rs @@ -203,7 +203,7 @@ impl Api for Server where C: Has + Send + Sync body: String, context: &C) -> Result { - info!("html_post(\"{}\") - X-Span-ID: {:?}", body, context.get().0.clone()); + info!("html_post({:?}) - X-Span-ID: {:?}", body, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) } @@ -212,7 +212,7 @@ impl Api for Server where C: Has + Send + Sync value: String, context: &C) -> Result { - info!("post_yaml(\"{}\") - X-Span-ID: {:?}", value, context.get().0.clone()); + info!("post_yaml({:?}) - X-Span-ID: {:?}", value, context.get().0.clone()); Err(ApiError("Api-Error: Operation is NOT implemented".into())) }