Repository: juhaku/utoipa
Pull request #764 (2023-09-18 11:46:58)

enable required usage with schema_with attribute
This PR fixes a bug which lead to the `required` attribute being ignored when `schema_with` was also used.

```rust
fn custom_schema() -> Object {
	ObjectBuilder::new()
		.schema_type(utoipa::openapi::SchemaType::Integer)
		.format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
			KnownFormat::Int32,
		)))
		.build()
}

#[derive(Debug, Serialize, Deserialize, ToSchema)]
/// Test Structure
struct Test {
        #[schema(schema_with = custom_schema, required)]
        id: Option<i32>,
}
```

Previously this would result in the following OpenApi Schema:

```json
{
  "type": "object",
  "description": "Test Structure",
  "required": [],
  "properties": {
    "id": {
      "type": "integer",
      "format": "int32"
    }
  }
}
```

But should be this instead: 
```json
{
  "type": "object",
  "description": "Test Structure",
  "required": [
     "id"
  ],
  "properties": {
    "id": {
      "type": "integer",
      "format": "int32"
    }
  }
}
```


Patch:
diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs
index 049454f..e1fe126 100644
--- a/utoipa-gen/src/component/schema.rs
+++ b/utoipa-gen/src/component/schema.rs
@@ -436,21 +436,19 @@ impl ToTokens for NamedStructSchema<'_> {
                                 .property(#name, #property)
                             });
 
