use crate::{ColIdx, ColumnDef, DbErr, Iterable, QueryResult, TryFromU64, TryGetError, TryGetable}; use sea_query::{DynIden, Expr, Nullable, SimpleExpr, Value, ValueType}; /// A Rust representation of enum defined in database. /// /// # Implementations /// /// You can implement [ActiveEnum] manually by hand or use the derive macro [DeriveActiveEnum](sea_orm_macros::DeriveActiveEnum). /// /// # Examples /// /// Implementing it manually versus using the derive macro [DeriveActiveEnum](sea_orm_macros::DeriveActiveEnum). /// /// > See [DeriveActiveEnum](sea_orm_macros::DeriveActiveEnum) for the full specification of macro attributes. /// /// ```rust /// use sea_orm::entity::prelude::*; /// /// // Using the derive macro /// #[derive(Debug, PartialEq, EnumIter, DeriveActiveEnum, DeriveDisplay)] /// #[sea_orm( /// rs_type = "String", /// db_type = "String(Some(1))", /// enum_name = "category" /// )] /// pub enum DeriveCategory { /// #[sea_orm(string_value = "B")] /// Big, /// #[sea_orm(string_value = "S")] /// Small, /// } /// /// // Implementing it manually /// #[derive(Debug, PartialEq, EnumIter)] /// pub enum Category { /// Big, /// Small, /// } /// /// #[derive(Debug, DeriveIden)] /// pub struct CategoryEnum; /// /// impl ActiveEnum for Category { /// // The macro attribute `rs_type` is being pasted here /// type Value = String; /// /// type ValueVec = Vec; /// /// // Will be atomically generated by `DeriveActiveEnum` /// fn name() -> DynIden { /// SeaRc::new(CategoryEnum) /// } /// /// // Will be atomically generated by `DeriveActiveEnum` /// fn to_value(&self) -> Self::Value { /// match self { /// Self::Big => "B", /// Self::Small => "S", /// } /// .to_owned() /// } /// /// // Will be atomically generated by `DeriveActiveEnum` /// fn try_from_value(v: &Self::Value) -> Result { /// match v.as_ref() { /// "B" => Ok(Self::Big), /// "S" => Ok(Self::Small), /// _ => Err(DbErr::Type(format!( /// "unexpected value for Category enum: {}", /// v /// ))), /// } /// } /// /// fn db_type() -> ColumnDef { /// // The macro attribute `db_type` is being pasted here /// ColumnType::String(Some(1)).def() /// } /// } /// ``` /// /// Using [ActiveEnum] on Model. /// /// ``` /// use sea_orm::entity::prelude::*; /// /// // Define the `Category` active enum /// #[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, DeriveDisplay)] /// #[sea_orm(rs_type = "String", db_type = "String(Some(1))")] /// pub enum Category { /// #[sea_orm(string_value = "B")] /// Big, /// #[sea_orm(string_value = "S")] /// Small, /// } /// /// #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] /// #[sea_orm(table_name = "active_enum")] /// pub struct Model { /// #[sea_orm(primary_key)] /// pub id: i32, /// // Represents a db column using `Category` active enum /// pub category: Category, /// pub category_opt: Option, /// } /// /// #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] /// pub enum Relation {} /// /// impl ActiveModelBehavior for ActiveModel {} /// ``` pub trait ActiveEnum: Sized + Iterable { /// Define the Rust type that each enum variant corresponds. type Value: ActiveEnumValue; /// This has no purpose. It will be removed in the next major version. type ValueVec; /// Get the name of enum fn name() -> DynIden; /// Convert enum variant into the corresponding value. fn to_value(&self) -> Self::Value; /// Try to convert the corresponding value into enum variant. fn try_from_value(v: &Self::Value) -> Result; /// Get the database column definition of this active enum. fn db_type() -> ColumnDef; /// Convert an owned enum variant into the corresponding value. fn into_value(self) -> Self::Value { Self::to_value(&self) } /// Construct a enum expression with casting fn as_enum(&self) -> SimpleExpr { Expr::val(Self::to_value(self)).as_enum(Self::name()) } /// Get the name of all enum variants fn values() -> Vec { Self::iter().map(Self::into_value).collect() } } /// The Rust Value backing ActiveEnums pub trait ActiveEnumValue: Into + ValueType + Nullable + TryGetable { /// For getting an array of enum. Postgres only fn try_get_vec_by(res: &QueryResult, index: I) -> Result, TryGetError>; } macro_rules! impl_active_enum_value { ($type:ident) => { impl ActiveEnumValue for $type { fn try_get_vec_by( _res: &QueryResult, _index: I, ) -> Result, TryGetError> { panic!("Not supported by `postgres-array`") } } }; } macro_rules! impl_active_enum_value_with_pg_array { ($type:ident) => { impl ActiveEnumValue for $type { fn try_get_vec_by( _res: &QueryResult, _index: I, ) -> Result, TryGetError> { #[cfg(feature = "postgres-array")] { >::try_get_by(_res, _index) } #[cfg(not(feature = "postgres-array"))] panic!("`postgres-array` is not enabled") } } }; } impl_active_enum_value!(u8); impl_active_enum_value!(u16); impl_active_enum_value!(u32); impl_active_enum_value!(u64); impl_active_enum_value_with_pg_array!(String); impl_active_enum_value_with_pg_array!(i8); impl_active_enum_value_with_pg_array!(i16); impl_active_enum_value_with_pg_array!(i32); impl_active_enum_value_with_pg_array!(i64); impl TryFromU64 for T where T: ActiveEnum, { fn try_from_u64(_: u64) -> Result { Err(DbErr::ConvertFromU64( "Fail to construct ActiveEnum from a u64, if your primary key consist of a ActiveEnum field, its auto increment should be set to false." )) } } #[cfg(test)] mod tests { use crate as sea_orm; use crate::{error::*, sea_query::SeaRc, *}; use pretty_assertions::assert_eq; #[test] fn active_enum_string() { #[derive(Debug, PartialEq, Eq, EnumIter)] pub enum Category { Big, Small, } #[derive(Debug, DeriveIden)] #[sea_orm(iden = "category")] pub struct CategoryEnum; impl ActiveEnum for Category { type Value = String; type ValueVec = Vec; fn name() -> DynIden { SeaRc::new(CategoryEnum) } fn to_value(&self) -> Self::Value { match self { Self::Big => "B", Self::Small => "S", } .to_owned() } fn try_from_value(v: &Self::Value) -> Result { match v.as_ref() { "B" => Ok(Self::Big), "S" => Ok(Self::Small), _ => Err(type_err(format!("unexpected value for Category enum: {v}"))), } } fn db_type() -> ColumnDef { ColumnType::String(Some(1)).def() } } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay)] #[sea_orm( rs_type = "String", db_type = "String(Some(1))", enum_name = "category" )] pub enum DeriveCategory { #[sea_orm(string_value = "B")] Big, #[sea_orm(string_value = "S")] Small, } assert_eq!(Category::Big.to_value(), "B".to_owned()); assert_eq!(Category::Small.to_value(), "S".to_owned()); assert_eq!(DeriveCategory::Big.to_value(), "B".to_owned()); assert_eq!(DeriveCategory::Small.to_value(), "S".to_owned()); assert_eq!( Category::try_from_value(&"A".to_owned()).err(), Some(type_err("unexpected value for Category enum: A")) ); assert_eq!( Category::try_from_value(&"B".to_owned()).ok(), Some(Category::Big) ); assert_eq!( Category::try_from_value(&"S".to_owned()).ok(), Some(Category::Small) ); assert_eq!( DeriveCategory::try_from_value(&"A".to_owned()).err(), Some(type_err("unexpected value for DeriveCategory enum: A")) ); assert_eq!( DeriveCategory::try_from_value(&"B".to_owned()).ok(), Some(DeriveCategory::Big) ); assert_eq!( DeriveCategory::try_from_value(&"S".to_owned()).ok(), Some(DeriveCategory::Small) ); assert_eq!(Category::db_type(), ColumnType::String(Some(1)).def()); assert_eq!(DeriveCategory::db_type(), ColumnType::String(Some(1)).def()); assert_eq!( Category::name().to_string(), DeriveCategory::name().to_string() ); assert_eq!(Category::values(), DeriveCategory::values()); assert_eq!(format!("{}", DeriveCategory::Big), "Big"); assert_eq!(format!("{}", DeriveCategory::Small), "Small"); } #[test] fn active_enum_derive_signed_integers() { macro_rules! test_num_value_int { ($ident: ident, $rs_type: expr, $db_type: expr, $col_def: ident) => { #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay)] #[sea_orm(rs_type = $rs_type, db_type = $db_type)] pub enum $ident { #[sea_orm(num_value = -10)] Negative, #[sea_orm(num_value = 1)] Big, #[sea_orm(num_value = 0)] Small, } test_int!($ident, $rs_type, $db_type, $col_def); }; } macro_rules! test_fallback_int { ($ident: ident, $fallback_type: ident, $rs_type: expr, $db_type: expr, $col_def: ident) => { #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay)] #[sea_orm(rs_type = $rs_type, db_type = $db_type)] #[repr(i32)] pub enum $ident { Big = 1, Small = 0, Negative = -10, } test_int!($ident, $rs_type, $db_type, $col_def); }; } macro_rules! test_int { ($ident: ident, $rs_type: expr, $db_type: expr, $col_def: ident) => { assert_eq!($ident::Big.to_value(), 1); assert_eq!($ident::Small.to_value(), 0); assert_eq!($ident::Negative.to_value(), -10); assert_eq!($ident::try_from_value(&1).ok(), Some($ident::Big)); assert_eq!($ident::try_from_value(&0).ok(), Some($ident::Small)); assert_eq!($ident::try_from_value(&-10).ok(), Some($ident::Negative)); assert_eq!( $ident::try_from_value(&2).err(), Some(type_err(format!( "unexpected value for {} enum: 2", stringify!($ident) ))) ); assert_eq!($ident::db_type(), ColumnType::$col_def.def()); assert_eq!(format!("{}", $ident::Big), "Big"); assert_eq!(format!("{}", $ident::Small), "Small"); assert_eq!(format!("{}", $ident::Negative), "Negative"); }; } test_num_value_int!(I8, "i8", "TinyInteger", TinyInteger); test_num_value_int!(I16, "i16", "SmallInteger", SmallInteger); test_num_value_int!(I32, "i32", "Integer", Integer); test_num_value_int!(I64, "i64", "BigInteger", BigInteger); test_fallback_int!(I8Fallback, i8, "i8", "TinyInteger", TinyInteger); test_fallback_int!(I16Fallback, i16, "i16", "SmallInteger", SmallInteger); test_fallback_int!(I32Fallback, i32, "i32", "Integer", Integer); test_fallback_int!(I64Fallback, i64, "i64", "BigInteger", BigInteger); } #[test] fn active_enum_derive_unsigned_integers() { macro_rules! test_num_value_uint { ($ident: ident, $rs_type: expr, $db_type: expr, $col_def: ident) => { #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay)] #[sea_orm(rs_type = $rs_type, db_type = $db_type)] pub enum $ident { #[sea_orm(num_value = 1)] Big, #[sea_orm(num_value = 0)] Small, } test_uint!($ident, $rs_type, $db_type, $col_def); }; } macro_rules! test_fallback_uint { ($ident: ident, $fallback_type: ident, $rs_type: expr, $db_type: expr, $col_def: ident) => { #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay)] #[sea_orm(rs_type = $rs_type, db_type = $db_type)] #[repr($fallback_type)] pub enum $ident { Big = 1, Small = 0, } test_uint!($ident, $rs_type, $db_type, $col_def); }; } macro_rules! test_uint { ($ident: ident, $rs_type: expr, $db_type: expr, $col_def: ident) => { assert_eq!($ident::Big.to_value(), 1); assert_eq!($ident::Small.to_value(), 0); assert_eq!($ident::try_from_value(&1).ok(), Some($ident::Big)); assert_eq!($ident::try_from_value(&0).ok(), Some($ident::Small)); assert_eq!( $ident::try_from_value(&2).err(), Some(type_err(format!( "unexpected value for {} enum: 2", stringify!($ident) ))) ); assert_eq!($ident::db_type(), ColumnType::$col_def.def()); assert_eq!(format!("{}", $ident::Big), "Big"); assert_eq!(format!("{}", $ident::Small), "Small"); }; } test_num_value_uint!(U8, "u8", "TinyInteger", TinyInteger); test_num_value_uint!(U16, "u16", "SmallInteger", SmallInteger); test_num_value_uint!(U32, "u32", "Integer", Integer); test_num_value_uint!(U64, "u64", "BigInteger", BigInteger); test_fallback_uint!(U8Fallback, u8, "u8", "TinyInteger", TinyInteger); test_fallback_uint!(U16Fallback, u16, "u16", "SmallInteger", SmallInteger); test_fallback_uint!(U32Fallback, u32, "u32", "Integer", Integer); test_fallback_uint!(U64Fallback, u64, "u64", "BigInteger", BigInteger); } #[test] fn escaped_non_uax31() { #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy)] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "pop_os_names_typos")] pub enum PopOSTypos { #[sea_orm(string_value = "Pop!_OS")] PopOSCorrect, #[sea_orm(string_value = "Pop\u{2757}_OS")] PopOSEmoji, #[sea_orm(string_value = "Pop!_操作系统")] PopOSChinese, #[sea_orm(string_value = "PopOS")] PopOSASCIIOnly, #[sea_orm(string_value = "Pop OS")] PopOSASCIIOnlyWithSpace, #[sea_orm(string_value = "Pop!OS")] PopOSNoUnderscore, #[sea_orm(string_value = "Pop_OS")] PopOSNoExclaimation, #[sea_orm(string_value = "!PopOS_")] PopOSAllOverThePlace, #[sea_orm(string_value = "Pop!_OS22.04LTS")] PopOSWithVersion, #[sea_orm(string_value = "22.04LTSPop!_OS")] PopOSWithVersionPrefix, #[sea_orm(string_value = "!_")] PopOSJustTheSymbols, #[sea_orm(string_value = "")] Nothing, // This WILL fail: // Both PopOs and PopOS will create identifier "Popos" // #[sea_orm(string_value = "PopOs")] // PopOSLowerCase, } let values = [ "Pop!_OS", "Pop\u{2757}_OS", "Pop!_操作系统", "PopOS", "Pop OS", "Pop!OS", "Pop_OS", "!PopOS_", "Pop!_OS22.04LTS", "22.04LTSPop!_OS", "!_", "", ]; for (variant, val) in PopOSTypos::iter().zip(values) { assert_eq!(variant.to_value(), val); assert_eq!(PopOSTypos::try_from_value(&val.to_owned()), Ok(variant)); } #[derive(Clone, Debug, PartialEq, EnumIter, DeriveActiveEnum, DeriveDisplay)] #[sea_orm( rs_type = "String", db_type = "String(None)", enum_name = "conflicting_string_values" )] pub enum ConflictingStringValues { #[sea_orm(string_value = "")] Member1, #[sea_orm(string_value = "$")] Member2, #[sea_orm(string_value = "$$")] Member3, #[sea_orm(string_value = "AB")] Member4, #[sea_orm(string_value = "A_B")] Member5, #[sea_orm(string_value = "A$B")] Member6, #[sea_orm(string_value = "0 123")] Member7, } type EnumVariant = ConflictingStringValuesVariant; assert_eq!(EnumVariant::__Empty.to_string(), ""); assert_eq!(EnumVariant::_0x24.to_string(), "$"); assert_eq!(EnumVariant::_0x240x24.to_string(), "$$"); assert_eq!(EnumVariant::Ab.to_string(), "AB"); assert_eq!(EnumVariant::A0x5Fb.to_string(), "A_B"); assert_eq!(EnumVariant::A0x24B.to_string(), "A$B"); assert_eq!(EnumVariant::_0x300x20123.to_string(), "0 123"); } #[test] fn test_derive_display() { use crate::DeriveDisplay; #[derive(DeriveDisplay)] enum DisplayTea { EverydayTea, #[sea_orm(display_value = "Breakfast Tea")] BreakfastTea, } assert_eq!(format!("{}", DisplayTea::EverydayTea), "EverydayTea"); assert_eq!(format!("{}", DisplayTea::BreakfastTea), "Breakfast Tea"); } }