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:
parent
17b4081444
commit
33230ab3ad
@ -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();
|
||||
|
||||
|
@ -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: {:?}",
|
||||
|
@ -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)]
|
||||
|
@ -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());
|
||||
|
109
sea-orm-macros/tests/derive_active_enum_test.rs
Normal file
109
sea-orm-macros/tests/derive_active_enum_test.rs
Normal 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)
|
||||
);
|
||||
}
|
40
sea-orm-macros/tests/derive_entity_model_column_name_test.rs
Normal file
40
sea-orm-macros/tests/derive_entity_model_column_name_test.rs
Normal 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",
|
||||
]
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user