From 33230ab3ade52f3dab163b9dea14d39d3e6eb90e Mon Sep 17 00:00:00 2001 From: Anshul Sanghi Date: Wed, 29 May 2024 00:32:41 +0530 Subject: [PATCH] Auto Generated ActiveEnum String Values And Model Column Names (#2170) * WIP: Add Basic Support For Providing String Value For ActiveEnum Variants Based On Renaming Rules #2160 * WIP: Use Existing Case Style Handlers For Enum Rename Rules, Add Unit Tests For DeriveActiveEnum rename rules #2160 * WIP: Improve Implementation Of Name Case Parameters In ActiveEnum Macros #2160 * WIP: Implement Case Styles Based Name Generation For Columns In A Model #2160 * Fix Formatting #2160 * Rename Column Name And Enum Variant Renaming Macro Attributes #2160 * Revert Adding `Rename` Attribute For Column Names #2160 * Revert Unintended Formatting Changes #2160 * Fix formatting --------- Co-authored-by: Billy Chan --- sea-orm-macros/src/derives/active_enum.rs | 32 ++++- .../src/derives/active_enum_display.rs | 4 + sea-orm-macros/src/derives/entity_model.rs | 11 +- .../src/strum/helpers/case_style.rs | 30 +++-- .../tests/derive_active_enum_test.rs | 109 ++++++++++++++++++ .../derive_entity_model_column_name_test.rs | 40 +++++++ 6 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 sea-orm-macros/tests/derive_active_enum_test.rs create mode 100644 sea-orm-macros/tests/derive_entity_model_column_name_test.rs diff --git a/sea-orm-macros/src/derives/active_enum.rs b/sea-orm-macros/src/derives/active_enum.rs index 41813499..bfd41c51 100644 --- a/sea-orm-macros/src/derives/active_enum.rs +++ b/sea-orm-macros/src/derives/active_enum.rs @@ -1,4 +1,5 @@ use super::util::camel_case_with_escaped_non_uax31; +use crate::strum::helpers::case_style::{CaseStyle, CaseStyleHelpers}; use heck::ToUpperCamelCase; use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned}; @@ -17,12 +18,14 @@ struct ActiveEnum { db_type: TokenStream, is_string: bool, variants: Vec, + rename_all: Option, } struct ActiveEnumVariant { ident: syn::Ident, string_value: Option, num_value: Option, + rename: Option, } impl ActiveEnum { @@ -37,6 +40,7 @@ impl ActiveEnum { let mut db_type = Err(Error::TT(quote_spanned! { ident_span => compile_error!("Missing macro attribute `db_type`"); })); + let mut rename_all_rule = None; input .attrs @@ -67,6 +71,8 @@ impl ActiveEnum { } else if meta.path.is_ident("enum_name") { let litstr: LitStr = meta.value()?.parse()?; enum_name = litstr.value(); + } else if meta.path.is_ident("rename_all") { + rename_all_rule = Some((&meta).try_into()?); } else { return Err(meta.error(format!( "Unknown attribute parameter found: {:?}", @@ -86,10 +92,13 @@ impl ActiveEnum { let mut is_string = false; let mut is_int = false; let mut variants = Vec::new(); + for variant in variant_vec { let variant_span = variant.ident.span(); let mut string_value = None; let mut num_value = None; + let mut rename_rule = None; + for attr in variant.attrs.iter() { if !attr.path().is_ident("sea_orm") { continue; @@ -105,6 +114,8 @@ impl ActiveEnum { // This is a placeholder to prevent the `display_value` proc_macro attribute of `DeriveDisplay` // to be considered unknown attribute parameter meta.value()?.parse::()?; + } else if meta.path.is_ident("rename") { + rename_rule = Some((&meta).try_into()?); } else { return Err(meta.error(format!( "Unknown attribute parameter found: {:?}", @@ -117,13 +128,16 @@ impl ActiveEnum { .map_err(Error::Syn)?; } - if is_string && is_int { + if (is_string || rename_rule.is_some() || rename_all_rule.is_some()) && is_int { return Err(Error::TT(quote_spanned! { ident_span => compile_error!("All enum variants should specify the same `*_value` macro attribute, either `string_value` or `num_value` but not both"); })); } - if string_value.is_none() && num_value.is_none() { + if string_value.is_none() + && num_value.is_none() + && rename_rule.or(rename_all_rule).is_none() + { match variant.discriminant { Some((_, Expr::Lit(exprlit))) => { if let Lit::Int(litint) = exprlit.lit { @@ -155,7 +169,7 @@ impl ActiveEnum { } _ => { return Err(Error::TT(quote_spanned! { - variant_span => compile_error!("Missing macro attribute, either `string_value` or `num_value` should be specified or specify repr[X] and have a value for every entry"); + variant_span => compile_error!("Missing macro attribute, either `string_value`, `num_value` or `rename` should be specified or specify repr[X] and have a value for every entry"); })); } } @@ -165,6 +179,7 @@ impl ActiveEnum { ident: variant.ident, string_value, num_value, + rename: rename_rule, }); } @@ -175,6 +190,7 @@ impl ActiveEnum { db_type: db_type?, is_string, variants, + rename_all: rename_all_rule, }) } @@ -192,6 +208,7 @@ impl ActiveEnum { db_type, is_string, variants, + rename_all, } = self; let variant_idents: Vec = variants @@ -209,9 +226,13 @@ impl ActiveEnum { quote! { #string } } else if let Some(num_value) = &variant.num_value { quote! { #num_value } + } else if let Some(rename_rule) = variant.rename.or(*rename_all) { + let variant_ident = variant.ident.convert_case(Some(rename_rule)); + + quote! { #variant_ident } } else { quote_spanned! { - variant_span => compile_error!("Missing macro attribute, either `string_value` or `num_value` should be specified"); + variant_span => compile_error!("Missing macro attribute, either `string_value`, `num_value` or `rename_all` should be specified"); } } }) @@ -232,6 +253,9 @@ impl ActiveEnum { .string_value .as_ref() .map(|string_value| string_value.value()) + .or(variant + .rename + .map(|rename| variant.ident.convert_case(Some(rename)))) }) .collect(); diff --git a/sea-orm-macros/src/derives/active_enum_display.rs b/sea-orm-macros/src/derives/active_enum_display.rs index 565e4193..b0bca2db 100644 --- a/sea-orm-macros/src/derives/active_enum_display.rs +++ b/sea-orm-macros/src/derives/active_enum_display.rs @@ -1,3 +1,4 @@ +use crate::strum::helpers::case_style::CaseStyle; use proc_macro2::TokenStream; use quote::{quote, quote_spanned, ToTokens}; use syn::{LitInt, LitStr}; @@ -29,6 +30,7 @@ impl Display { let mut variants = Vec::new(); for variant in variant_vec { let mut display_value = variant.ident.to_string().to_token_stream(); + for attr in variant.attrs.iter() { if !attr.path().is_ident("sea_orm") { continue; @@ -40,6 +42,8 @@ impl Display { meta.value()?.parse::()?; } else if meta.path.is_ident("display_value") { display_value = meta.value()?.parse::()?.to_token_stream(); + } else if meta.path.is_ident("rename") { + CaseStyle::try_from(&meta)?; } else { return Err(meta.error(format!( "Unknown attribute parameter found: {:?}", diff --git a/sea-orm-macros/src/derives/entity_model.rs b/sea-orm-macros/src/derives/entity_model.rs index 6c4f8f27..f7a62380 100644 --- a/sea-orm-macros/src/derives/entity_model.rs +++ b/sea-orm-macros/src/derives/entity_model.rs @@ -1,4 +1,5 @@ use super::util::{escape_rust_keyword, trim_starting_raw_identifier}; +use crate::strum::helpers::case_style::{CaseStyle, CaseStyleHelpers}; use heck::{ToSnakeCase, ToUpperCamelCase}; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; @@ -13,6 +14,8 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res let mut comment = quote! {None}; let mut schema_name = quote! { None }; let mut table_iden = false; + let mut rename_all: Option = None; + attrs .iter() .filter(|attr| attr.path().is_ident("sea_orm")) @@ -28,6 +31,8 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res schema_name = quote! { Some(#name) }; } else if meta.path.is_ident("table_iden") { table_iden = true; + } else if meta.path.is_ident("rename_all") { + rename_all = Some((&meta).try_into()?); } else { // Reads the value expression to advance the parse stream. // Some parameters, such as `primary_key`, do not have any value, @@ -38,6 +43,7 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res Ok(()) }) })?; + let entity_def = table_name .as_ref() .map(|table_name| { @@ -106,7 +112,9 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res let mut ignore = false; let mut unique = false; let mut sql_type = None; - let mut column_name = if original_field_name + let mut column_name = if let Some(case_style) = rename_all { + Some(field_name.convert_case(Some(case_style))) + } else if original_field_name != original_field_name.to_upper_camel_case().to_snake_case() { // `to_snake_case` was used to trim prefix and tailing underscore @@ -114,6 +122,7 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res } else { None }; + let mut enum_name = None; let mut is_primary_key = false; // search for #[sea_orm(primary_key, auto_increment = false, column_type = "String(Some(255))", default_value = "new user", default_expr = "gen_random_uuid()", column_name = "name", enum_name = "Name", nullable, indexed, unique)] diff --git a/sea-orm-macros/src/strum/helpers/case_style.rs b/sea-orm-macros/src/strum/helpers/case_style.rs index 42538260..ef7ec442 100644 --- a/sea-orm-macros/src/strum/helpers/case_style.rs +++ b/sea-orm-macros/src/strum/helpers/case_style.rs @@ -2,6 +2,7 @@ use heck::{ ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase, }; use std::str::FromStr; +use syn::meta::ParseNestedMeta; use syn::{ parse::{Parse, ParseStream}, Ident, LitStr, @@ -24,15 +25,15 @@ pub enum CaseStyle { const VALID_CASE_STYLES: &[&str] = &[ "camelCase", - "PascalCase", "kebab-case", - "snake_case", - "SCREAMING_SNAKE_CASE", - "SCREAMING-KEBAB-CASE", - "lowercase", - "UPPERCASE", - "title_case", "mixed_case", + "SCREAMING_SNAKE_CASE", + "snake_case", + "title_case", + "UPPERCASE", + "lowercase", + "SCREAMING-KEBAB-CASE", + "PascalCase", ]; impl Parse for CaseStyle { @@ -109,6 +110,21 @@ impl CaseStyleHelpers for Ident { } } +impl<'meta> TryFrom<&ParseNestedMeta<'meta>> for CaseStyle { + type Error = syn::Error; + + fn try_from(value: &ParseNestedMeta) -> Result { + let meta_string_literal: LitStr = value.value()?.parse()?; + let value_string = meta_string_literal.value(); + match CaseStyle::from_str(value_string.as_str()) { + Ok(rule) => Ok(rule), + Err(()) => Err(value.error(format!( + "Unknown value for attribute parameter: `{value_string}`. Valid values are: `{VALID_CASE_STYLES:?}`" + ))), + } + } +} + #[test] fn test_convert_case() { let id = Ident::new("test_me", proc_macro2::Span::call_site()); diff --git a/sea-orm-macros/tests/derive_active_enum_test.rs b/sea-orm-macros/tests/derive_active_enum_test.rs new file mode 100644 index 00000000..806d0cc8 --- /dev/null +++ b/sea-orm-macros/tests/derive_active_enum_test.rs @@ -0,0 +1,109 @@ +use sea_orm::ActiveEnum; +use sea_orm_macros::{DeriveActiveEnum, EnumIter}; + +#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "test_enum", + rename_all = "camelCase" +)] +enum TestEnum { + DefaultVariant, + #[sea_orm(rename = "camelCase")] + VariantCamelCase, + #[sea_orm(rename = "kebab-case")] + VariantKebabCase, + #[sea_orm(rename = "mixed_case")] + VariantMixedCase, + #[sea_orm(rename = "SCREAMING_SNAKE_CASE")] + VariantShoutySnakeCase, + #[sea_orm(rename = "snake_case")] + VariantSnakeCase, + #[sea_orm(rename = "title_case")] + VariantTitleCase, + #[sea_orm(rename = "UPPERCASE")] + VariantUpperCase, + #[sea_orm(rename = "lowercase")] + VariantLowerCase, + #[sea_orm(rename = "SCREAMING-KEBAB-CASE")] + VariantScreamingKebabCase, + #[sea_orm(rename = "PascalCase")] + VariantPascalCase, + #[sea_orm(string_value = "CuStOmStRiNgVaLuE")] + CustomStringValue, +} + +#[test] +fn derive_active_enum_value() { + assert_eq!(TestEnum::DefaultVariant.to_value(), "defaultVariant"); + assert_eq!(TestEnum::VariantCamelCase.to_value(), "variantCamelCase"); + assert_eq!(TestEnum::VariantKebabCase.to_value(), "variant-kebab-case"); + assert_eq!(TestEnum::VariantMixedCase.to_value(), "variantMixedCase"); + assert_eq!( + TestEnum::VariantShoutySnakeCase.to_value(), + "VARIANT_SHOUTY_SNAKE_CASE" + ); + assert_eq!(TestEnum::VariantSnakeCase.to_value(), "variant_snake_case"); + assert_eq!(TestEnum::VariantTitleCase.to_value(), "Variant Title Case"); + assert_eq!(TestEnum::VariantUpperCase.to_value(), "VARIANTUPPERCASE"); + assert_eq!(TestEnum::VariantLowerCase.to_value(), "variantlowercase"); + assert_eq!( + TestEnum::VariantScreamingKebabCase.to_value(), + "VARIANT-SCREAMING-KEBAB-CASE" + ); + assert_eq!(TestEnum::VariantPascalCase.to_value(), "VariantPascalCase"); + assert_eq!(TestEnum::CustomStringValue.to_value(), "CuStOmStRiNgVaLuE"); +} + +#[test] +fn derive_active_enum_from_value() { + assert_eq!( + TestEnum::try_from_value(&"defaultVariant".to_string()), + Ok(TestEnum::DefaultVariant) + ); + assert_eq!( + TestEnum::try_from_value(&"variantCamelCase".to_string()), + Ok(TestEnum::VariantCamelCase) + ); + assert_eq!( + TestEnum::try_from_value(&"variant-kebab-case".to_string()), + Ok(TestEnum::VariantKebabCase) + ); + assert_eq!( + TestEnum::try_from_value(&"variantMixedCase".to_string()), + Ok(TestEnum::VariantMixedCase) + ); + assert_eq!( + TestEnum::try_from_value(&"VARIANT_SHOUTY_SNAKE_CASE".to_string()), + Ok(TestEnum::VariantShoutySnakeCase), + ); + assert_eq!( + TestEnum::try_from_value(&"variant_snake_case".to_string()), + Ok(TestEnum::VariantSnakeCase) + ); + assert_eq!( + TestEnum::try_from_value(&"Variant Title Case".to_string()), + Ok(TestEnum::VariantTitleCase) + ); + assert_eq!( + TestEnum::try_from_value(&"VARIANTUPPERCASE".to_string()), + Ok(TestEnum::VariantUpperCase) + ); + assert_eq!( + TestEnum::try_from_value(&"variantlowercase".to_string()), + Ok(TestEnum::VariantLowerCase) + ); + assert_eq!( + TestEnum::try_from_value(&"VARIANT-SCREAMING-KEBAB-CASE".to_string()), + Ok(TestEnum::VariantScreamingKebabCase), + ); + assert_eq!( + TestEnum::try_from_value(&"VariantPascalCase".to_string()), + Ok(TestEnum::VariantPascalCase) + ); + assert_eq!( + TestEnum::try_from_value(&"CuStOmStRiNgVaLuE".to_string()), + Ok(TestEnum::CustomStringValue) + ); +} diff --git a/sea-orm-macros/tests/derive_entity_model_column_name_test.rs b/sea-orm-macros/tests/derive_entity_model_column_name_test.rs new file mode 100644 index 00000000..639dcc8c --- /dev/null +++ b/sea-orm-macros/tests/derive_entity_model_column_name_test.rs @@ -0,0 +1,40 @@ +use sea_orm::prelude::*; +use sea_orm::Iden; +use sea_orm::Iterable; +use sea_orm_macros::DeriveEntityModel; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "user")] +#[sea_orm(rename_all = "camelCase")] +pub struct Model { + #[sea_orm(primary_key)] + id: i32, + username: String, + first_name: String, + middle_name: String, + #[sea_orm(column_name = "lAsTnAmE")] + last_name: String, + orders_count: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +#[test] +fn test_column_names() { + let columns: Vec = Column::iter().map(|item| item.to_string()).collect(); + + assert_eq!( + columns, + vec![ + "id", + "username", + "firstName", + "middleName", + "lAsTnAmE", + "ordersCount", + ] + ); +}