es-entity-macros 0.10.36

Proc macros for es-entity
Documentation
use convert_case::{Case, Casing};
use darling::ToTokens;
use proc_macro2::{Span, TokenStream};
use quote::{TokenStreamExt, quote};

use super::{list_by_fn::CursorStruct, options::*};

pub struct ComboCursor<'a> {
    entity: &'a syn::Ident,
    cursors: Vec<CursorStruct<'a>>,
}

impl<'a> ComboCursor<'a> {
    pub fn new(opts: &'a RepositoryOptions, cursors: Vec<CursorStruct<'a>>) -> Self {
        Self {
            entity: opts.entity(),
            cursors,
        }
    }

    #[cfg(test)]
    pub fn new_test(entity: &'a syn::Ident, cursors: Vec<CursorStruct<'a>>) -> Self {
        Self { entity, cursors }
    }

    pub fn ident(&self) -> syn::Ident {
        let entity_name = format!("{}", self.entity);
        syn::Ident::new(
            &format!("{entity_name}_cursor").to_case(Case::UpperCamel),
            Span::call_site(),
        )
    }

    pub fn tag(column: &Column) -> syn::Ident {
        let tag_name = format!("By{}", column.name());
        syn::Ident::new(&tag_name, Span::call_site())
    }

    pub fn variants(&self) -> TokenStream {
        let variants = self
            .cursors
            .iter()
            .map(|cursor| {
                let tag = Self::tag(cursor.column);
                let ident = cursor.ident();
                quote! {
                    #tag(#ident),
                }
            })
            .collect::<TokenStream>();

        quote! {
            #variants
        }
    }

    pub fn trait_impls(&self) -> TokenStream {
        let self_ident = self.ident();
        let trait_impls = self
            .cursors
            .iter()
            .map(|cursor| {
                let tag =
                    syn::Ident::new(&format!("By{}", cursor.column.name()), Span::call_site());
                let ident = cursor.ident();
                quote! {
                    impl From<#ident> for #self_ident {
                        fn from(cursor: #ident) -> Self {
                            Self::#tag(cursor)
                        }
                    }

                    impl TryFrom<#self_ident> for #ident {
                        type Error = es_entity::CursorDestructureError;

                        fn try_from(cursor: #self_ident) -> Result<Self, Self::Error> {
                            match cursor {
                                #self_ident::#tag(cursor) => Ok(cursor),
                                _ => Err(es_entity::CursorDestructureError::from((stringify!(#self_ident), stringify!(#ident)))),
                            }
                        }
                    }
                }
            })
            .collect::<TokenStream>();

        quote! {
            #trait_impls
        }
    }

    pub fn sort_by_name(&self) -> syn::Ident {
        let entity_name = format!("{}", self.entity);
        syn::Ident::new(
            &format!("{entity_name}_sort_by").to_case(Case::UpperCamel),
            Span::call_site(),
        )
    }

    pub fn sort_by(&self) -> TokenStream {
        let mut default = true;
        let variants = self.cursors.iter().map(|cursor| {
            let name = syn::Ident::new(
                &format!("{}", cursor.column.name()).to_case(Case::UpperCamel),
                Span::call_site(),
            );
            if default {
                default = false;
                quote! {
                    #[default]
                    #name
                }
            } else {
                quote! {
                    #name
                }
            }
        });
        let name = self.sort_by_name();
        #[cfg(feature = "graphql")]
        let mod_name = syn::Ident::new(&format!("{name}").to_case(Case::Snake), Span::call_site());
        #[cfg(feature = "graphql")]
        let sort_by_enum = quote! {
            mod #mod_name {
                #[derive(es_entity::graphql::async_graphql::Enum, Default, Debug, Clone, Copy, PartialEq, Eq)]
                #[graphql(crate = "es_entity::graphql::async_graphql")]
                pub enum #name {
                    #(#variants),*
                }
            }
            pub use #mod_name::#name;
        };
        #[cfg(not(feature = "graphql"))]
        let sort_by_enum = quote! {
            #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
            pub enum #name {
                #(#variants),*
            }
        };
        quote! {
            #sort_by_enum
        }
    }

    #[cfg(feature = "graphql")]
    pub fn gql_cursor(&self) -> TokenStream {
        let ident = self.ident();
        quote! {
            impl es_entity::graphql::async_graphql::connection::CursorType for #ident {
                type Error = String;

                fn encode_cursor(&self) -> String {
                    use es_entity::graphql::base64::{engine::general_purpose, Engine as _};
                    let json = es_entity::prelude::serde_json::to_string(&self).expect("could not serialize token");
                    general_purpose::STANDARD_NO_PAD.encode(json.as_bytes())
                }

                fn decode_cursor(s: &str) -> Result<Self, Self::Error> {
                    use es_entity::graphql::base64::{engine::general_purpose, Engine as _};
                    let bytes = general_purpose::STANDARD_NO_PAD
                        .decode(s.as_bytes())
                        .map_err(|e| e.to_string())?;
                    let json = String::from_utf8(bytes).map_err(|e| e.to_string())?;
                    es_entity::prelude::serde_json::from_str(&json).map_err(|e| e.to_string())
                }
            }
        }
    }
}

