Draft ActiveEnum

This commit is contained in:
Billy Chan 2021-10-19 19:08:02 +08:00
parent 10b101b142
commit 8627c8d961
No known key found for this signature in database
GPG Key ID: A2D690CAC7DF3CC7
13 changed files with 597 additions and 18 deletions

View File

@ -0,0 +1,219 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::{punctuated::Punctuated, token::Comma, Lit, Meta};
enum Error {
InputNotEnum,
Syn(syn::Error),
}
struct ActiveEnum {
ident: syn::Ident,
rs_type: TokenStream,
db_type: TokenStream,
variants: syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
}
impl ActiveEnum {
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
let ident = input.ident;
let mut rs_type = None;
let mut db_type = None;
for attr in input.attrs.iter() {
if let Some(ident) = attr.path.get_ident() {
if ident != "sea_orm" {
continue;
}
} else {
continue;
}
if let Ok(list) = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated) {
for meta in list.iter() {
if let Meta::NameValue(nv) = meta {
if let Some(name) = nv.path.get_ident() {
if name == "rs_type" {
if let Lit::Str(litstr) = &nv.lit {
rs_type = syn::parse_str::<TokenStream>(&litstr.value()).ok();
}
} else if name == "db_type" {
if let Lit::Str(litstr) = &nv.lit {
db_type = syn::parse_str::<TokenStream>(&litstr.value()).ok();
}
}
}
}
}
}
}
let rs_type = rs_type.expect("Missing rs_type");
let db_type = db_type.expect("Missing db_type");
let variants = match input.data {
syn::Data::Enum(syn::DataEnum { variants, .. }) => variants,
_ => return Err(Error::InputNotEnum),
};
Ok(ActiveEnum {
ident,
rs_type,
db_type,
variants,
})
}
fn expand(&self) -> syn::Result<TokenStream> {
let expanded_impl_active_enum = self.impl_active_enum();
Ok(expanded_impl_active_enum)
}
fn impl_active_enum(&self) -> TokenStream {
let Self {
ident,
rs_type,
db_type,
variants,
} = self;
let variant_idents: Vec<syn::Ident> = variants
.iter()
.map(|variant| variant.ident.clone())
.collect();
let mut is_string = false;
let variant_values: Vec<TokenStream> = variants
.iter()
.map(|variant| {
let mut string_value = None;
let mut num_value = None;
for attr in variant.attrs.iter() {
if let Some(ident) = attr.path.get_ident() {
if ident != "sea_orm" {
continue;
}
} else {
continue;
}
if let Ok(list) =
attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
{
for meta in list.iter() {
if let Meta::NameValue(nv) = meta {
if let Some(name) = nv.path.get_ident() {
if name == "string_value" {
if let Lit::Str(litstr) = &nv.lit {
string_value = Some(litstr.value());
}
} else if name == "num_value" {
if let Lit::Int(litstr) = &nv.lit {
num_value = litstr.base10_parse::<i32>().ok();
}
}
}
}
}
}
}
if let Some(string_value) = string_value {
is_string = true;
quote! { #string_value }
} else if let Some(num_value) = num_value {
quote! { #num_value }
} else {
panic!("Either string_value or num_value should be specified")
}
})
.collect();
let val = if is_string {
quote! { v.as_ref() }
} else {
quote! { v }
};
quote!(
#[automatically_derived]
impl sea_orm::ActiveEnum for #ident {
type Value = #rs_type;
fn to_value(&self) -> Self::Value {
match self {
#( Self::#variant_idents => #variant_values, )*
}
.to_owned()
}
fn try_from_value(v: &Self::Value) -> Result<Self, sea_orm::DbErr> {
match #val {
#( #variant_values => Ok(Self::#variant_idents), )*
_ => Err(sea_orm::DbErr::Query(format!(
"unexpected value for {} enum: {}",
stringify!(#ident),
v
))),
}
}
fn db_type() -> sea_orm::ColumnDef {
sea_orm::ColumnType::#db_type.def()
}
}
#[automatically_derived]
impl Into<sea_query::Value> for #ident {
fn into(self) -> sea_query::Value {
<Self as sea_orm::ActiveEnum>::to_value(&self).into()
}
}
#[automatically_derived]
impl sea_orm::TryGetable for #ident {
fn try_get(res: &sea_orm::QueryResult, pre: &str, col: &str) -> Result<Self, sea_orm::TryGetError> {
let value = <<Self as sea_orm::ActiveEnum>::Value as sea_orm::TryGetable>::try_get(res, pre, col)?;
<Self as sea_orm::ActiveEnum>::try_from_value(&value).map_err(|e| sea_orm::TryGetError::DbErr(e))
}
}
#[automatically_derived]
impl sea_query::ValueType for #ident {
fn try_from(v: sea_query::Value) -> Result<Self, sea_query::ValueTypeErr> {
let value = <<Self as sea_orm::ActiveEnum>::Value as sea_query::ValueType>::try_from(v)?;
<Self as sea_orm::ActiveEnum>::try_from_value(&value).map_err(|_| sea_query::ValueTypeErr)
}
fn type_name() -> String {
<<Self as sea_orm::ActiveEnum>::Value as sea_query::ValueType>::type_name()
}
fn column_type() -> sea_query::ColumnType {
<Self as sea_orm::ActiveEnum>::db_type()
.get_column_type()
.to_owned()
.into()
}
}
#[automatically_derived]
impl sea_query::Nullable for #ident {
fn null() -> sea_query::Value {
<<Self as sea_orm::ActiveEnum>::Value as sea_query::Nullable>::null()
}
}
)
}
}
pub fn expand_derive_active_enum(input: syn::DeriveInput) -> syn::Result<TokenStream> {
let ident_span = input.ident.span();
match ActiveEnum::new(input) {
Ok(model) => model.expand(),
Err(Error::InputNotEnum) => Ok(quote_spanned! {
ident_span => compile_error!("you can only derive ActiveEnum on enums");
}),
Err(Error::Syn(err)) => Err(err),
}
}

