juhaku / utoipa

Simple, Fast, Code first and Compile time generated OpenAPI documentation for Rust
Apache License 2.0
2.05k stars 163 forks source link

Proposal: New handling of type aliases #818

Open nik-here opened 7 months ago

nik-here commented 7 months ago

What about an another approach to the macro ToSchema for aliases? This library could add another trait like this:

// utoipa/src/lib.rs
pub trait ToGenricSchema<'__s> {
    fn build_schema(
        generic_properties: std::collections::HashMap<
            &'__s str,
            utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
        >,
    ) -> utoipa::openapi::ObjectBuilder;
}

The purpose of build_schema is to dynamically generate new schemas, depending on the given HashMap generice_properties.

This trait would be implemented by the macro ToSchema instead of the trait ToSchema<'__s> for generic structs. However non generic structs implement ToSchema<'__s> directly. On top the ToSchema<'__s> trait would lose the function aliases.

Each alias would implement ToSchema<'__s> separately and they call build_schema with a generated HashMap.

Here is an quick example, how an implementation by the macro should look like:

pub trait ToGenricSchema<'__s> {
    fn build_schema(
        generic_properties: std::collections::HashMap<
            &'__s str,
            utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
        >,
    ) -> utoipa::openapi::ObjectBuilder;
}

fn get_ref_t_default_schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
    utoipa::openapi::RefOr::T(utoipa::openapi::schema::empty())
}

#[derive(serde::Serialize, serde::Deserialize)]
struct Pet<T, S> {
    id: u64,
    name: String,
    age: Option<i32>,
    generic_property: T,
    generic_property2: S,
    generic_property3: S,
    generic_property4: Option<S>,
}

impl<'__s, T, S> ToGenricSchema<'__s> for Pet<T, S> {
    fn build_schema(
        mut generic_properties: std::collections::HashMap<
            &'__s str,
            utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
        >,
    ) -> utoipa::openapi::ObjectBuilder {
        utoipa::openapi::ObjectBuilder::new()
            .property(
                "id",
                utoipa::openapi::ObjectBuilder::new()
                    .schema_type(utoipa::openapi::SchemaType::Integer)
                    .format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
                        utoipa::openapi::KnownFormat::Int64,
                    ))),
            )
            .required("id")
            .property(
                "name",
                utoipa::openapi::ObjectBuilder::new()
                    .schema_type(utoipa::openapi::SchemaType::String),
            )
            .required("name")
            .property(
                "age",
                utoipa::openapi::ObjectBuilder::new()
                    .schema_type(utoipa::openapi::SchemaType::Integer)
                    .format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
                        utoipa::openapi::KnownFormat::Int32,
                    ))),
            )
            .property(
                "generic_property",
                generic_properties
                    .remove("generic_property")
                    .unwrap_or_else(get_ref_t_default_schema),
            )
            .required("generic_property")
            .property(
                "generic_property2",
                generic_properties
                    .remove("generic_property2")
                    .unwrap_or_else(get_ref_t_default_schema),
            )
            .required("generic_property2")
            .property(
                "generic_property3",
                generic_properties
                    .remove("generic_property3")
                    .unwrap_or_else(get_ref_t_default_schema),
            )
            .required("generic_property3")
            .property(
                "generic_property4",
                generic_properties
                    .remove("generic_property4")
                    .unwrap_or_else(get_ref_t_default_schema),
            )
            .example(Some(serde_json::json!({
              "name":"bob the cat","id":1
            })))
    }
}

impl<'__s> utoipa::ToSchema<'__s> for Pet<u32, String> {
    fn schema() -> (
        &'__s str,
        utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
    ) {
        let mut generic_properties = std::collections::HashMap::new();
        generic_properties.insert(
            "generic_property",
            utoipa::openapi::ObjectBuilder::new()
                .schema_type(utoipa::openapi::SchemaType::Integer)
                .format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
                    utoipa::openapi::KnownFormat::Int32,
                )))
                .into(),
        );
        generic_properties.insert(
            "generic_property2",
            utoipa::openapi::ObjectBuilder::new()
                .schema_type(utoipa::openapi::SchemaType::String)
                .into(),
        );
        generic_properties.insert(
            "generic_property3",
            utoipa::openapi::ObjectBuilder::new()
                .schema_type(utoipa::openapi::SchemaType::String)
                .into(),
        );
        generic_properties.insert(
            "generic_property4",
            utoipa::openapi::ObjectBuilder::new()
                .schema_type(utoipa::openapi::SchemaType::String)
                .into(),
        );
        (
            "PetU32String",
            Self::build_schema(generic_properties).into(),
        )
    }
}

impl<'__s> utoipa::ToSchema<'__s> for Pet<u32, u32> {
    fn schema() -> (
        &'__s str,
        utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
    ) {
        let mut generic_properties = std::collections::HashMap::new();
        // generated inserts
        ("PetU32U32", Self::build_schema(generic_properties).into())
    }
}

struct Pet2<T, S> {
    pet: Pet<T, S>,
    mood: String,
}

impl<'__s, T, S> ToGenricSchema<'__s> for Pet2<T, S> {
    fn build_schema(
        generic_properties: std::collections::HashMap<
            &'__s str,
            utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
        >,
    ) -> utoipa::openapi::ObjectBuilder {
        utoipa::openapi::ObjectBuilder::new()
            .property("pet", Pet::<T, S>::build_schema(generic_properties))
            .required("pet")
            .property(
                "mood",
                utoipa::openapi::ObjectBuilder::new()
                    .schema_type(utoipa::openapi::SchemaType::String),
            )
            .required("mood")
    }
}

#[derive(serde::Serialize, serde::Deserialize)]
struct Pet3<T, S> {
    #[serde(flatten)]
    pet: Pet<T, S>,
    mood: String,
}

impl<'__s, T, S> ToGenricSchema<'__s> for Pet3<T, S> {
    fn build_schema(
        generic_properties: std::collections::HashMap<
            &'__s str,
            utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
        >,
    ) -> utoipa::openapi::ObjectBuilder {
        Pet::<T, S>::build_schema(generic_properties)
            .property(
                "mood",
                utoipa::openapi::ObjectBuilder::new()
                    .schema_type(utoipa::openapi::SchemaType::String),
            )
            .required("mood")
    }
}

fn main() {
    let _schema1 = <Pet<u32, String> as utoipa::ToSchema>::schema();
    let _schema2 = <Pet<u32, u32> as utoipa::ToSchema>::schema();
}

As you can see this issue https://github.com/juhaku/utoipa/issues/703 would be fixed by this.

Additionally it would be possible to implement a macro #[alias], which is mentioned here https://github.com/juhaku/utoipa/issues/790#issue-1970346581.

What are your thoughts on this?

JMLX42 commented 6 months ago

I was thinking of something similar, albeit more focused on the type parameters (and not the properties):

#[derive(serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
struct Pet<T> {
    id: u64,
    name: String,
    age: Option<i32>,
    generic_property: T,
}

struct Pet2(Pet<String>);

impl<'__s> utoipa::ToSchema<'__s> for Pet2 {
    fn schema() -> (&'__s str, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>) {
      Pet::<String>::to_schema_builder()
        .with_type_parameter_alias::<T>("String")
        .build()
    }
}

We already have ObjectBuilder. So maybe ObjectBuilder::with_type_parameter_alias() would be better.

Anyway the crux of it is implementing with_type_parameter_alias::<P>() to be able to replace the token used when building the schema.