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 <ccw.billy.123@gmail.com>
This commit is contained in:
Anshul Sanghi 2024-05-29 00:32:41 +05:30 committed by GitHub
parent 17b4081444
commit 33230ab3ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 214 additions and 12 deletions

View File

@ -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<ActiveEnumVariant>,
rename_all: Option<CaseStyle>,
}
struct ActiveEnumVariant {
ident: syn::Ident,
string_value: Option<LitStr>,
num_value: Option<LitInt>,
rename: Option<CaseStyle>,
}
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::<LitStr>()?;
} 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<syn::Ident> = 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();

View File

@ -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::<LitInt>()?;
} else if meta.path.is_ident("display_value") {
display_value = meta.value()?.parse::<LitStr>()?.to_token_stream();
} else if meta.path.is_ident("rename") {
CaseStyle::try_from(&meta)?;
} else {
return Err(meta.error(format!(
"Unknown attribute parameter found: {:?}",

View File

@ -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<Attribute>) -> syn::Res
let mut comment = quote! {None};
let mut schema_name = quote! { None };
let mut table_iden = false;
let mut rename_all: Option<CaseStyle> = 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<Attribute>) -> 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<Attribute>) -> 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<Attribute>) -> 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<Attribute>) -> 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)]

View File

@ -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<Self, Self::Error> {
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());

View File

@ -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)
);
}

View File

@ -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<String> = Column::iter().map(|item| item.to_string()).collect();
assert_eq!(
columns,
vec![
"id",
"username",
"firstName",
"middleName",
"lAsTnAmE",
"ordersCount",
]
);
}