View File

@ -1,7 +1,7 @@
use crate::util::{escape_rust_keyword, trim_starting_raw_identifier};
use convert_case::{Case, Casing};
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use quote::{format_ident, quote, quote_spanned};
use syn::{
parse::Error, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Data, Fields,
Lit, Meta,
@ -192,8 +192,8 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec<Attribute>) -> syn::Res
primary_keys.push(quote! { #field_name });
}
let field_type = match sql_type {
Some(t) => t,
let col_type = match sql_type {
Some(t) => quote! { sea_orm::prelude::ColumnType::#t.def() },
None => {
let field_type = &field.ty;
let temp = quote! { #field_type }
@ -205,7 +205,7 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec<Attribute>) -> syn::Res
} else {
temp.as_str()
};
match temp {
let col_type = match temp {
"char" => quote! { Char(None) },
"String" | "&str" => quote! { String(None) },
"u8" | "i8" => quote! { TinyInteger },
@ -228,16 +228,24 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec<Attribute>) -> syn::Res
"Decimal" => quote! { Decimal(None) },
"Vec<u8>" => quote! { Binary },
_ => {
return Err(Error::new(
field.span(),
format!("unrecognized type {}", temp),
))
// Assumed it's ActiveEnum if none of the above type matches
quote! {}
}
};
if col_type.is_empty() {
let field_span = field.span();
let ty = format_ident!("{}", temp);
let def = quote_spanned! { field_span => {
<#ty as ActiveEnum>::db_type()
}};
quote! { #def }
} else {
quote! { sea_orm::prelude::ColumnType::#col_type.def() }
}
}
};
let mut match_row = quote! { Self::#field_name => sea_orm::prelude::ColumnType::#field_type.def() };
let mut match_row = quote! { Self::#field_name => #col_type };
if nullable {
match_row = quote! { #match_row.nullable() };
}

View File

@ -1,3 +1,4 @@
mod active_enum;
mod active_model;
mod active_model_behavior;
mod column;
@ -9,6 +10,7 @@ mod model;
mod primary_key;
mod relation;
pub use active_enum::*;
pub use active_model::*;
pub use active_model_behavior::*;
pub use column::*;

View File

@ -102,6 +102,15 @@ pub fn derive_active_model_behavior(input: TokenStream) -> TokenStream {
}
}
#[proc_macro_derive(DeriveActiveEnum, attributes(sea_orm))]
pub fn derive_active_enum(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match derives::expand_derive_active_enum(input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
#[proc_macro_derive(FromQueryResult)]
pub fn derive_from_query_result(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);

184
src/entity/active_enum.rs Normal file
View File

@ -0,0 +1,184 @@
use crate::{ColumnDef, DbErr, TryGetable};
use sea_query::{Nullable, Value, ValueType};
use std::fmt::Debug;
pub trait ActiveEnum: Sized {
type Value: Sized + Send + Debug + PartialEq + Into<Value> + ValueType + Nullable + TryGetable;
fn to_value(&self) -> Self::Value;
fn try_from_value(v: &Self::Value) -> Result<Self, DbErr>;
fn db_type() -> ColumnDef;
}
#[cfg(test)]
mod tests {
use crate as sea_orm;
use crate::{entity::prelude::*, *};
use pretty_assertions::assert_eq;
#[test]
fn active_enum_1() {
#[derive(Debug, PartialEq)]
pub enum Category {
Big,
Small,
}
impl ActiveEnum for Category {
type Value = String;
fn to_value(&self) -> Self::Value {
match self {
Self::Big => "B",
Self::Small => "S",
}
.to_owned()
}
fn try_from_value(v: &Self::Value) -> Result<Self, DbErr> {
match v.as_ref() {
"B" => Ok(Self::Big),
"S" => Ok(Self::Small),
_ => Err(DbErr::Query(format!(
"unexpected value for Category enum: {}",
v
))),
}
}
fn db_type() -> ColumnDef {
ColumnType::String(Some(1)).def()
}
}
#[derive(Debug, PartialEq, DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "String(Some(1))")]
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(DbErr::Query(
"unexpected value for Category enum: A".to_owned()
))
);
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(DbErr::Query(
"unexpected value for DeriveCategory enum: A".to_owned()
))
);
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());
}
#[test]
fn active_enum_2() {
#[derive(Debug, PartialEq)]
pub enum Category {
Big,
Small,
}
impl ActiveEnum for Category {
type Value = i32; // FIXME: only support i32 for now
fn to_value(&self) -> Self::Value {
match self {
Self::Big => 1,
Self::Small => 0,
}
.to_owned()
}
fn try_from_value(v: &Self::Value) -> Result<Self, DbErr> {
match v {
1 => Ok(Self::Big),
0 => Ok(Self::Small),
_ => Err(DbErr::Query(format!(
"unexpected value for Category enum: {}",
v
))),
}
}
fn db_type() -> ColumnDef {
ColumnType::Integer.def()
}
}
#[derive(Debug, PartialEq, DeriveActiveEnum)]
#[sea_orm(rs_type = "i32", db_type = "Integer")]
pub enum DeriveCategory {
#[sea_orm(num_value = 1)]
Big,
#[sea_orm(num_value = 0)]
Small,
}
assert_eq!(Category::Big.to_value(), 1);
assert_eq!(Category::Small.to_value(), 0);
assert_eq!(DeriveCategory::Big.to_value(), 1);
assert_eq!(DeriveCategory::Small.to_value(), 0);
assert_eq!(
Category::try_from_value(&2).err(),
Some(DbErr::Query(
"unexpected value for Category enum: 2".to_owned()
))
);
assert_eq!(
Category::try_from_value(&1).ok(),
Some(Category::Big)
);
assert_eq!(
Category::try_from_value(&0).ok(),
Some(Category::Small)
);
assert_eq!(
DeriveCategory::try_from_value(&2).err(),
Some(DbErr::Query(
"unexpected value for DeriveCategory enum: 2".to_owned()
))
);
assert_eq!(
DeriveCategory::try_from_value(&1).ok(),
Some(DeriveCategory::Big)
);
assert_eq!(
DeriveCategory::try_from_value(&0).ok(),
Some(DeriveCategory::Small)
);
assert_eq!(Category::db_type(), ColumnType::Integer.def());
assert_eq!(DeriveCategory::db_type(), ColumnType::Integer.def());
}
}

View File

@ -262,6 +262,10 @@ impl ColumnDef {
self.indexed = true;
self
}
pub fn get_column_type(&self) -> &ColumnType {
&self.col_type
}
}
impl From<ColumnType> for sea_query::ColumnType {

View File

@ -1,3 +1,4 @@
mod active_enum;
mod active_model;
mod base_entity;
mod column;
@ -8,6 +9,7 @@ pub mod prelude;
mod primary_key;
mod relation;
pub use active_enum::*;
pub use active_model::*;
pub use base_entity::*;
pub use column::*;

View File

@ -1,14 +1,15 @@
pub use crate::{
error::*, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait, ColumnType,
DatabaseConnection, DbConn, EntityName, EntityTrait, EnumIter, ForeignKeyAction, Iden,
IdenStatic, Linked, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult,
Related, RelationDef, RelationTrait, Select, Value,
error::*, ActiveEnum, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait,
ColumnType, DatabaseConnection, DbConn, EntityName, EntityTrait, EnumIter, ForeignKeyAction,
Iden, IdenStatic, Linked, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter,
QueryResult, Related, RelationDef, RelationTrait, Select, Value,
};
#[cfg(feature = "macros")]
pub use crate::{
DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity,
DeriveEntityModel, DeriveIntoActiveModel, DeriveModel, DerivePrimaryKey, DeriveRelation,
DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn,
DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, DeriveModel,
DerivePrimaryKey, DeriveRelation,
};
#[cfg(feature = "with-json")]

View File

@ -288,9 +288,9 @@ pub use schema::*;
#[cfg(feature = "macros")]
pub use sea_orm_macros::{
DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity,
DeriveEntityModel, DeriveIntoActiveModel, DeriveModel, DerivePrimaryKey, DeriveRelation,
FromQueryResult,
DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn,
DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, DeriveModel,
DerivePrimaryKey, DeriveRelation, FromQueryResult,
};
pub use sea_query;

View File

@ -0,0 +1,39 @@
pub mod common;
pub use common::{features::*, setup::*, TestContext};
use sea_orm::{entity::prelude::*, entity::*, DatabaseConnection};
#[sea_orm_macros::test]
#[cfg(any(
feature = "sqlx-mysql",
feature = "sqlx-sqlite",
feature = "sqlx-postgres"
))]
async fn main() -> Result<(), DbErr> {
let ctx = TestContext::new("active_enum_tests").await;
create_tables(&ctx.db).await?;
insert_active_enum(&ctx.db).await?;
ctx.delete().await;
Ok(())
}
pub async fn insert_active_enum(db: &DatabaseConnection) -> Result<(), DbErr> {
active_enum::ActiveModel {
category: Set(active_enum::Category::Big),
..Default::default()
}
.insert(db)
.await?;
assert_eq!(
active_enum::Entity::find().one(db).await?.unwrap(),
active_enum::Model {
id: 1,
category: active_enum::Category::Big,
category_opt: None,
}
);
Ok(())
}

View File

@ -0,0 +1,87 @@
use sea_orm::{entity::prelude::*, TryGetError, TryGetable};
use sea_query::{Nullable, ValueType, ValueTypeErr};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "active_enum")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub category: Category,
pub category_opt: Option<Category>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, Clone, PartialEq)]
pub enum Category {
Big,
Small,
}
impl ActiveEnum for Category {
type Value = String;
fn to_value(&self) -> Self::Value {
match self {
Self::Big => "B",
Self::Small => "S",
}
.to_owned()
}
fn try_from_value(v: &Self::Value) -> Result<Self, DbErr> {
match v.as_ref() {
"B" => Ok(Self::Big),
"S" => Ok(Self::Small),
_ => Err(DbErr::Query(format!(
"unexpected value for {} enum: {}",
stringify!(Category),
v
))),
}
}
fn db_type() -> ColumnDef {
ColumnType::String(Some(1)).def()
}
}
impl Into<Value> for Category {
fn into(self) -> Value {
self.to_value().into()
}
}
impl TryGetable for Category {
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, TryGetError> {
let value = <<Self as ActiveEnum>::Value as TryGetable>::try_get(res, pre, col)?;
Self::try_from_value(&value).map_err(|e| TryGetError::DbErr(e))
}
}
impl ValueType for Category {
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
let value = <<Self as ActiveEnum>::Value as ValueType>::try_from(v)?;
Self::try_from_value(&value).map_err(|_| ValueTypeErr)
}
fn type_name() -> String {
<<Self as ActiveEnum>::Value as ValueType>::type_name()
}
fn column_type() -> sea_query::ColumnType {
<Self as ActiveEnum>::db_type()
.get_column_type()
.to_owned()
.into()
}
}
impl Nullable for Category {
fn null() -> Value {
<<Self as ActiveEnum>::Value as Nullable>::null()
}
}

View File

@ -1,8 +1,10 @@
pub mod active_enum;
pub mod applog;
pub mod metadata;
pub mod repository;
pub mod schema;
pub use active_enum::Entity as ActiveEnum;
pub use applog::Entity as Applog;
pub use metadata::Entity as Metadata;
pub use repository::Entity as Repository;

View File

@ -9,6 +9,7 @@ pub async fn create_tables(db: &DatabaseConnection) -> Result<(), DbErr> {
create_log_table(db).await?;
create_metadata_table(db).await?;
create_repository_table(db).await?;
create_active_enum_table(db).await?;
Ok(())
}
@ -75,3 +76,24 @@ pub async fn create_repository_table(db: &DbConn) -> Result<ExecResult, DbErr> {
create_table(db, &stmt, Repository).await
}
pub async fn create_active_enum_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
.table(active_enum::Entity)
.col(
ColumnDef::new(active_enum::Column::Id)
.integer()
.not_null()
.primary_key()
.auto_increment(),
)
.col(
ColumnDef::new(active_enum::Column::Category)
.string_len(1)
.not_null(),
)
.col(ColumnDef::new(active_enum::Column::CategoryOpt).string_len(1))
.to_owned();
create_table(db, &stmt, ActiveEnum).await
}