-                            if let Property::Schema(_) = property {
-                                if (!is_option
-                                    && super::is_required(
-                                        field_rule.as_ref(),
-                                        container_rules.as_ref(),
-                                    ))
-                                    || required
-                                        .as_ref()
-                                        .map(super::features::Required::is_true)
-                                        .unwrap_or(false)
-                                {
-                                    object_tokens.extend(quote! {
-                                        .required(#name)
-                                    })
-                                }
+                            if (!is_option
+                                && super::is_required(
+                                    field_rule.as_ref(),
+                                    container_rules.as_ref(),
+                                ))
+                                || required
+                                    .as_ref()
+                                    .map(super::features::Required::is_true)
+                                    .unwrap_or(false)
+                            {
+                                object_tokens.extend(quote! {
+                                    .required(#name)
+                                })
                             }
 
                             object_tokens
diff --git a/utoipa-swagger-ui/src/rocket.rs b/utoipa-swagger-ui/src/rocket.rs
index 0b8224b..b122216 100644
--- a/utoipa-swagger-ui/src/rocket.rs
+++ b/utoipa-swagger-ui/src/rocket.rs
@@ -4,10 +4,7 @@ use std::{borrow::Cow, io::Cursor, sync::Arc};
 
 use rocket::{
     http::{Header, Status},
-    response::{
-        status::{self, NotFound},
-        Responder as RocketResponder,
-    },
+    response::{status::NotFound, Responder as RocketResponder},
     route::{Handler, Outcome},
     serde::json::Json,
     Data as RocketData, Request, Response, Route,
@@ -79,18 +76,23 @@ struct ServeSwagger(Cow<'static, str>, Arc<Config<'static>>);
 #[rocket::async_trait]
 impl Handler for ServeSwagger {
     async fn handle<'r>(&self, request: &'r Request<'_>, _: RocketData<'r>) -> Outcome<'r> {
-        let mut path = self.0.as_ref();
+        let mut base_path = self.0.as_ref();
         if let Some(index) = self.0.find('<') {
-            path = &path[..index];
+            base_path = &base_path[..index];
         }
 
-        match super::serve(&request.uri().path().as_str()[path.len()..], self.1.clone()) {
+        let request_path = request.uri().path().as_str();
+        let request_path = match request_path.strip_prefix(base_path) {
+            Some(stripped) => stripped,
+            None => return Outcome::from(request, RedirectResponder(base_path.into())),
+        };
+        match super::serve(request_path, self.1.clone()) {
             Ok(swagger_file) => swagger_file
                 .map(|file| Outcome::from(request, file))
                 .unwrap_or_else(|| Outcome::from(request, NotFound("Swagger UI file not found"))),
             Err(error) => Outcome::from(
                 request,
-                status::Custom(Status::InternalServerError, error.to_string()),
+                rocket::response::status::Custom(Status::InternalServerError, error.to_string()),
             ),
         }
     }
@@ -98,12 +100,20 @@ impl Handler for ServeSwagger {
 
 impl<'r, 'o: 'r> RocketResponder<'r, 'o> for SwaggerFile<'o> {
     fn respond_to(self, _: &'r Request<'_>) -> rocket::response::Result<'o> {
-        rocket::response::Result::Ok(
-            Response::build()
-                .header(Header::new("Content-Type", self.content_type))
-                .sized_body(self.bytes.len(), Cursor::new(self.bytes.to_vec()))
-                .status(Status::Ok)
-                .finalize(),
-        )
+        Ok(Response::build()
+            .header(Header::new("Content-Type", self.content_type))
+            .sized_body(self.bytes.len(), Cursor::new(self.bytes.to_vec()))
+            .status(Status::Ok)
+            .finalize())
+    }
+}
+
+struct RedirectResponder(String);
+impl<'r, 'a: 'r> RocketResponder<'r, 'a> for RedirectResponder {
+    fn respond_to(self, _request: &'r Request<'_>) -> rocket::response::Result<'a> {
+        Response::build()
+            .status(Status::Found)
+            .raw_header("Location", self.0)
+            .ok()
     }
 }
diff --git a/utoipa/src/openapi/schema.rs b/utoipa/src/openapi/schema.rs
index c8e5ccb..53d3c76 100644
--- a/utoipa/src/openapi/schema.rs
+++ b/utoipa/src/openapi/schema.rs
@@ -284,6 +284,12 @@ pub struct Discriminator {
     /// Defines a discriminator property name which must be found within all composite
     /// objects.
     pub property_name: String,
+
+    /// An object to hold mappings between payload values and schema names or references.
+    /// This field can only be populated manually. There is no macro support and no
+    /// validation.
+    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
+    pub mapping: BTreeMap<String, String>,
 }
 
 impl Discriminator {
@@ -299,6 +305,7 @@ impl Discriminator {
     pub fn new<I: Into<String>>(property_name: I) -> Self {
         Self {
             property_name: property_name.into(),
+            mapping: BTreeMap::new(),
         }
     }
 }
@@ -2037,4 +2044,34 @@ mod tests {
 
         assert_eq!(json_str, json_de_str);
     }
+
+    #[test]
+    fn serialize_discriminator_with_mapping() {
+        let mut discriminator = Discriminator::new("type");
+        discriminator.mapping = [("int".to_string(), "#/components/schemas/MyInt".to_string())]
+            .into_iter()
+            .collect::<BTreeMap<_, _>>();
+        let one_of = OneOfBuilder::new()
+            .item(Ref::from_schema_name("MyInt"))
+            .discriminator(Some(discriminator))
+            .build();
+        let json_value = serde_json::to_value(one_of).unwrap();
+
+        assert_json_eq!(
+            json_value,
+            json!({
+                "oneOf": [
+                    {
+                        "$ref": "#/components/schemas/MyInt"
+                    }
+                ],
+                "discriminator": {
+                    "propertyName": "type",
+                    "mapping": {
+                        "int": "#/components/schemas/MyInt"
+                    }
+                }
+            })
+        );
+    }
 }


Test patch:
diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs
index 0402787..d1283c7 100644
--- a/utoipa-gen/tests/schema_derive_test.rs
+++ b/utoipa-gen/tests/schema_derive_test.rs
@@ -4273,6 +4273,7 @@ fn derive_schema_with_custom_field_with_schema() {
                     "format": "email"
                 }
             },
+            "required": [ "id" ],
             "type": "object"
         })
     )