Codegen: Entity Generator (#23)

This commit is contained in:
Billy Chan 2021-06-19 19:47:59 +08:00 committed by GitHub
parent 0b3ea685d5
commit cab4b5a3f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1411 additions and 2 deletions

View File

@ -2,7 +2,10 @@
members = [
".",
"sea-orm-macros",
"sea-orm-codegen",
"sea-orm-cli",
"examples/sqlx-mysql",
"examples/codegen",
]
[package]
@ -28,7 +31,6 @@ futures = { version = "^0.3" }
futures-util = { version = "^0.3" }
sea-query = { path = "../sea-query", version = "^0.12" }
sea-orm-macros = { path = "sea-orm-macros", optional = true }
# sea-schema = { path = "../sea-schema" }
serde = { version = "^1.0", features = [ "derive" ] }
sqlx = { version = "^0.5", optional = true }
strum = { version = "^0.20", features = [ "derive" ] }

View File

@ -0,0 +1,14 @@
[package]
name = "sea-orm-codegen-example"
version = "0.1.0"
edition = "2018"
publish = false
[dependencies]
async-std = { version = "^1.9", features = [ "attributes" ] }
sea-orm = { path = "../../", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "debug-print" ] }
sea-orm-codegen = { path = "../../sea-orm-codegen" }
sea-query = { path = "../../../sea-query" }
strum = { version = "^0.20", features = [ "derive" ] }
serde_json = { version = "^1" }
quote = "1"

View File

@ -0,0 +1,12 @@
# SeaORM Entity Generator Example
Prepare:
Setup a test database and configure the connection string in `main.rs`.
Run `bakery.sql` to setup the test table and data.
Running:
```sh
cargo run
```

View File

@ -0,0 +1,16 @@
mod out;
use sea_orm_codegen::{EntityGenerator, Error};
#[async_std::main]
async fn main() -> Result<(), Error> {
let uri = "mysql://sea:sea@localhost/bakery";
let schema = "bakery";
let _generator = EntityGenerator::discover(uri, schema)
.await?
.transform()?
.generate("src/out")?;
Ok(())
}

View File

