#661: Add flag to sea-orm-cli to generate code for time crate (#724)

* Implement entity writer context and date time crate enum

* Use entity writer context to render correct rust type according to chrono/time enum

* Add unit test to get col rs type with time crate

* Add date-time-crate flag for cli

* Edit usage of updated get_rs_type method

* Use arg enum for date time crate

* Move date time crate arg enum conversion into commands module

* Directly use arg enum

* Fixing import issues for arg enum w/ help from Billy
This commit is contained in:
Nahua 2022-07-10 09:22:47 +02:00 committed by GitHub
parent 54adc76850
commit a1bf662fc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 228 additions and 63 deletions

View File

@ -1,4 +1,4 @@
use clap::{ArgGroup, Parser, Subcommand}; use clap::{ArgEnum, ArgGroup, Parser, Subcommand};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap(version)] #[clap(version)]
@ -166,5 +166,20 @@ pub enum GenerateSubcommands {
help = "Automatically derive serde Serialize / Deserialize traits for the entity (none, serialize, deserialize, both)" help = "Automatically derive serde Serialize / Deserialize traits for the entity (none, serialize, deserialize, both)"
)] )]
with_serde: String, with_serde: String,
#[clap(
arg_enum,
value_parser,
long,
default_value = "chrono",
help = "The datetime crate to use for generating entities."
)]
date_time_crate: DateTimeCrate,
}, },
} }
#[derive(ArgEnum, Copy, Clone, Debug, PartialEq)]
pub enum DateTimeCrate {
Chrono,
Time,
}

View File

