Skip to content

Commit

Permalink
WIP: Add Basic Support For Providing String Value For ActiveEnum Vari…
Browse files Browse the repository at this point in the history
…ants Based On Renaming Rules SeaQL#2160
  • Loading branch information
anshap1719 committed Mar 22, 2024
1 parent e7a4909 commit ab04027
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 3 deletions.
26 changes: 23 additions & 3 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::derives::enum_variant_rename::RenameRule;
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<RenameRule>,
}

struct ActiveEnumVariant {
ident: syn::Ident,
string_value: Option<LitStr>,
num_value: Option<LitInt>,
rename: Option<RenameRule>,
}

impl ActiveEnum {
Expand All @@ -37,6 +40,8 @@ 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 +72,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 +93,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 +115,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 @@ -123,7 +135,7 @@ impl ActiveEnum {
}));
}

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 +167,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 +177,7 @@ impl ActiveEnum {
ident: variant.ident,
string_value,
num_value,
rename: rename_rule,
});
}

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

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

let variant_idents: Vec<syn::Ident> = variants
Expand All @@ -209,10 +224,15 @@ 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.to_string();
let variant_ident = rename_rule.apply_to_variant(&variant_ident);

quote! { #variant_ident }
} else {
quote_spanned! {
variant_span => compile_error!("Missing macro attribute, either `string_value` or `num_value` should be specified");
}
}
}
})
.collect();
Expand Down
141 changes: 141 additions & 0 deletions sea-orm-macros/src/derives/enum_variant_rename.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use std::fmt;
use std::fmt::{Debug, Display};
use syn::meta::ParseNestedMeta;
use syn::Error;
use syn::LitStr;
use RenameRule::*;

#[derive(Copy, Clone, PartialEq)]
pub enum RenameRule {
/// Rename direct children to "lowercase" style.
LowerCase,
/// Rename direct children to "UPPERCASE" style.
UpperCase,
/// Rename direct children to "PascalCase" style, as typically used for
/// enum variants.
PascalCase,
/// Rename direct children to "camelCase" style.
CamelCase,
/// Rename direct children to "snake_case" style, as commonly used for
/// fields.
SnakeCase,
/// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly
/// used for constants.
ScreamingSnakeCase,
/// Rename direct children to "kebab-case" style.
KebabCase,
/// Rename direct children to "SCREAMING-KEBAB-CASE" style.
ScreamingKebabCase,
}

static RENAME_RULES: &[(&str, RenameRule)] = &[
("lowercase", LowerCase),
("UPPERCASE", UpperCase),
("PascalCase", PascalCase),
("camelCase", CamelCase),
("snake_case", SnakeCase),
("SCREAMING_SNAKE_CASE", ScreamingSnakeCase),
("kebab-case", KebabCase),
("SCREAMING-KEBAB-CASE", ScreamingKebabCase),
];

impl RenameRule {
pub fn from_str(rename_all_str: &str) -> Result<Self, ParseError> {
for (name, rule) in RENAME_RULES {
if rename_all_str == *name {
return Ok(*rule);
}
}
Err(ParseError {
unknown: rename_all_str,
})
}

/// Apply a renaming rule to an enum variant, returning the version expected in the source.
pub fn apply_to_variant(self, variant: &str) -> String {
match self {
PascalCase => variant.to_owned(),
LowerCase => variant.to_ascii_lowercase(),
UpperCase => variant.to_ascii_uppercase(),
CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..],
SnakeCase => {
let mut snake = String::new();
for (i, ch) in variant.char_indices() {
if i > 0 && ch.is_uppercase() {
snake.push('_');
}
snake.push(ch.to_ascii_lowercase());
}
snake
}
ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(),
KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"),
ScreamingKebabCase => ScreamingSnakeCase
.apply_to_variant(variant)
.replace('_', "-"),
}
}

/// Apply a renaming rule to a struct field, returning the version expected in the source.
pub fn apply_to_field(self, field: &str) -> String {
match self {
LowerCase | SnakeCase => field.to_owned(),
UpperCase => field.to_ascii_uppercase(),
PascalCase => {
let mut pascal = String::new();
let mut capitalize = true;
for ch in field.chars() {
if ch == '_' {
capitalize = true;
} else if capitalize {
pascal.push(ch.to_ascii_uppercase());
capitalize = false;
} else {
pascal.push(ch);
}
}
pascal
}
CamelCase => {
let pascal = PascalCase.apply_to_field(field);
pascal[..1].to_ascii_lowercase() + &pascal[1..]
}
ScreamingSnakeCase => field.to_ascii_uppercase(),
KebabCase => field.replace('_', "-"),
ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"),
}
}
}

pub struct ParseError<'a> {
unknown: &'a str,
}

impl<'a> Display for ParseError<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("unknown rename rule `rename_all = ")?;
Debug::fmt(self.unknown, f)?;
f.write_str("`, expected one of ")?;
for (i, (name, _rule)) in RENAME_RULES.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
Debug::fmt(name, f)?;
}
Ok(())
}
}

impl<'meta> TryFrom<&ParseNestedMeta<'meta>> for RenameRule {
type Error = Error;

fn try_from(value: &ParseNestedMeta) -> Result<Self, Self::Error> {
let meta_string_literal: LitStr = value.value()?.parse()?;
match RenameRule::from_str(meta_string_literal.value().as_str()) {
Ok(rule) => Ok(rule),
Err(error) => Err(value.error(format!(
"Unknown value for attribute parameter `rename_all`: {error}",
))),
}
}
}
1 change: 1 addition & 0 deletions sea-orm-macros/src/derives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod column;
mod derive_iden;
mod entity;
mod entity_model;
mod enum_variant_rename;
mod from_query_result;
mod into_active_model;
mod migration;
Expand Down
15 changes: 15 additions & 0 deletions sea-orm-macros/tests/derive_value_type_test.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use sea_orm_macros::DeriveActiveEnum;

#[test]
fn when_user_import_nothing_macro_still_works_test() {
#[derive(sea_orm::DeriveValueType)]
Expand All @@ -11,3 +13,16 @@ fn when_user_alias_result_macro_still_works_test() {
#[derive(sea_orm::DeriveValueType)]
struct MyString(String);
}

#[derive(DeriveActiveEnum)]
#[sea_orm(
rs_type = "String",
db_type = "Enum",
enum_name = "test_enum",
rename_all = "camelCase"
)]
pub enum TestEnum {
Variant,
#[sea_orm(rename = "PascalCase")]
AnotherVariant,
}

0 comments on commit ab04027

Please sign in to comment.