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

Auto Generated ActiveEnum String Values And Model Column Names #2170

Merged
merged 9 commits into from
May 28, 2024
32 changes: 28 additions & 4 deletions sea-orm-macros/src/derives/active_enum.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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: {:?}",
Expand All @@ -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;
Expand All @@ -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: {:?}",
Expand All @@ -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 {
Expand Down Expand Up @@ -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");
}));
}
}
Expand All @@ -165,6 +179,7 @@ impl ActiveEnum {
ident: variant.ident,
string_value,
num_value,
rename: rename_rule,
});
}

Expand All @@ -175,6 +190,7 @@ impl ActiveEnum {
db_type: db_type?,
is_string,
variants,
rename_all: rename_all_rule,
})
}

Expand All @@ -192,6 +208,7 @@ impl ActiveEnum {
db_type,
is_string,
variants,
rename_all,
} = self;

let variant_idents: Vec<syn::Ident> = variants
Expand All @@ -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");
}
}
})
Expand All @@ -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();

Expand Down
4 changes: 4 additions & 0 deletions sea-orm-macros/src/derives/active_enum_display.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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;
Expand All @@ -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: {:?}",
Expand Down
11 changes: 10 additions & 1 deletion sea-orm-macros/src/derives/entity_model.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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"))
Expand All @@ -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,
Expand All @@ -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| {
Expand Down Expand Up @@ -106,14 +112,17 @@ 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
Some(original_field_name.to_snake_case())
} 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)]
Expand Down
28 changes: 22 additions & 6 deletions sea-orm-macros/src/strum/helpers/case_style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,15 +25,15 @@ pub enum CaseStyle {

const VALID_CASE_STYLES: &[&str] = &[
"camelCase",
"PascalCase",
"kebab-case",
"snake_case",
"mixed_case",
"SCREAMING_SNAKE_CASE",
"SCREAMING-KEBAB-CASE",
"lowercase",
"UPPERCASE",
"snake_case",
"title_case",
"mixed_case",
"UPPERCASE",
"lowercase",
"SCREAMING-KEBAB-CASE",
"PascalCase",
];

impl Parse for CaseStyle {
Expand Down Expand Up @@ -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());
Expand Down
109 changes: 109 additions & 0 deletions sea-orm-macros/tests/derive_active_enum_test.rs
Original file line number Diff line number Diff line change
@@ -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)
);
}