@ -0,0 +1,89 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude::*;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl EntityName for Entity {
fn table_name(&self) -> &str {
"cake"
}
}
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
pub id: i32,
pub name: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Name,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
CakeFilling,
Fruit,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnType {
match self {
Self::Id => ColumnType::Integer(Some(11u32)),
Self::Name => ColumnType::String(Some(255u32)),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::CakeFilling => Entity::has_many(super::cake_filling::Entity)
.from(Column::Id)
.to(super::cake_filling::Column::CakeId)
.into(),
Self::Fruit => Entity::has_many(super::fruit::Entity)
.from(Column::Id)
.to(super::fruit::Column::CakeId)
.into(),
}
}
}
impl Related<super::cake_filling::Entity> for Entity {
fn to() -> RelationDef {
Relation::CakeFilling.def()
}
}
impl Related<super::fruit::Entity> for Entity {
fn to() -> RelationDef {
Relation::Fruit.def()
}
}
impl Model {
pub fn find_cake_filling(&self) -> Select<super::cake_filling::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
pub fn find_fruit(&self) -> Select<super::fruit::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,90 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude::*;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl EntityName for Entity {
fn table_name(&self) -> &str {
"cake_filling"
}
}
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
pub cake_id: i32,
pub filling_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
CakeId,
FillingId,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
CakeId,
FillingId,
}
impl PrimaryKeyTrait for PrimaryKey {
fn auto_increment() -> bool {
false
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Cake,
Filling,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnType {
match self {
Self::CakeId => ColumnType::Integer(Some(11u32)),
Self::FillingId => ColumnType::Integer(Some(11u32)),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Cake => Entity::has_one(super::cake::Entity)
.from(Column::CakeId)
.to(super::cake::Column::Id)
.into(),
Self::Filling => Entity::has_one(super::filling::Entity)
.from(Column::FillingId)
.to(super::filling::Column::Id)
.into(),
}
}
}
impl Related<super::cake::Entity> for Entity {
fn to() -> RelationDef {
Relation::Cake.def()
}
}
impl Related<super::filling::Entity> for Entity {
fn to() -> RelationDef {
Relation::Filling.def()
}
}
impl Model {
pub fn find_cake(&self) -> Select<super::cake::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
pub fn find_filling(&self) -> Select<super::filling::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,75 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude::*;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl EntityName for Entity {
fn table_name(&self) -> &str {
"filling"
}
}
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
pub id: i32,
pub name: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Name,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
CakeFilling,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnType {
match self {
Self::Id => ColumnType::Integer(Some(11u32)),
Self::Name => ColumnType::String(Some(255u32)),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::CakeFilling => Entity::has_many(super::cake_filling::Entity)
.from(Column::Id)
.to(super::cake_filling::Column::FillingId)
.into(),
}
}
}
impl Related<super::cake_filling::Entity> for Entity {
fn to() -> RelationDef {
Relation::CakeFilling.def()
}
}
impl Model {
pub fn find_cake_filling(&self) -> Select<super::cake_filling::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,78 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude::*;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl EntityName for Entity {
fn table_name(&self) -> &str {
"fruit"
}
}
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
pub id: i32,
pub name: Option<String>,
pub cake_id: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Name,
CakeId,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Cake,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnType {
match self {
Self::Id => ColumnType::Integer(Some(11u32)),
Self::Name => ColumnType::String(Some(255u32)),
Self::CakeId => ColumnType::Integer(Some(11u32)),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Cake => Entity::has_one(super::cake::Entity)
.from(Column::CakeId)
.to(super::cake::Column::Id)
.into(),
}
}
}
impl Related<super::cake::Entity> for Entity {
fn to() -> RelationDef {
Relation::Cake.def()
}
}
impl Model {
pub fn find_cake(&self) -> Select<super::cake::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,63 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude::*;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl EntityName for Entity {
fn table_name(&self) -> &str {
"fruit_copy"
}
}
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
pub id: i32,
pub name: Option<String>,
pub cake_id: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Name,
CakeId,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnType {
match self {
Self::Id => ColumnType::Integer(Some(11u32)),
Self::Name => ColumnType::String(Some(255u32)),
Self::CakeId => ColumnType::Integer(Some(11u32)),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
_ => panic!("No RelationDef"),
}
}
}
impl Model {}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,7 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
pub mod cake;
pub mod cake_filling;
pub mod filling;
pub mod fruit;
pub mod fruit_copy;

View File

@ -0,0 +1,7 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
pub use super::cake::Entity as Cake;
pub use super::cake_filling::Entity as CakeFilling;
pub use super::filling::Entity as Filling;
pub use super::fruit::Entity as Fruit;
pub use super::fruit_copy::Entity as FruitCopy;

21
sea-orm-cli/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "sea-orm-cli"
version = "0.1.0"
authors = [ "Billy Chan <ccw.billy.123@gmail.com>" ]
edition = "2018"
description = ""
license = "MIT OR Apache-2.0"
documentation = "https://docs.rs/sea-orm"
repository = "https://github.com/SeaQL/sea-orm"
categories = [ "database" ]
keywords = [ "orm", "database", "sql", "mysql", "postgres", "sqlite", "cli" ]
publish = false
[[bin]]
name = "sea-orm"
path = "src/main.rs"
[dependencies]
clap = { version = "^2.33.3" }
async-std = { version = "^1.9", features = [ "attributes" ] }
sea-orm-codegen = { path = "../sea-orm-codegen" }

13
sea-orm-cli/README.md Normal file
View File

@ -0,0 +1,13 @@
# SeaORM CLI
Getting Help:
```sh
cargo run -- -h
```
Running Entity Generator:
```sh
cargo run -- entity generate -u mysql://sea:sea@localhost/bakery -s bakery -o out
```

41
sea-orm-cli/src/cli.rs Normal file
View File

@ -0,0 +1,41 @@
use clap::{App, AppSettings, Arg, SubCommand};
pub fn build_cli() -> App<'static, 'static> {
let entity_subcommand = SubCommand::with_name("entity")
.about("Entity related commands")
.setting(AppSettings::VersionlessSubcommands)
.subcommand(
SubCommand::with_name("generate")
.about("Generate entity")
.arg(
Arg::with_name("DATABASE_URI")
.long("uri")
.short("u")
.help("Database URI")
.takes_value(true)
.required(true),
)
.arg(
Arg::with_name("DATABASE_SCHEMA")
.long("schema")
.short("s")
.help("Database schema")
.takes_value(true)
.required(true),
)
.arg(
Arg::with_name("OUTPUT_DIR")
.long("output_dir")
.short("o")
.help("Entity file output directory")
.takes_value(true)
.default_value("./"),
),
)
.setting(AppSettings::SubcommandRequiredElseHelp);
App::new("sea-orm")
.version(env!("CARGO_PKG_VERSION"))
.setting(AppSettings::VersionlessSubcommands)
.subcommand(entity_subcommand)
}

42
sea-orm-cli/src/main.rs Normal file
View File

@ -0,0 +1,42 @@
use clap::ArgMatches;
use sea_orm_codegen::EntityGenerator;
use std::{error::Error, fmt::Display};
mod cli;
#[async_std::main]
async fn main() {
let matches = cli::build_cli().get_matches();
match matches.subcommand() {
("entity", Some(matches)) => run_entity_command(matches)
.await
.unwrap_or_else(handle_error),
_ => unreachable!("You should never see this message"),
}
}
async fn run_entity_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Error>> {
match matches.subcommand() {
("generate", Some(args)) => {
let uri = args.value_of("DATABASE_URI").unwrap();
let schema = args.value_of("DATABASE_SCHEMA").unwrap();
let output_dir = args.value_of("OUTPUT_DIR").unwrap();
EntityGenerator::discover(uri, schema)
.await?
.transform()?
.generate(output_dir)?;
}
_ => unreachable!("You should never see this message"),
};
Ok(())
}
fn handle_error<E>(error: E)
where
E: Display,
{
eprintln!("{}", error);
::std::process::exit(1);
}

View File

@ -0,0 +1,26 @@
[package]
name = "sea-orm-codegen"
version = "0.1.0"
authors = [ "Billy Chan <ccw.billy.123@gmail.com>" ]
edition = "2018"
description = ""
license = "MIT OR Apache-2.0"
documentation = "https://docs.rs/sea-orm"
repository = "https://github.com/SeaQL/sea-orm"
categories = [ "database" ]
keywords = [ "orm", "database", "sql", "mysql", "postgres", "sqlite" ]
publish = false
[lib]
name = "sea_orm_codegen"
path = "src/lib.rs"
[dependencies]
sea-orm = { path = "../", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "debug-print", "with-json", "macros" ], default-features = false }
sea-schema = { path = "../../sea-schema", default-features = false, features = [ "sqlx-mysql", "runtime-async-std-native-tls", "discovery", "writer" ] }
sea-query = { path = "../../sea-query", version = "^0.12" }
sqlx = { version = "^0.5", features = [ "mysql", "runtime-async-std-native-tls" ] }
syn = { version = "1", default-features = false, features = [ "derive", "parsing", "proc-macro", "printing" ] }
quote = "1"
heck = "0.3"
proc-macro2 = "1"

