diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index b6aa10ba..2f06a558 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -76,12 +76,12 @@ impl<'p> PathAttr<'p> { ) { let ext_params = ext_parameters.into_iter(); - let (existing_params, new_params): (Vec, Vec) = + let (existing_incoming_params, new_params): (Vec, Vec) = ext_params.partition(|param| self.params.iter().any(|p| p == param)); - for existing in existing_params { - if let Some(param) = self.params.iter_mut().find(|p| **p == existing) { - param.merge(existing); + for existing_incoming in existing_incoming_params { + if let Some(param) = self.params.iter_mut().find(|p| **p == existing_incoming) { + param.merge(existing_incoming); } } diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index 77cdd6af..c22ac067 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -52,7 +52,12 @@ impl<'p> Parameter<'p> { pub fn merge(&mut self, other: Parameter<'p>) { match (self, other) { (Self::Value(value), Parameter::Value(other)) => { + let (schema_features, _) = &value.features; value.parameter_schema = other.parameter_schema; + + if let Some(parameter_schema) = &mut value.parameter_schema { + parameter_schema.features.clone_from(schema_features); + } } (Self::IntoParamsIdent(into_params), Parameter::IntoParamsIdent(other)) => { *into_params = other; @@ -157,7 +162,7 @@ impl ToTokensDiagnostics for ParameterSchema<'_> { ParameterType::External(type_tree) => { let required: Required = (!type_tree.is_option()).into(); - Ok(to_tokens( + to_tokens( ComponentSchema::new(component::ComponentSchemaProps { type_tree, features: Some(self.features.clone()), @@ -166,7 +171,8 @@ impl ToTokensDiagnostics for ParameterSchema<'_> { object_name: "", }), required, - )) + ); + Ok(()) } ParameterType::Parsed(inline_type) => { let type_tree = inline_type.as_type_tree()?; @@ -175,7 +181,7 @@ impl ToTokensDiagnostics for ParameterSchema<'_> { schema_features.clone_from(&self.features); schema_features.push(Feature::Inline(inline_type.is_inline.into())); - Ok(to_tokens( + to_tokens( ComponentSchema::new(component::ComponentSchemaProps { type_tree: &type_tree, features: Some(schema_features), @@ -184,7 +190,8 @@ impl ToTokensDiagnostics for ParameterSchema<'_> { object_name: "", }), required, - )) + ); + Ok(()) } } } diff --git a/utoipa-gen/tests/path_derive_axum_test.rs b/utoipa-gen/tests/path_derive_axum_test.rs index d7a1dfe8..f3b2cd0b 100644 --- a/utoipa-gen/tests/path_derive_axum_test.rs +++ b/utoipa-gen/tests/path_derive_axum_test.rs @@ -180,6 +180,7 @@ fn get_todo_with_extension() { fn derive_path_params_into_params_unnamed() { #[derive(Deserialize, IntoParams)] #[into_params(names("id", "name"))] + #[allow(dead_code)] struct IdAndName(u64, String); #[utoipa::path( @@ -235,6 +236,7 @@ fn derive_path_params_with_ignored_parameter() { struct Auth; #[derive(Deserialize, IntoParams)] #[into_params(names("id", "name"))] + #[allow(dead_code)] struct IdAndName(u64, String); #[utoipa::path( @@ -537,6 +539,7 @@ fn path_param_single_arg_primitive_type() { #[test] fn path_param_single_arg_non_primitive_type() { #[derive(utoipa::ToSchema)] + #[allow(dead_code)] struct Id(String); #[utoipa::path( @@ -577,6 +580,7 @@ fn path_param_single_arg_non_primitive_type() { fn path_param_single_arg_non_primitive_type_into_params() { #[derive(utoipa::ToSchema, utoipa::IntoParams)] #[into_params(names("id"))] + #[allow(dead_code)] struct Id(String); #[utoipa::path( @@ -611,3 +615,143 @@ fn path_param_single_arg_non_primitive_type_into_params() { ]) ) } + +#[test] +fn derive_path_with_validation_attributes_axum() { + #[derive(IntoParams)] + #[allow(dead_code)] + struct Params { + #[param(maximum = 10, minimum = 5, multiple_of = 2.5)] + id: i32, + + #[param(max_length = 10, min_length = 5, pattern = "[a-z]*")] + value: String, + + #[param(max_items = 5, min_items = 1)] + items: Vec, + } + + #[utoipa::path( + get, + path = "foo/{foo_id}", + responses( + (status = 200, description = "success response") + ), + params( + ("foo_id" = String, Path, min_length = 1, description = "Id of Foo to get"), + Params, + ("name" = Option, description = "Foo name", min_length = 3), + ("nonnullable" = String, description = "Foo nonnullable", min_length = 3, max_length = 10), + ("namequery" = Option, Query, description = "Foo name", min_length = 3), + ("nonnullablequery" = String, Query, description = "Foo nonnullable", min_length = 3, max_length = 10), + ) + )] + #[allow(unused)] + fn get_foo(path: Path, query: Query) {} + + #[derive(OpenApi, Default)] + #[openapi(paths(get_foo))] + struct ApiDoc; + + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); + let parameters = doc.pointer("/paths/foo~1{foo_id}/get/parameters").unwrap(); + + let config = Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloat); + + assert_json_matches!( + parameters, + json!([ + { + "schema": { + "type": "string", + "minLength": 1, + }, + "required": true, + "name": "foo_id", + "in": "path", + "description": "Id of Foo to get" + }, + { + "schema": { + "format": "int32", + "type": "integer", + "maximum": 10.0, + "minimum": 5.0, + "multipleOf": 2.5, + }, + "required": true, + "name": "id", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 10, + "minLength": 5, + "pattern": "[a-z]*" + }, + "required": true, + "name": "value", + "in": "query" + }, + { + "schema": { + "type": "array", + "items": { + "type": "string", + }, + "maxItems": 5, + "minItems": 1, + }, + "required": true, + "name": "items", + "in": "query" + }, + { + "schema": { + "type": "string", + "nullable": true, + "minLength": 3, + }, + "required": true, + "name": "name", + "in": "path", + "description": "Foo name" + }, + { + "schema": { + "type": "string", + "minLength": 3, + "maxLength": 10, + }, + "required": true, + "name": "nonnullable", + "in": "path", + "description": "Foo nonnullable" + }, + { + "schema": { + "type": "string", + "nullable": true, + "minLength": 3, + }, + "required": false, + "name": "namequery", + "in": "query", + "description": "Foo name" + }, + { + "schema": { + "type": "string", + "minLength": 3, + "maxLength": 10, + }, + "required": true, + "name": "nonnullablequery", + "in": "query", + "description": "Foo nonnullable" + } + ]), + config + ); +} diff --git a/utoipa-gen/tests/path_parameter_derive_actix.rs b/utoipa-gen/tests/path_parameter_derive_actix.rs index 732ddc24..f943a89b 100644 --- a/utoipa-gen/tests/path_parameter_derive_actix.rs +++ b/utoipa-gen/tests/path_parameter_derive_actix.rs @@ -166,7 +166,8 @@ fn derive_params_from_method_args_actix_success() { } #[test] -fn derive_path_with_date_params_implicit() { +#[cfg(feature = "chrono")] +fn derive_path_with_date_params_chrono_implicit() { mod mod_derive_path_with_date_params { use actix_web::{get, web, HttpResponse, Responder}; use chrono::{DateTime, Utc}; @@ -221,7 +222,8 @@ fn derive_path_with_date_params_implicit() { } #[test] -fn derive_path_with_date_params_explicit_ignored() { +#[cfg(feature = "time")] +fn derive_path_with_date_params_explicit_ignored_time() { mod mod_derive_path_with_date_params { use actix_web::{get, web, HttpResponse, Responder}; use serde_json::json; @@ -270,6 +272,6 @@ fn derive_path_with_date_params_explicit_ignored() { "[1].required" = r#"true"#, "Parameter required" "[1].deprecated" = r#"null"#, "Parameter deprecated" "[1].schema.type" = r#""string""#, "Parameter schema type" - "[1].schema.format" = r#"null"#, "Parameter schema format" + "[1].schema.format" = r#""date-time""#, "Parameter schema format" }; }