@ -1,11 +1,13 @@
use chrono::Local; use chrono::Local;
use regex::Regex; use regex::Regex;
use sea_orm_codegen::{EntityTransformer, OutputFile, WithSerde}; use sea_orm_codegen::{
EntityTransformer, EntityWriterContext, OutputFile, WithSerde, DateTimeCrate as CodegenDateTimeCrate,
};
use std::{error::Error, fmt::Display, fs, io::Write, path::Path, process::Command, str::FromStr}; use std::{error::Error, fmt::Display, fs, io::Write, path::Path, process::Command, str::FromStr};
use tracing_subscriber::{prelude::*, EnvFilter}; use tracing_subscriber::{prelude::*, EnvFilter};
use url::Url; use url::Url;
use crate::{GenerateSubcommands, MigrateSubcommands}; use crate::{DateTimeCrate, GenerateSubcommands, MigrateSubcommands};
pub async fn run_generate_command( pub async fn run_generate_command(
command: GenerateSubcommands, command: GenerateSubcommands,
@ -23,6 +25,7 @@ pub async fn run_generate_command(
database_schema, database_schema,
database_url, database_url,
with_serde, with_serde,
date_time_crate,
} => { } => {
if verbose { if verbose {
let _ = tracing_subscriber::fmt() let _ = tracing_subscriber::fmt()
@ -173,8 +176,12 @@ pub async fn run_generate_command(
_ => unimplemented!("{} is not supported", url.scheme()), _ => unimplemented!("{} is not supported", url.scheme()),
}; };
let output = EntityTransformer::transform(table_stmts)? let writer_context = EntityWriterContext::new(
.generate(expanded_format, WithSerde::from_str(&with_serde).unwrap()); expanded_format,
WithSerde::from_str(&with_serde).unwrap(),
date_time_crate.into(),
);
let output = EntityTransformer::transform(table_stmts)?.generate(&writer_context);
let dir = Path::new(&output_dir); let dir = Path::new(&output_dir);
fs::create_dir_all(dir)?; fs::create_dir_all(dir)?;
@ -375,6 +382,15 @@ where
::std::process::exit(1); ::std::process::exit(1);
} }
impl From<DateTimeCrate> for CodegenDateTimeCrate {
fn from(date_time_crate: DateTimeCrate) -> CodegenDateTimeCrate {
match date_time_crate {
DateTimeCrate::Chrono => CodegenDateTimeCrate::Chrono,
DateTimeCrate::Time => CodegenDateTimeCrate::Time,
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use clap::StructOpt; use clap::StructOpt;

View File

@ -1,4 +1,4 @@
use crate::{Column, ConjunctRelation, PrimaryKey, Relation}; use crate::{Column, ConjunctRelation, DateTimeCrate, PrimaryKey, Relation};
use heck::{CamelCase, SnakeCase}; use heck::{CamelCase, SnakeCase};
use proc_macro2::{Ident, TokenStream}; use proc_macro2::{Ident, TokenStream};
use quote::format_ident; use quote::format_ident;
@ -43,11 +43,11 @@ impl Entity {
.collect() .collect()
} }
pub fn get_column_rs_types(&self) -> Vec<TokenStream> { pub fn get_column_rs_types(&self, date_time_crate: &DateTimeCrate) -> Vec<TokenStream> {
self.columns self.columns
.clone() .clone()
.into_iter() .into_iter()
.map(|col| col.get_rs_type()) .map(|col| col.get_rs_type(date_time_crate))
.collect() .collect()
} }
@ -100,7 +100,7 @@ impl Entity {
format_ident!("{}", auto_increment) format_ident!("{}", auto_increment)
} }
pub fn get_primary_key_rs_type(&self) -> TokenStream { pub fn get_primary_key_rs_type(&self, date_time_crate: &DateTimeCrate) -> TokenStream {
let types = self let types = self
.primary_keys .primary_keys
.iter() .iter()
@ -109,7 +109,7 @@ impl Entity {
.iter() .iter()
.find(|col| col.name.eq(&primary_key.name)) .find(|col| col.name.eq(&primary_key.name))
.unwrap() .unwrap()
.get_rs_type() .get_rs_type(date_time_crate)
.to_string() .to_string()
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -149,7 +149,7 @@ impl Entity {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{Column, Entity, PrimaryKey, Relation, RelationType}; use crate::{Column, DateTimeCrate, Entity, PrimaryKey, Relation, RelationType};
use quote::format_ident; use quote::format_ident;
use sea_query::{ColumnType, ForeignKeyAction}; use sea_query::{ColumnType, ForeignKeyAction};
@ -260,10 +260,16 @@ mod tests {
fn test_get_column_rs_types() { fn test_get_column_rs_types() {
let entity = setup(); let entity = setup();
for (i, elem) in entity.get_column_rs_types().into_iter().enumerate() { for (i, elem) in entity
.get_column_rs_types(&DateTimeCrate::Chrono)
.into_iter()
.enumerate()
{
assert_eq!( assert_eq!(
elem.to_string(), elem.to_string(),
entity.columns[i].get_rs_type().to_string() entity.columns[i]
.get_rs_type(&DateTimeCrate::Chrono)
.to_string()
); );
} }
} }
@ -363,8 +369,12 @@ mod tests {
let entity = setup(); let entity = setup();
assert_eq!( assert_eq!(
entity.get_primary_key_rs_type().to_string(), entity
entity.columns[0].get_rs_type().to_string() .get_primary_key_rs_type(&DateTimeCrate::Chrono)
.to_string(),
entity.columns[0]
.get_rs_type(&DateTimeCrate::Chrono)
.to_string()
); );
} }

View File

@ -1,4 +1,4 @@
use crate::util::escape_rust_keyword; use crate::{util::escape_rust_keyword, DateTimeCrate};
use heck::{CamelCase, SnakeCase}; use heck::{CamelCase, SnakeCase};
use proc_macro2::{Ident, TokenStream}; use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote}; use quote::{format_ident, quote};
@ -27,7 +27,7 @@ impl Column {
self.name.to_snake_case() == self.name self.name.to_snake_case() == self.name
} }
pub fn get_rs_type(&self) -> TokenStream { pub fn get_rs_type(&self, date_time_crate: &DateTimeCrate) -> TokenStream {
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
let ident: TokenStream = match &self.col_type { let ident: TokenStream = match &self.col_type {
ColumnType::Char(_) ColumnType::Char(_)
@ -45,11 +45,27 @@ impl Column {
ColumnType::Float(_) => "f32".to_owned(), ColumnType::Float(_) => "f32".to_owned(),
ColumnType::Double(_) => "f64".to_owned(), ColumnType::Double(_) => "f64".to_owned(),
ColumnType::Json | ColumnType::JsonBinary => "Json".to_owned(), ColumnType::Json | ColumnType::JsonBinary => "Json".to_owned(),
ColumnType::Date => "Date".to_owned(), ColumnType::Date => match date_time_crate {
ColumnType::Time(_) => "Time".to_owned(), DateTimeCrate::Chrono => "Date".to_owned(),
ColumnType::DateTime(_) => "DateTime".to_owned(), DateTimeCrate::Time => "TimeDate".to_owned(),
ColumnType::Timestamp(_) => "DateTimeUtc".to_owned(), },
ColumnType::TimestampWithTimeZone(_) => "DateTimeWithTimeZone".to_owned(), ColumnType::Time(_) => match date_time_crate {
DateTimeCrate::Chrono => "Time".to_owned(),
DateTimeCrate::Time => "TimeTime".to_owned(),
},
ColumnType::DateTime(_) => match date_time_crate {
DateTimeCrate::Chrono => "DateTime".to_owned(),
DateTimeCrate::Time => "TimeDateTime".to_owned(),
},
ColumnType::Timestamp(_) => match date_time_crate {
DateTimeCrate::Chrono => "DateTimeUtc".to_owned(),
// ColumnType::Timpestamp(_) => time::PrimitiveDateTime: https://docs.rs/sqlx/0.3.5/sqlx/postgres/types/index.html#time
DateTimeCrate::Time => "TimeDateTime".to_owned(),
},
ColumnType::TimestampWithTimeZone(_) => match date_time_crate {
DateTimeCrate::Chrono => "DateTimeWithTimeZone".to_owned(),
DateTimeCrate::Time => "TimeDateTimeWithTimeZone".to_owned(),
},
ColumnType::Decimal(_) | ColumnType::Money(_) => "Decimal".to_owned(), ColumnType::Decimal(_) | ColumnType::Money(_) => "Decimal".to_owned(),
ColumnType::Uuid => "Uuid".to_owned(), ColumnType::Uuid => "Uuid".to_owned(),
ColumnType::Binary(_) => "Vec<u8>".to_owned(), ColumnType::Binary(_) => "Vec<u8>".to_owned(),
@ -147,7 +163,7 @@ impl Column {
pub fn get_info(&self) -> String { pub fn get_info(&self) -> String {
let mut info = String::new(); let mut info = String::new();
let type_info = self.get_rs_type().to_string().replace(' ', ""); let type_info = self.get_rs_type(&DateTimeCrate::Chrono).to_string().replace(' ', "");
let col_info = self.col_info(); let col_info = self.col_info();
write!( write!(
&mut info, &mut info,
@ -210,7 +226,7 @@ impl From<&ColumnDef> for Column {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::Column; use crate::{Column, DateTimeCrate};
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::quote; use quote::quote;
use sea_query::{Alias, BlobSize, ColumnDef, ColumnType, SeaRc}; use sea_query::{Alias, BlobSize, ColumnDef, ColumnType, SeaRc};
@ -312,8 +328,9 @@ mod tests {
} }
#[test] #[test]
fn test_get_rs_type() { fn test_get_rs_type_with_chrono() {
let columns = setup(); let columns = setup();
let chrono_crate = DateTimeCrate::Chrono;
let rs_types = vec![ let rs_types = vec![
"String", "String",
"String", "String",
@ -339,11 +356,56 @@ mod tests {
let rs_type: TokenStream = rs_type.parse().unwrap(); let rs_type: TokenStream = rs_type.parse().unwrap();
col.not_null = true; col.not_null = true;
assert_eq!(col.get_rs_type().to_string(), quote!(#rs_type).to_string()); assert_eq!(
col.get_rs_type(&chrono_crate).to_string(),
quote!(#rs_type).to_string()
);
col.not_null = false; col.not_null = false;
assert_eq!( assert_eq!(
col.get_rs_type().to_string(), col.get_rs_type(&chrono_crate).to_string(),
quote!(Option<#rs_type>).to_string()
);
}
}
#[test]
fn test_get_rs_type_with_time() {
let columns = setup();
let time_crate = DateTimeCrate::Time;
let rs_types = vec![
"String",
"String",
"i8",
"u8",
"i16",
"u16",
"i32",
"u32",
"i64",
"u64",
"f32",
"f64",
"Vec<u8>",
"bool",
"TimeDate",
"TimeTime",
"TimeDateTime",
"TimeDateTime",
"TimeDateTimeWithTimeZone",
];
for (mut col, rs_type) in columns.into_iter().zip(rs_types) {
let rs_type: TokenStream = rs_type.parse().unwrap();
col.not_null = true;
assert_eq!(
col.get_rs_type(&time_crate).to_string(),
quote!(#rs_type).to_string()
);
col.not_null = false;
assert_eq!(
col.get_rs_type(&time_crate).to_string(),
quote!(Option<#rs_type>).to_string() quote!(Option<#rs_type>).to_string()
); );
} }

View File

@ -29,6 +29,19 @@ pub enum WithSerde {
Both, Both,
} }
#[derive(Debug)]
pub enum DateTimeCrate {
Chrono,
Time,
}
#[derive(Debug)]
pub struct EntityWriterContext {
pub(crate) expanded_format: bool,
pub(crate) with_serde: WithSerde,
pub(crate) date_time_crate: DateTimeCrate,
}
impl WithSerde { impl WithSerde {
pub fn extra_derive(&self) -> TokenStream { pub fn extra_derive(&self) -> TokenStream {
let mut extra_derive = match self { let mut extra_derive = match self {
@ -79,19 +92,33 @@ impl FromStr for WithSerde {
} }
} }
impl EntityWriterContext {
pub fn new(
expanded_format: bool,
with_serde: WithSerde,
date_time_crate: DateTimeCrate,
) -> Self {
Self {
expanded_format,
with_serde,
date_time_crate,
}
}
}
impl EntityWriter { impl EntityWriter {
pub fn generate(self, expanded_format: bool, with_serde: WithSerde) -> WriterOutput { pub fn generate(self, context: &EntityWriterContext) -> WriterOutput {
let mut files = Vec::new(); let mut files = Vec::new();
files.extend(self.write_entities(expanded_format, &with_serde)); files.extend(self.write_entities(context));
files.push(self.write_mod()); files.push(self.write_mod());
files.push(self.write_prelude()); files.push(self.write_prelude());
if !self.enums.is_empty() { if !self.enums.is_empty() {
files.push(self.write_sea_orm_active_enums(&with_serde)); files.push(self.write_sea_orm_active_enums(&context.with_serde));
} }
WriterOutput { files } WriterOutput { files }
} }
pub fn write_entities(&self, expanded_format: bool, with_serde: &WithSerde) -> Vec<OutputFile> { pub fn write_entities(&self, context: &EntityWriterContext) -> Vec<OutputFile> {
self.entities self.entities
.iter() .iter()
.map(|entity| { .map(|entity| {
@ -109,10 +136,18 @@ impl EntityWriter {
let mut lines = Vec::new(); let mut lines = Vec::new();
Self::write_doc_comment(&mut lines); Self::write_doc_comment(&mut lines);
let code_blocks = if expanded_format { let code_blocks = if context.expanded_format {
Self::gen_expanded_code_blocks(entity, with_serde) Self::gen_expanded_code_blocks(
entity,
&context.with_serde,
&context.date_time_crate,
)
} else { } else {
Self::gen_compact_code_blocks(entity, with_serde) Self::gen_compact_code_blocks(
entity,
&context.with_serde,
&context.date_time_crate,
)
}; };
Self::write(&mut lines, code_blocks); Self::write(&mut lines, code_blocks);
OutputFile { OutputFile {
@ -196,17 +231,21 @@ impl EntityWriter {
lines.push("".to_owned()); lines.push("".to_owned());
} }
pub fn gen_expanded_code_blocks(entity: &Entity, with_serde: &WithSerde) -> Vec<TokenStream> { pub fn gen_expanded_code_blocks(
entity: &Entity,
with_serde: &WithSerde,
date_time_crate: &DateTimeCrate,
) -> Vec<TokenStream> {
let mut imports = Self::gen_import(with_serde); let mut imports = Self::gen_import(with_serde);
imports.extend(Self::gen_import_active_enum(entity)); imports.extend(Self::gen_import_active_enum(entity));
let mut code_blocks = vec![ let mut code_blocks = vec![
imports, imports,
Self::gen_entity_struct(), Self::gen_entity_struct(),
Self::gen_impl_entity_name(entity), Self::gen_impl_entity_name(entity),
Self::gen_model_struct(entity, with_serde), Self::gen_model_struct(entity, with_serde, date_time_crate),
Self::gen_column_enum(entity), Self::gen_column_enum(entity),
Self::gen_primary_key_enum(entity), Self::gen_primary_key_enum(entity),
Self::gen_impl_primary_key(entity), Self::gen_impl_primary_key(entity, date_time_crate),
Self::gen_relation_enum(entity), Self::gen_relation_enum(entity),
Self::gen_impl_column_trait(entity), Self::gen_impl_column_trait(entity),
Self::gen_impl_relation_trait(entity), Self::gen_impl_relation_trait(entity),
@ -217,10 +256,17 @@ impl EntityWriter {
code_blocks code_blocks
} }
pub fn gen_compact_code_blocks(entity: &Entity, with_serde: &WithSerde) -> Vec<TokenStream> { pub fn gen_compact_code_blocks(
entity: &Entity,
with_serde: &WithSerde,
date_time_crate: &DateTimeCrate,
) -> Vec<TokenStream> {
let mut imports = Self::gen_import(with_serde); let mut imports = Self::gen_import(with_serde);
imports.extend(Self::gen_import_active_enum(entity)); imports.extend(Self::gen_import_active_enum(entity));
let mut code_blocks = vec![imports, Self::gen_compact_model_struct(entity, with_serde)]; let mut code_blocks = vec![
imports,
Self::gen_compact_model_struct(entity, with_serde, date_time_crate),
];
let relation_defs = if entity.get_relation_enum_name().is_empty() { let relation_defs = if entity.get_relation_enum_name().is_empty() {
vec![ vec![
Self::gen_relation_enum(entity), Self::gen_relation_enum(entity),
@ -299,9 +345,13 @@ impl EntityWriter {
}) })
} }
pub fn gen_model_struct(entity: &Entity, with_serde: &WithSerde) -> TokenStream { pub fn gen_model_struct(
entity: &Entity,
with_serde: &WithSerde,
date_time_crate: &DateTimeCrate,
) -> TokenStream {
let column_names_snake_case = entity.get_column_names_snake_case(); let column_names_snake_case = entity.get_column_names_snake_case();
let column_rs_types = entity.get_column_rs_types(); let column_rs_types = entity.get_column_rs_types(date_time_crate);
let extra_derive = with_serde.extra_derive(); let extra_derive = with_serde.extra_derive();
@ -344,9 +394,9 @@ impl EntityWriter {
} }
} }
pub fn gen_impl_primary_key(entity: &Entity) -> TokenStream { pub fn gen_impl_primary_key(entity: &Entity, date_time_crate: &DateTimeCrate) -> TokenStream {
let primary_key_auto_increment = entity.get_primary_key_auto_increment(); let primary_key_auto_increment = entity.get_primary_key_auto_increment();
let value_type = entity.get_primary_key_rs_type(); let value_type = entity.get_primary_key_rs_type(date_time_crate);
quote! { quote! {
impl PrimaryKeyTrait for PrimaryKey { impl PrimaryKeyTrait for PrimaryKey {
type ValueType = #value_type; type ValueType = #value_type;
@ -479,10 +529,14 @@ impl EntityWriter {
} }
} }
pub fn gen_compact_model_struct(entity: &Entity, with_serde: &WithSerde) -> TokenStream { pub fn gen_compact_model_struct(
entity: &Entity,
with_serde: &WithSerde,
date_time_crate: &DateTimeCrate,
) -> TokenStream {
let table_name = entity.table_name.as_str(); let table_name = entity.table_name.as_str();
let column_names_snake_case = entity.get_column_names_snake_case(); let column_names_snake_case = entity.get_column_names_snake_case();
let column_rs_types = entity.get_column_rs_types(); let column_rs_types = entity.get_column_rs_types(date_time_crate);
let primary_keys: Vec<String> = entity let primary_keys: Vec<String> = entity
.primary_keys .primary_keys
.iter() .iter()
@ -561,8 +615,8 @@ impl EntityWriter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
Column, ConjunctRelation, Entity, EntityWriter, PrimaryKey, Relation, RelationType, Column, ConjunctRelation, DateTimeCrate, Entity, EntityWriter, PrimaryKey, Relation,
WithSerde, RelationType, WithSerde,
}; };
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
@ -958,7 +1012,11 @@ mod tests {
} }
let content = lines.join(""); let content = lines.join("");
let expected: TokenStream = content.parse().unwrap(); let expected: TokenStream = content.parse().unwrap();
let generated = EntityWriter::gen_expanded_code_blocks(entity, &crate::WithSerde::None) let generated = EntityWriter::gen_expanded_code_blocks(
entity,
&crate::WithSerde::None,
&crate::DateTimeCrate::Chrono,
)
.into_iter() .into_iter()
.skip(1) .skip(1)
.fold(TokenStream::new(), |mut acc, tok| { .fold(TokenStream::new(), |mut acc, tok| {
@ -998,7 +1056,11 @@ mod tests {
} }
let content = lines.join(""); let content = lines.join("");
let expected: TokenStream = content.parse().unwrap(); let expected: TokenStream = content.parse().unwrap();
let generated = EntityWriter::gen_compact_code_blocks(entity, &crate::WithSerde::None) let generated = EntityWriter::gen_compact_code_blocks(
entity,
&crate::WithSerde::None,
&crate::DateTimeCrate::Chrono,
)
.into_iter() .into_iter()
.skip(1) .skip(1)
.fold(TokenStream::new(), |mut acc, tok| { .fold(TokenStream::new(), |mut acc, tok| {
@ -1091,7 +1153,7 @@ mod tests {
fn assert_serde_variant_results( fn assert_serde_variant_results(
cake_entity: &Entity, cake_entity: &Entity,
entity_serde_variant: &(String, WithSerde), entity_serde_variant: &(String, WithSerde),
generator: Box<dyn Fn(&Entity, &WithSerde) -> Vec<TokenStream>>, generator: Box<dyn Fn(&Entity, &WithSerde, &DateTimeCrate) -> Vec<TokenStream>>,
) -> io::Result<()> { ) -> io::Result<()> {
let mut reader = BufReader::new(entity_serde_variant.0.as_bytes()); let mut reader = BufReader::new(entity_serde_variant.0.as_bytes());
let mut lines: Vec<String> = Vec::new(); let mut lines: Vec<String> = Vec::new();
@ -1105,7 +1167,7 @@ mod tests {
} }
let content = lines.join(""); let content = lines.join("");
let expected: TokenStream = content.parse().unwrap(); let expected: TokenStream = content.parse().unwrap();
let generated = generator(cake_entity, &entity_serde_variant.1) let generated = generator(cake_entity, &entity_serde_variant.1, &DateTimeCrate::Chrono)
.into_iter() .into_iter()
.fold(TokenStream::new(), |mut acc, tok| { .fold(TokenStream::new(), |mut acc, tok| {
acc.extend(tok); acc.extend(tok);