View File

@ -0,0 +1 @@
# SeaORM Codegen

View File

@ -0,0 +1,155 @@
use heck::{CamelCase, SnakeCase};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use sea_query::{ColumnDef, ColumnSpec, ColumnType};
#[derive(Clone, Debug)]
pub struct Column {
pub(crate) name: String,
pub(crate) col_type: ColumnType,
pub(crate) auto_increment: bool,
pub(crate) not_null: bool,
}
impl Column {
pub fn get_name_snake_case(&self) -> Ident {
format_ident!("{}", self.name.to_snake_case())
}
pub fn get_name_camel_case(&self) -> Ident {
format_ident!("{}", self.name.to_camel_case())
}
pub fn get_rs_type(&self) -> TokenStream {
let ident = match self.col_type {
ColumnType::Char(_)
| ColumnType::String(_)
| ColumnType::Text
| ColumnType::DateTime(_)
| ColumnType::Timestamp(_)
| ColumnType::Time(_)
| ColumnType::Date
| ColumnType::Json
| ColumnType::JsonBinary
| ColumnType::Custom(_) => format_ident!("String"),
ColumnType::TinyInteger(_) => format_ident!("i8"),
ColumnType::SmallInteger(_) => format_ident!("i16"),
ColumnType::Integer(_) => format_ident!("i32"),
ColumnType::BigInteger(_) => format_ident!("i64"),
ColumnType::Float(_)
| ColumnType::Decimal(_)
| ColumnType::Money(_) => format_ident!("f32"),
ColumnType::Double(_) => format_ident!("f64"),
ColumnType::Binary(_) => format_ident!("Vec<u8>"),
ColumnType::Boolean => format_ident!("bool"),
};
match self.not_null {
true => quote! { #ident },
false => quote! { Option<#ident> },
}
}
pub fn get_type(&self) -> TokenStream {
match &self.col_type {
ColumnType::Char(s) => match s {
Some(s) => quote! { ColumnType::Char(Some(#s)) },
None => quote! { ColumnType::Char(None) },
},
ColumnType::String(s) => match s {
Some(s) => quote! { ColumnType::String(Some(#s)) },
None => quote! { ColumnType::String(None) },
},
ColumnType::Text => quote! { ColumnType::Text },
ColumnType::TinyInteger(s) => match s {
Some(s) => quote! { ColumnType::TinyInteger(Some(#s)) },
None => quote! { ColumnType::TinyInteger(None) },
},
ColumnType::SmallInteger(s) => match s {
Some(s) => quote! { ColumnType::SmallInteger(Some(#s)) },
None => quote! { ColumnType::SmallInteger(None) },
},
ColumnType::Integer(s) => match s {
Some(s) => quote! { ColumnType::Integer(Some(#s)) },
None => quote! { ColumnType::Integer(None) },
},
ColumnType::BigInteger(s) => match s {
Some(s) => quote! { ColumnType::BigInteger(Some(#s)) },
None => quote! { ColumnType::BigInteger(None) },
},
ColumnType::Float(s) => match s {
Some(s) => quote! { ColumnType::Float(Some(#s)) },
None => quote! { ColumnType::Float(None) },
},
ColumnType::Double(s) => match s {
Some(s) => quote! { ColumnType::Double(Some(#s)) },
None => quote! { ColumnType::Double(None) },
},
ColumnType::Decimal(s) => match s {
Some((s1, s2)) => quote! { ColumnType::Decimal(Some((#s1, #s2))) },
None => quote! { ColumnType::Decimal(None) },
},
ColumnType::DateTime(s) => match s {
Some(s) => quote! { ColumnType::DateTime(Some(#s)) },
None => quote! { ColumnType::DateTime(None) },
},
ColumnType::Timestamp(s) => match s {
Some(s) => quote! { ColumnType::Timestamp(Some(#s)) },
None => quote! { ColumnType::Timestamp(None) },
},
ColumnType::Time(s) => match s {
Some(s) => quote! { ColumnType::Time(Some(#s)) },
None => quote! { ColumnType::Time(None) },
},
ColumnType::Date => quote! { ColumnType::Date },
ColumnType::Binary(s) => match s {
Some(s) => quote! { ColumnType::Binary(Some(#s)) },
None => quote! { ColumnType::Binary(None) },
},
ColumnType::Boolean => quote! { ColumnType::Boolean },
ColumnType::Money(s) => match s {
Some((s1, s2)) => quote! { ColumnType::Money(Some((#s1, #s2))) },
None => quote! { ColumnType::Money(None) },
},
ColumnType::Json => quote! { ColumnType::Json },
ColumnType::JsonBinary => quote! { ColumnType::JsonBinary },
ColumnType::Custom(s) => {
let s = s.to_string();
quote! { ColumnType::Custom(sea_query::SeaRc::new(sea_query::Alias::new(#s))) }
}
}
}
}
impl From<&ColumnDef> for Column {
fn from(col_def: &ColumnDef) -> Self {
let name = col_def.get_column_name();
let col_type = match col_def.get_column_type() {
Some(ty) => ty.clone(),
None => panic!("ColumnType should not be empty"),
};
let auto_increments: Vec<bool> = col_def
.get_column_spec()
.iter()
.filter_map(|spec| match spec {
ColumnSpec::AutoIncrement => Some(true),
_ => None,
})
.collect();
let auto_increment = !auto_increments.is_empty();
let not_nulls: Vec<bool> = col_def
.get_column_spec()
.iter()
.filter_map(|spec| match spec {
ColumnSpec::NotNull => Some(true),
_ => None,
})
.collect();
let not_null = !not_nulls.is_empty();
Self {
name,
col_type,
auto_increment,
not_null,
}
}
}

View File

@ -0,0 +1,121 @@
use crate::{Column, PrimaryKey, Relation};
use heck::{CamelCase, SnakeCase};
use proc_macro2::{Ident, TokenStream};
use quote::format_ident;
#[derive(Clone, Debug)]
pub struct Entity {
pub(crate) table_name: String,
pub(crate) columns: Vec<Column>,
pub(crate) relations: Vec<Relation>,
pub(crate) primary_keys: Vec<PrimaryKey>,
}
impl Entity {
pub fn get_table_name_snake_case(&self) -> String {
self.table_name.to_snake_case()
}
pub fn get_table_name_camel_case(&self) -> String {
self.table_name.to_camel_case()
}
pub fn get_table_name_snake_case_ident(&self) -> Ident {
format_ident!("{}", self.get_table_name_snake_case())
}
pub fn get_table_name_camel_case_ident(&self) -> Ident {
format_ident!("{}", self.get_table_name_camel_case())
}
pub fn get_column_names_snake_case(&self) -> Vec<Ident> {
self.columns
.iter()
.map(|col| col.get_name_snake_case())
.collect()
}
pub fn get_column_names_camel_case(&self) -> Vec<Ident> {
self.columns
.iter()
.map(|col| col.get_name_camel_case())
.collect()
}
pub fn get_column_rs_types(&self) -> Vec<TokenStream> {
self.columns
.clone()
.into_iter()
.map(|col| col.get_rs_type())
.collect()
}
pub fn get_column_types(&self) -> Vec<TokenStream> {
self.columns
.clone()
.into_iter()
.map(|col| col.get_type())
.collect()
}
pub fn get_primary_key_names_snake_case(&self) -> Vec<Ident> {
self.primary_keys
.iter()
.map(|pk| pk.get_name_snake_case())
.collect()
}
pub fn get_primary_key_names_camel_case(&self) -> Vec<Ident> {
self.primary_keys
.iter()
.map(|pk| pk.get_name_camel_case())
.collect()
}
pub fn get_relation_ref_tables_snake_case(&self) -> Vec<Ident> {
self.relations
.iter()
.map(|rel| rel.get_ref_table_snake_case())
.collect()
}
pub fn get_relation_ref_tables_camel_case(&self) -> Vec<Ident> {
self.relations
.iter()
.map(|rel| rel.get_ref_table_camel_case())
.collect()
}
pub fn get_relation_rel_types(&self) -> Vec<Ident> {
self.relations
.iter()
.map(|rel| rel.get_rel_type())
.collect()
}
pub fn get_relation_columns_camel_case(&self) -> Vec<Ident> {
self.relations
.iter()
.map(|rel| rel.get_column_camel_case())
.collect()
}
pub fn get_relation_ref_columns_camel_case(&self) -> Vec<Ident> {
self.relations
.iter()
.map(|rel| rel.get_ref_column_camel_case())
.collect()
}
pub fn get_relation_rel_find_helpers(&self) -> Vec<Ident> {
self.relations
.iter()
.map(|rel| rel.get_rel_find_helper())
.collect()
}
pub fn get_primary_key_auto_increment(&self) -> Ident {
let auto_increment = self.columns.iter().any(|col| col.auto_increment);
format_ident!("{}", auto_increment)
}
}

View File

@ -0,0 +1,15 @@
use crate::{EntityTransformer, Error};
use sea_schema::mysql::discovery::SchemaDiscovery;
use sqlx::MySqlPool;
#[derive(Clone, Debug)]
pub struct EntityGenerator {}
impl EntityGenerator {
pub async fn discover(uri: &str, schema: &str) -> Result<EntityTransformer, Error> {
let connection = MySqlPool::connect(uri).await?;
let schema_discovery = SchemaDiscovery::new(connection, schema);
let schema = schema_discovery.discover().await;
Ok(EntityTransformer { schema })
}
}

View File

@ -0,0 +1,15 @@
mod column;
mod entity;
mod generator;
mod primary_key;
mod relation;
mod transformer;
mod writer;
pub use column::*;
pub use entity::*;
pub use generator::*;
pub use primary_key::*;
pub use relation::*;
pub use transformer::*;
pub use writer::*;

View File

@ -0,0 +1,18 @@
use heck::{CamelCase, SnakeCase};
use proc_macro2::Ident;
use quote::format_ident;
#[derive(Clone, Debug)]
pub struct PrimaryKey {
pub(crate) name: String,
}
impl PrimaryKey {
pub fn get_name_snake_case(&self) -> Ident {
format_ident!("{}", self.name.to_snake_case())
}
pub fn get_name_camel_case(&self) -> Ident {
format_ident!("{}", self.name.to_camel_case())
}
}

View File

@ -0,0 +1,60 @@
use heck::{CamelCase, SnakeCase};
use proc_macro2::Ident;
use quote::format_ident;
use sea_orm::RelationType;
use sea_query::TableForeignKey;
#[derive(Clone, Debug)]
pub struct Relation {
pub(crate) ref_table: String,
pub(crate) columns: Vec<String>,
pub(crate) ref_columns: Vec<String>,
pub(crate) rel_type: RelationType,
}
impl Relation {
pub fn get_ref_table_snake_case(&self) -> Ident {
format_ident!("{}", self.ref_table.to_snake_case())
}
pub fn get_ref_table_camel_case(&self) -> Ident {
format_ident!("{}", self.ref_table.to_camel_case())
}
pub fn get_rel_type(&self) -> Ident {
match self.rel_type {
RelationType::HasOne => format_ident!("has_one"),
RelationType::HasMany => format_ident!("has_many"),
}
}
pub fn get_column_camel_case(&self) -> Ident {
format_ident!("{}", self.columns[0].to_camel_case())
}
pub fn get_ref_column_camel_case(&self) -> Ident {
format_ident!("{}", self.ref_columns[0].to_camel_case())
}
pub fn get_rel_find_helper(&self) -> Ident {
format_ident!("find_{}", self.ref_table.to_snake_case())
}
}
impl From<&TableForeignKey> for Relation {
fn from(tbl_fk: &TableForeignKey) -> Self {
let ref_table = match tbl_fk.get_ref_table() {
Some(s) => s,
None => panic!("RefTable should not be empty"),
};
let columns = tbl_fk.get_columns();
let ref_columns = tbl_fk.get_ref_columns();
let rel_type = RelationType::HasOne;
Self {
ref_table,
columns,
ref_columns,
rel_type,
}
}
}

View File

@ -0,0 +1,86 @@
use crate::{Entity, EntityWriter, Error, PrimaryKey, Relation};
use sea_orm::RelationType;
use sea_query::TableStatement;
use sea_schema::mysql::def::Schema;
use std::{collections::HashMap, mem::swap};
#[derive(Clone, Debug)]
pub struct EntityTransformer {
pub(crate) schema: Schema,
}
impl EntityTransformer {
pub fn transform(self) -> Result<EntityWriter, Error> {
let mut inverse_relations: HashMap<String, Vec<Relation>> = HashMap::new();
let mut entities = Vec::new();
for table_ref in self.schema.tables.iter() {
let table_stmt = table_ref.write();
let table_create = match table_stmt {
TableStatement::Create(stmt) => stmt,
_ => {
return Err(Error::TransformError(
"TableStatement should be create".into(),
))
}
};
let table_name = match table_create.get_table_name() {
Some(s) => s,
None => {
return Err(Error::TransformError(
"Table name should not be empty".into(),
))
}
};
let columns = table_create
.get_columns()
.iter()
.map(|col_def| col_def.into())
.collect();
let relations = table_create
.get_foreign_key_create_stmts()
.iter()
.map(|fk_create_stmt| fk_create_stmt.get_foreign_key())
.map(|tbl_fk| tbl_fk.into());
let primary_keys = table_create
.get_indexes()
.iter()
.filter(|index| index.is_primary_key())
.map(|index| {
index
.get_index_spec()
.get_column_names()
.into_iter()
.map(|name| PrimaryKey { name })
.collect::<Vec<_>>()
})
.flatten()
.collect();
let entity = Entity {
table_name: table_name.clone(),
columns,
relations: relations.clone().collect(),
primary_keys,
};
entities.push(entity);
for mut rel in relations.into_iter() {
let ref_table = rel.ref_table;
swap(&mut rel.columns, &mut rel.ref_columns);
rel.rel_type = RelationType::HasMany;
rel.ref_table = table_name.clone();
if let Some(vec) = inverse_relations.get_mut(&ref_table) {
vec.push(rel);
} else {
inverse_relations.insert(ref_table, vec![rel]);
}
}
}
for (tbl_name, relations) in inverse_relations.iter() {
for ent in entities.iter_mut() {
if ent.table_name.eq(tbl_name) {
ent.relations.append(relations.clone().as_mut());
}
}
}
Ok(EntityWriter { entities })
}
}

View File

@ -0,0 +1,297 @@
use crate::{Entity, Error};
use proc_macro2::TokenStream;
use quote::quote;
use std::{
fs::{self, File},
io::{self, Write},
path::Path,
process::Command,
};
#[derive(Clone, Debug)]
pub struct EntityWriter {
pub(crate) entities: Vec<Entity>,
}
impl EntityWriter {
pub fn generate(self, output_dir: &str) -> Result<(), Error> {
for entity in self.entities.iter() {
let code_blocks = Self::gen_code_blocks(entity);
Self::write(output_dir, entity, code_blocks)?;
}
for entity in self.entities.iter() {
Self::format_entity(output_dir, entity)?;
}
self.write_mod(output_dir)?;
self.write_prelude(output_dir)?;
Ok(())
}
pub fn write_mod(&self, output_dir: &str) -> io::Result<()> {
let file_name = "mod.rs";
let dir = Self::create_dir(output_dir)?;
let file_path = dir.join(file_name);
let mut file = fs::File::create(file_path)?;
Self::write_doc_comment(&mut file)?;
for entity in self.entities.iter() {
let code_block = Self::gen_mod(entity);
file.write_all(code_block.to_string().as_bytes())?;
}
Self::format_file(output_dir, file_name)?;
Ok(())
}
pub fn write_prelude(&self, output_dir: &str) -> io::Result<()> {
let file_name = "prelude.rs";
let dir = Self::create_dir(output_dir)?;
let file_path = dir.join(file_name);
let mut file = fs::File::create(file_path)?;
Self::write_doc_comment(&mut file)?;
for entity in self.entities.iter() {
let code_block = Self::gen_prelude_use(entity);
file.write_all(code_block.to_string().as_bytes())?;
}
Self::format_file(output_dir, file_name)?;
Ok(())
}
pub fn write(
output_dir: &str,
entity: &Entity,
code_blocks: Vec<TokenStream>,
) -> io::Result<()> {
let dir = Self::create_dir(output_dir)?;
let file_path = dir.join(format!("{}.rs", entity.table_name));
let mut file = fs::File::create(file_path)?;
Self::write_doc_comment(&mut file)?;
for code_block in code_blocks {
file.write_all(code_block.to_string().as_bytes())?;
file.write_all(b"\n\n")?;
}
Ok(())
}
pub fn write_doc_comment(file: &mut File) -> io::Result<()> {
let ver = env!("CARGO_PKG_VERSION");
let comments = vec![format!(
"//! SeaORM Entity. Generated by sea-orm-codegen {}",
ver
)];
for comment in comments {
file.write_all(comment.as_bytes())?;
file.write_all(b"\n\n")?;
}
Ok(())
}
pub fn create_dir(output_dir: &str) -> io::Result<&Path> {
let dir = Path::new(output_dir);
fs::create_dir_all(dir)?;
Ok(dir)
}
pub fn format_entity(output_dir: &str, entity: &Entity) -> io::Result<()> {
Self::format_file(output_dir, &format!("{}.rs", entity.table_name))
}
pub fn format_file(output_dir: &str, file_name: &str) -> io::Result<()> {
Command::new("rustfmt")
.arg(Path::new(output_dir).join(file_name))
.spawn()?
.wait()?;
Ok(())
}
pub fn gen_code_blocks(entity: &Entity) -> Vec<TokenStream> {
let mut code_blocks = vec![
Self::gen_import(),
Self::gen_entity_struct(),
Self::gen_impl_entity_name(entity),
Self::gen_model_struct(entity),
Self::gen_column_enum(entity),
Self::gen_primary_key_enum(entity),
Self::gen_impl_primary_key(entity),
Self::gen_relation_enum(entity),
Self::gen_impl_column_trait(entity),
Self::gen_impl_relation_trait(entity),
];
code_blocks.extend(Self::gen_impl_related(entity));
code_blocks.extend(vec![
Self::gen_impl_model(entity),
Self::gen_impl_active_model_behavior(),
]);
code_blocks
}
pub fn gen_import() -> TokenStream {
quote! {
use sea_orm::entity::prelude::*;
}
}
pub fn gen_entity_struct() -> TokenStream {
quote! {
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
}
}
pub fn gen_impl_entity_name(entity: &Entity) -> TokenStream {
let table_name_snake_case = entity.get_table_name_snake_case();
quote! {
impl EntityName for Entity {
fn table_name(&self) -> &str {
#table_name_snake_case
}
}
}
}
pub fn gen_model_struct(entity: &Entity) -> TokenStream {
let column_names_snake_case = entity.get_column_names_snake_case();
let column_rs_types = entity.get_column_rs_types();
quote! {
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
#(pub #column_names_snake_case: #column_rs_types),*
}
}
}
pub fn gen_column_enum(entity: &Entity) -> TokenStream {
let column_names_camel_case = entity.get_column_names_camel_case();
quote! {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
#(#column_names_camel_case),*
}
}
}
pub fn gen_primary_key_enum(entity: &Entity) -> TokenStream {
let primary_key_names_camel_case = entity.get_primary_key_names_camel_case();
quote! {
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
#(#primary_key_names_camel_case),*
}
}
}
pub fn gen_impl_primary_key(entity: &Entity) -> TokenStream {
let primary_key_auto_increment = entity.get_primary_key_auto_increment();
quote! {
impl PrimaryKeyTrait for PrimaryKey {
fn auto_increment() -> bool {
#primary_key_auto_increment
}
}
}
}
pub fn gen_relation_enum(entity: &Entity) -> TokenStream {
let relation_ref_tables_camel_case = entity.get_relation_ref_tables_camel_case();
quote! {
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
#(#relation_ref_tables_camel_case),*
}
}
}
pub fn gen_impl_column_trait(entity: &Entity) -> TokenStream {
let column_names_camel_case = entity.get_column_names_camel_case();
let column_types = entity.get_column_types();
quote! {
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnType {
match self {
#(Self::#column_names_camel_case => #column_types),*
}
}
}
}
}
pub fn gen_impl_relation_trait(entity: &Entity) -> TokenStream {
let relation_ref_tables_camel_case = entity.get_relation_ref_tables_camel_case();
let relation_rel_types = entity.get_relation_rel_types();
let relation_ref_tables_snake_case = entity.get_relation_ref_tables_snake_case();
let relation_columns_camel_case = entity.get_relation_columns_camel_case();
let relation_ref_columns_camel_case = entity.get_relation_ref_columns_camel_case();
let quoted = if relation_ref_tables_camel_case.is_empty() {
quote! {
_ => panic!("No RelationDef"),
}
} else {
quote! {
#(Self::#relation_ref_tables_camel_case => Entity::#relation_rel_types(super::#relation_ref_tables_snake_case::Entity)
.from(Column::#relation_columns_camel_case)
.to(super::#relation_ref_tables_snake_case::Column::#relation_ref_columns_camel_case)
.into()),*
}
};
quote! {
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
#quoted
}
}
}
}
}
pub fn gen_impl_related(entity: &Entity) -> Vec<TokenStream> {
let camel = entity.get_relation_ref_tables_camel_case();
let snake = entity.get_relation_ref_tables_snake_case();
camel
.iter()
.zip(snake)
.map(|(c, s)| {
quote! {
impl Related<super::#s::Entity> for Entity {
fn to() -> RelationDef {
Relation::#c.def()
}
}
}
})
.collect()
}
pub fn gen_impl_model(entity: &Entity) -> TokenStream {
let relation_ref_tables_snake_case = entity.get_relation_ref_tables_snake_case();
let relation_rel_find_helpers = entity.get_relation_rel_find_helpers();
quote! {
impl Model {
#(pub fn #relation_rel_find_helpers(&self) -> Select<super::#relation_ref_tables_snake_case::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
})*
}
}
}
pub fn gen_impl_active_model_behavior() -> TokenStream {
quote! {
impl ActiveModelBehavior for ActiveModel {}
}
}
pub fn gen_mod(entity: &Entity) -> TokenStream {
let table_name_snake_case_ident = entity.get_table_name_snake_case_ident();
quote! {
pub mod #table_name_snake_case_ident;
}
}
pub fn gen_prelude_use(entity: &Entity) -> TokenStream {
let table_name_snake_case_ident = entity.get_table_name_snake_case_ident();
let table_name_camel_case_ident = entity.get_table_name_camel_case_ident();
quote! {
pub use super::#table_name_snake_case_ident::Entity as #table_name_camel_case_ident;
}
}
}

View File

@ -0,0 +1,40 @@
use std::{error, fmt, io};
#[derive(Debug)]
pub enum Error {
StdIoError(io::Error),
SqlxError(sqlx::Error),
TransformError(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::StdIoError(e) => write!(f, "{:?}", e),
Self::SqlxError(e) => write!(f, "{:?}", e),
Self::TransformError(e) => write!(f, "{:?}", e),
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Self::StdIoError(e) => Some(e),
Self::SqlxError(e) => Some(e),
Self::TransformError(_) => None,
}
}
}
impl From<io::Error> for Error {
fn from(io_err: io::Error) -> Self {
Self::StdIoError(io_err)
}
}
impl From<sqlx::Error> for Error {
fn from(sqlx_err: sqlx::Error) -> Self {
Self::SqlxError(sqlx_err)
}
}

View File

@ -0,0 +1,5 @@
mod entity;
mod error;
pub use entity::*;
pub use error::*;

View File

@ -3,7 +3,7 @@ use core::marker::PhantomData;
use sea_query::{DynIden, IntoIden, JoinType};
use std::fmt::Debug;
#[derive(Debug)]
#[derive(Clone, Debug)]
pub enum RelationType {
HasOne,
HasMany,