Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: New handling of type aliases - new generics handling #818

Closed
nik-here opened this issue Dec 6, 2023 · 8 comments · Fixed by #1034
Closed

Proposal: New handling of type aliases - new generics handling #818

nik-here opened this issue Dec 6, 2023 · 8 comments · Fixed by #1034
Labels
enhancement New feature or request Generics - Hard Stuff concerning generics implementation

Comments

@nik-here
Copy link

nik-here commented Dec 6, 2023

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 #703 would be fixed by this.

Additionally it would be possible to implement a macro #[alias], which is mentioned here #790 (comment).

What are your thoughts on this?

@JMLX42
Copy link
Contributor

JMLX42 commented Jan 24, 2024

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.

@juhaku
Copy link
Owner

juhaku commented Sep 4, 2024

@nik-here This is pretty good. Well thought implementation. I'll give this some solicitude and see if I can get some kind of solution for this long dragging issue.

@juhaku juhaku added enhancement New feature or request investigate Futher investigation needed before other action Generics - Hard Stuff concerning generics implementation labels Sep 4, 2024
@juhaku
Copy link
Owner

juhaku commented Sep 4, 2024

@JMLX42 @nik-here I think the implementation will be something from between the two. I start to believe that the generics must be wrapped with unnamed struct type as shown above.

struct Pet2(Pet<String>);

Otherwise they wont work because we need the name for the generic schema which then will be the unnamed struct name.

The unnamed struct can be used to reference the schema in the codebase. Directly inlining the instance of generic type definitions would not know which type to refer in schema. Maybe if possible it could directly create inlined schema in such a case.

That said if there will be a new implementation I will most certainly scrap the current aliases implementation.

@juhaku juhaku changed the title Proposal: New handling of type aliases Proposal: New handling of type aliases - new generics handling Sep 4, 2024
@JMLX42
Copy link
Contributor

JMLX42 commented Sep 4, 2024

@juhaku I have a new working proposal we've been working for with a colleague. I'll try to post it in the upcoming days.

@juhaku
Copy link
Owner

juhaku commented Sep 4, 2024

@JMLX42 Please do, and thanks for helping out ❤️

@juhaku
Copy link
Owner

juhaku commented Sep 7, 2024

I have now a draft design in my head of how it at least in theory could work. And I am ready to test it out.

@juhaku juhaku removed the investigate Futher investigation needed before other action label Sep 9, 2024
@JMLX42
Copy link
Contributor

JMLX42 commented Sep 9, 2024

We have a macro that allows to build this:

#[derive(Serialize, ToSchemaAlias)]
#[alias(type_parameter(T = MyType))]
struct MeshLinks(Links<MyType>);

Where Links<T> derive ToSchema.

The macro iterates over the schema built by Links<T>::to_schema() to:

  • replace OpenAPI $refs to T by refs to the type specified using type_parameter(T = ...)
  • evaluates/replace refs to T members in strings (such as descriptions), so the OpenAPI attributes can also be "templated" (as in a template rendering engine for text)

@juhaku
Copy link
Owner

juhaku commented Sep 10, 2024

Nice idea, This allows using references in generic schemas whereas the current new implementation will always inline the generic implementations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request Generics - Hard Stuff concerning generics implementation
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

3 participants