impl ToTokens for ComboCursor<'_> {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let ident = self.ident();
        let variants = self.variants();
        let trait_impls = self.trait_impls();

        tokens.append_all(quote! {
            #[derive(Debug, serde::Serialize, serde::Deserialize)]
            #[allow(clippy::enum_variant_names)]
            #[serde(tag = "type")]
            pub enum #ident {
                #variants
            }

            #trait_impls
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::repo::list_by_fn::CursorStruct;
    use proc_macro2::Span;
    use syn::Ident;

    #[test]
    fn combo_cursor_generation() {
        let entity = Ident::new("User", Span::call_site());
        let cursor_mod = Ident::new("cursor_mod", Span::call_site());
        let id = syn::Ident::new("UserId", Span::call_site());

        let id_column = Column::for_id(syn::parse_str("UserId").unwrap());
        let name_column = Column::new(
            syn::Ident::new("name", proc_macro2::Span::call_site()),
            syn::parse_str("String").unwrap(),
        );

        let id_cursor = CursorStruct {
            column: &id_column,
            id: &id,
            entity: &entity,
            cursor_mod: &cursor_mod,
        };

        let name_cursor = CursorStruct {
            column: &name_column,
            id: &id,
            entity: &entity,
            cursor_mod: &cursor_mod,
        };

        let cursors = vec![id_cursor, name_cursor];

        let combo_cursor = ComboCursor {
            entity: &entity,
            cursors,
        };

        let mut tokens = TokenStream::new();
        combo_cursor.to_tokens(&mut tokens);

        let expected = quote! {
            #[derive(Debug, serde::Serialize, serde::Deserialize)]
            #[allow(clippy::enum_variant_names)]
            #[serde(tag = "type")]
            pub enum UserCursor {
                Byid(UserByIdCursor),
                Byname(UserByNameCursor),
            }

            impl From<UserByIdCursor> for UserCursor {
                fn from(cursor: UserByIdCursor) -> Self {
                    Self::Byid(cursor)
                }
            }

            impl TryFrom<UserCursor> for UserByIdCursor {
                type Error = es_entity::CursorDestructureError;

                fn try_from(cursor: UserCursor) -> Result<Self, Self::Error> {
                    match cursor {
                        UserCursor::Byid(cursor) => Ok(cursor),
                        _ => Err(es_entity::CursorDestructureError::from((stringify!(UserCursor), stringify!(UserByIdCursor)))),
                    }
                }
            }
            impl From<UserByNameCursor> for UserCursor {
                fn from(cursor: UserByNameCursor) -> Self {
                    Self::Byname(cursor)
                }
            }

            impl TryFrom<UserCursor> for UserByNameCursor {
                type Error = es_entity::CursorDestructureError;

                fn try_from(cursor: UserCursor) -> Result<Self, Self::Error> {
                    match cursor {
                        UserCursor::Byname(cursor) => Ok(cursor),
                        _ => Err(es_entity::CursorDestructureError::from((stringify!(UserCursor), stringify!(UserByNameCursor)))),
                    }
                }
            }
        };

        assert_eq!(tokens.to_string(), expected.to_string());
    }

    #[test]
    fn combo_cursor_sort_by_generation() {
        let entity = Ident::new("Order", Span::call_site());
        let cursor_mod = Ident::new("cursor_mod", Span::call_site());
        let id = syn::Ident::new("OrderId", Span::call_site());

        let id_column = Column::for_id(syn::parse_str("OrderId").unwrap());
        let status_column = Column::new(
            syn::Ident::new("status", proc_macro2::Span::call_site()),
            syn::parse_str("String").unwrap(),
        );
        let created_at_column = Column::new(
            syn::Ident::new("created_at", proc_macro2::Span::call_site()),
            syn::parse_str("chrono::DateTime<chrono::Utc>").unwrap(),
        );

        let id_cursor = CursorStruct {
            column: &id_column,
            id: &id,
            entity: &entity,
            cursor_mod: &cursor_mod,
        };

        let status_cursor = CursorStruct {
            column: &status_column,
            id: &id,
            entity: &entity,
            cursor_mod: &cursor_mod,
        };

        let created_at_cursor = CursorStruct {
            column: &created_at_column,
            id: &id,
            entity: &entity,
            cursor_mod: &cursor_mod,
        };

        let cursors = vec![id_cursor, status_cursor, created_at_cursor];

        let combo_cursor = ComboCursor {
            entity: &entity,
            cursors,
        };

        let sort_by_tokens = combo_cursor.sort_by();

        let expected = quote! {
            #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
            pub enum OrderSortBy {
                #[default]
                Id,
                Status,
                CreatedAt
            }
        };

        assert_eq!(sort_by_tokens.to_string(), expected.to_string());
    }
}