restructure generator

This commit is contained in:
2025-04-08 22:03:18 +04:00
parent 3ed3e079f2
commit 8fd390fe18
25 changed files with 595 additions and 377 deletions

83
Cargo.lock generated
View File

@@ -108,15 +108,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "atomic"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
dependencies = [
"bytemuck",
]
[[package]]
name = "autocfg"
version = "1.4.0"
@@ -180,12 +171,6 @@ version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "bytemuck"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
[[package]]
name = "byteorder"
version = "1.5.0"
@@ -595,19 +580,6 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "figment"
version = "0.10.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
dependencies = [
"atomic",
"serde",
"serde_yaml",
"uncased",
"version_check",
]
[[package]]
name = "flume"
version = "0.11.1"
@@ -1379,6 +1351,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@@ -1675,23 +1653,22 @@ dependencies = [
"color-eyre",
"comfy-table",
"comment-parser",
"figment",
"handlebars",
"heck 0.5.0",
"include_dir",
"indicatif",
"inquire",
"path-clean",
"pathdiff",
"quote",
"sea-orm-codegen",
"sea-schema",
"serde",
"serde-inline-default",
"serde_json",
"serde_yaml",
"sqlx",
"syn",
"tokio",
"toml",
"toml_edit",
"tracing",
"tracing-subscriber",
@@ -1821,19 +1798,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha1"
version = "0.10.6"
@@ -2304,6 +2268,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
@@ -2410,15 +2386,6 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "uncased"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.18"
@@ -2464,12 +2431,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "url"
version = "2.5.4"
@@ -2787,9 +2748,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.4"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
dependencies = [
"memchr",
]

View File

@@ -10,23 +10,22 @@ clap = { version = "4.5.32", features = ["derive", "env"] }
color-eyre = "0.6.3"
comfy-table = { version = "7.1.4", default-features = false }
comment-parser = "0.1.0"
figment = { version = "0.10.19", features = ["yaml"] }
handlebars = "6.3.2"
heck = "0.5.0"
include_dir = "0.7.4"
indicatif = "0.17.11"
inquire = "0.7.5"
path-clean = "1.0.1"
pathdiff = "0.2.3"
quote = "1.0.40"
sea-orm-codegen = "1.1.8"
sea-schema = { version = "0.16.1", features = ["sqlx-all"] }
serde = { version = "1.0.219", features = ["derive"] }
serde-inline-default = "0.2.3"
serde_json = "1.0.140"
serde_yaml = "0.9.34"
sqlx = { version = "0.8.3", features = ["mysql", "postgres", "sqlite", "runtime-tokio"] }
syn = { version = "2.0.100", features = ["extra-traits", "full"] }
tokio = { version = "1.44.1", features = ["full"] }
toml = "0.8.20"
toml_edit = { version = "0.22.24", features = ["serde"] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }

View File

@@ -1,10 +1,19 @@
# This file is used to configure the SeaORM generator.
[modules.discovery]
enable = true
[modules.discovery.filter]
include_hidden = false
skip_seaql_migrations = true
[modules.sea_orm]
enable = true
path = "./tests/src/models/_entities"
[modules.template]
enable = true
[modules.template.tables]
[modules.model]
enable = true
prelude = true
path = "./tests/src/models"
# [modules.annotate]
# enable = true

View File

@@ -3,7 +3,7 @@
processes = {
frontend = {
command = ''
RUST_LOG=debug,sqlx=warn ${pkgs.cargo-watch}/bin/cargo-watch -x 'run'
RUST_LOG=debug,sqlx=warn ${pkgs.cargo-watch}/bin/cargo-watch -i tests/src -x 'run'
'';
};
};

View File

@@ -1,154 +0,0 @@
use core::time;
use color_eyre::eyre::{eyre, ContextCompat, Report, Result};
use sea_schema::sea_query::TableCreateStatement;
use url::Url;
use crate::config::db::DbConfig;
#[derive(Debug, Clone)]
pub enum DbType {
MySql,
Postgres,
Sqlite,
}
pub async fn get_tables(
database_url: String,
filter: Box<dyn Fn(&String) -> bool>,
database_config: &DbConfig,
) -> Result<(Vec<TableCreateStatement>, DbType)> {
let url = Url::parse(&database_url)?;
tracing::trace!(?url);
let is_sqlite = url.scheme() == "sqlite";
let database_name: &str = (if !is_sqlite {
let database_name = url
.path_segments()
.context("No database name as part of path")?
.next()
.context("No database name as part of path")?;
if database_name.is_empty() {
return Err(eyre!("Database path name is empty"));
}
Ok::<&str, Report>(database_name)
} else {
Ok(Default::default())
})?;
let (table_stmts, db_type) = match url.scheme() {
"mysql" => {
use sea_schema::mysql::discovery::SchemaDiscovery;
use sqlx::MySql;
tracing::info!("Connecting to MySQL");
let connection = sqlx_connect::<MySql>(
database_config.max_connections,
database_config.acquire_timeout,
url.as_str(),
None,
)
.await?;
tracing::info!("Discovering schema");
let schema_discovery = SchemaDiscovery::new(connection, database_name);
let schema = schema_discovery.discover().await?;
let table_stmts = schema
.tables
.into_iter()
.filter(|schema| filter(&schema.info.name))
.map(|schema| schema.write())
.collect();
(table_stmts, DbType::MySql)
}
"sqlite" => {
use sea_schema::sqlite::discovery::SchemaDiscovery;
use sqlx::Sqlite;
tracing::info!("Connecting to SQLite");
let connection = sqlx_connect::<Sqlite>(
database_config.max_connections,
database_config.acquire_timeout,
url.as_str(),
None,
)
.await?;
tracing::info!("Discovering schema");
let schema_discovery = SchemaDiscovery::new(connection);
let schema = schema_discovery
.discover()
.await?
.merge_indexes_into_table();
let table_stmts = schema
.tables
.into_iter()
.filter(|schema| filter(&schema.name))
.map(|schema| schema.write())
.collect();
(table_stmts, DbType::Sqlite)
}
"postgres" | "potgresql" => {
use sea_schema::postgres::discovery::SchemaDiscovery;
use sqlx::Postgres;
tracing::info!("Connecting to Postgres");
let schema = &database_config
.database_schema
.as_deref()
.unwrap_or("public");
let connection = sqlx_connect::<Postgres>(
database_config.max_connections,
database_config.acquire_timeout,
url.as_str(),
Some(schema),
)
.await?;
tracing::info!("Discovering schema");
let schema_discovery = SchemaDiscovery::new(connection, schema);
let schema = schema_discovery.discover().await?;
tracing::info!(?schema);
let table_stmts = schema
.tables
.into_iter()
.filter(|schema| filter(&schema.info.name))
.map(|schema| schema.write())
.collect();
(table_stmts, DbType::Postgres)
}
_ => unimplemented!("{} is not supported", url.scheme()),
};
tracing::info!("Schema discovered");
Ok((table_stmts, db_type))
}
async fn sqlx_connect<DB>(
max_connections: u32,
acquire_timeout: u64,
url: &str,
schema: Option<&str>,
) -> Result<sqlx::Pool<DB>>
where
DB: sqlx::Database,
for<'a> &'a mut <DB as sqlx::Database>::Connection: sqlx::Executor<'a>,
{
let mut pool_options = sqlx::pool::PoolOptions::<DB>::new()
.max_connections(max_connections)
.acquire_timeout(time::Duration::from_secs(acquire_timeout));
// Set search_path for Postgres, E.g. Some("public") by default
// MySQL & SQLite connection initialize with schema `None`
if let Some(schema) = schema {
let sql = format!("SET search_path = '{schema}'");
pool_options = pool_options.after_connect(move |conn, _| {
let sql = sql.clone();
Box::pin(async move {
sqlx::Executor::execute(conn, sql.as_str())
.await
.map(|_| ())
})
});
}
pool_options.connect(url).await.map_err(Into::into)
}

View File

@@ -1,6 +1,7 @@
use color_eyre::Result;
use path_clean::PathClean;
use std::{collections::HashMap, path::PathBuf};
use tokio::{fs::File, io::AsyncWriteExt};
pub fn pathbuf_to_rust_path(path: PathBuf) -> String {
let clean_path = path.clean();
@@ -29,6 +30,7 @@ pub fn pathbuf_to_rust_path(path: PathBuf) -> String {
#[derive(Debug, Clone)]
pub enum InsertPoint {
Start,
Replace(String),
End,
}
#[derive(Debug, Clone)]
@@ -47,20 +49,44 @@ impl FileManager {
files: HashMap::new(),
}
}
pub fn insert_file(
pub fn insert(
&mut self,
file: PathBuf,
content: String,
file: &PathBuf,
content: &str,
insert_point: Option<InsertPoint>,
) -> Result<()> {
if let Some(file) = self.files.get_mut(&file) {
if let Some(file) = self.files.get_mut(file) {
match insert_point {
Some(InsertPoint::Start) => file.content.insert_str(0, &content),
Some(InsertPoint::End) => file.content.push_str(&content),
None => file.content.push_str(&content),
Some(InsertPoint::Start) => file.content.insert_str(0, content),
Some(InsertPoint::End) => file.content.push_str(content),
None => file.content.push_str(content),
Some(InsertPoint::Replace(replace)) => {
let content = file.content.replace(&replace, content);
file.content = content;
}
}
} else {
self.files.insert(file.clone(), FileContent { content });
self.files.insert(
file.clone(),
FileContent {
content: content.to_string(),
},
);
}
Ok(())
}
pub fn get(&self, file: &PathBuf) -> Option<&FileContent> {
self.files.get(file)
}
pub async fn write_files(&self) -> Result<()> {
for (file, content) in &self.files {
tracing::debug!(?file, "Writing file");
let parent = file.parent().unwrap();
if !parent.exists() {
tokio::fs::create_dir_all(parent).await?;
}
let mut opened_file = File::create(file).await?;
opened_file.write_all(content.content.as_bytes()).await?;
}
Ok(())
}
@@ -68,7 +94,7 @@ impl FileManager {
#[cfg(test)]
mod test {
use crate::generator::file::pathbuf_to_rust_path;
use crate::generator::file::{pathbuf_to_rust_path, FileManager, InsertPoint};
use std::path::PathBuf;
#[test]
fn test_pathbuf_to_rust_path() {
@@ -90,4 +116,49 @@ mod test {
let rust_path = pathbuf_to_rust_path(path);
assert_eq!(rust_path, "");
}
#[test]
fn test_fildmanager_insert() {
let mut file_manager = FileManager::new();
let file_path = PathBuf::from("test.rs");
file_manager.insert(&file_path, "test", None).unwrap();
file_manager.insert(&file_path, "test1", None).unwrap();
assert_eq!(file_manager.get(&file_path).unwrap().content, "testtest1");
}
#[test]
fn test_fildmanager_insert_start() {
let mut file_manager = FileManager::new();
let file_path = PathBuf::from("test.rs");
file_manager.insert(&file_path, "test", None).unwrap();
file_manager
.insert(&file_path, "teststart", Some(InsertPoint::Start))
.unwrap();
assert_eq!(
file_manager.get(&file_path).unwrap().content,
"teststarttest"
);
}
#[test]
fn test_fildmanager_insert_end() {
let mut file_manager = FileManager::new();
let file_path = PathBuf::from("test.rs");
file_manager.insert(&file_path, "test", None).unwrap();
file_manager
.insert(&file_path, "testend", Some(InsertPoint::End))
.unwrap();
assert_eq!(file_manager.get(&file_path).unwrap().content, "testtestend");
}
#[test]
fn test_fildmanager_insert_replace() {
let mut file_manager = FileManager::new();
let file_path = PathBuf::from("test.rs");
file_manager.insert(&file_path, "test", None).unwrap();
file_manager
.insert(
&file_path,
"testreplace",
Some(InsertPoint::Replace("test".to_string())),
)
.unwrap();
assert_eq!(file_manager.get(&file_path).unwrap().content, "testreplace");
}
}

View File

@@ -1,4 +1,3 @@
pub mod discover;
pub mod file;
pub mod modules;
use color_eyre::Result;
@@ -15,6 +14,11 @@ pub async fn generate(database_url: &str, root_config: DocumentMut) -> Result<()
.insert(DatabaseUrl(database_url.to_owned()));
module_manager.validate().await?;
module_manager.execute().await?;
module_manager
.get_context_mut()
.get_file_manager()
.write_files()
.await?;
// let db_filter = config.sea_orm.entity.tables.get_filter();
// let (table_stmts, db_type) =

View File

@@ -0,0 +1,43 @@
use super::{models::ModelsConfig, Module, ModulesContext};
use color_eyre::Result;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct AnnotateConfig {
pub enable: bool,
}
impl Default for AnnotateConfig {
fn default() -> Self {
Self { enable: false }
}
}
#[derive(Debug, Default)]
pub struct AnnotateModule {
pub models: bool,
}
#[async_trait::async_trait]
impl Module for AnnotateModule {
fn init(&mut self, ctx: &mut ModulesContext) -> Result<()> {
ctx.get_config_auto::<AnnotateConfig>("modules.annotate")?;
Ok(())
}
async fn validate(&mut self, ctx: &mut ModulesContext) -> Result<bool> {
let map = ctx.get_anymap();
if let (Some(config), Some(models_config)) =
(map.get::<AnnotateConfig>(), map.get::<ModelsConfig>())
{
Ok(config.enable)
} else {
// One or both keys are missing
Ok(false)
}
}
async fn execute(&mut self, ctx: &mut ModulesContext) -> Result<()> {
Ok(())
}
}

View File

@@ -2,7 +2,7 @@ use color_eyre::{eyre::ContextCompat, Result};
use heck::ToUpperCamelCase;
use sea_schema::sea_query::{ColumnDef, ColumnSpec, ColumnType, IndexCreateStatement};
use crate::config::sea_orm_config::DateTimeCrate;
use crate::generator::modules::sea_orm::config::DateTimeCrate;
use super::db::DbType;
#[derive(Clone, Debug)]

View File

@@ -4,8 +4,6 @@ use color_eyre::eyre::{eyre, ContextCompat, Report, Result};
use sea_schema::sea_query::TableCreateStatement;
use url::Url;
use crate::config::db::DbConfig;
use super::DiscoveryConfig;
#[derive(Debug, Clone)]
pub enum DbType {

View File

@@ -8,7 +8,6 @@ use color_eyre::Result;
use db::DbType;
use sea_schema::sea_query::TableCreateStatement;
use serde::Deserialize;
use serde_inline_default::serde_inline_default;
use table::Table;
#[derive(Debug, Clone, Deserialize)]

View File

@@ -1,25 +1,18 @@
use std::{
fmt::Debug,
sync::{Arc, MutexGuard},
};
use std::fmt::Debug;
use anymap::{
any::{Any, CloneAny},
Map,
};
use annotate::AnnotateModule;
use anymap::{any::CloneAny, Map};
use color_eyre::{eyre::eyre, Result};
use discovery::DiscoveryModule;
use sea_orm::{SeaOrmConfig, SeaOrmModule};
// use models::ModelsModule;
use models::ModelsModule;
use sea_orm::SeaOrmModule;
use serde::{de::IntoDeserializer, Deserialize};
use std::sync::Mutex;
use templates::TemplateModule;
use toml_edit::{de::ValueDeserializer, DocumentMut, Item, Value};
// use models::table::Table;
//
// use super::discover::DbType;
use toml_edit::{DocumentMut, Item};
use super::file::FileManager;
type AnyCloneMap = Map<dyn CloneAny + Send>;
pub mod annotate;
pub mod discovery;
pub mod models;
pub mod sea_orm;
@@ -28,12 +21,14 @@ pub mod templates;
pub struct ModulesContext {
pub anymap: AnyCloneMap,
pub root_config: DocumentMut,
pub file_manager: FileManager,
}
impl ModulesContext {
pub fn new(root_config: DocumentMut) -> Self {
Self {
anymap: AnyCloneMap::new(),
root_config,
file_manager: FileManager::new(),
}
}
pub fn get_config_raw(&self, path: &str) -> Result<&Item> {
@@ -44,12 +39,12 @@ impl ModulesContext {
if let Some(v) = item.get(i) {
*item = v;
} else {
return Err(eyre!("Config not found"));
return Err(eyre!("Config not found \"{i}\""));
}
} else if let Some(v) = self.root_config.get(i) {
item = Some(v);
} else {
return Err(eyre!("Config not found"));
return Err(eyre!("Config not found \"{i}\""));
}
}
if let Some(v) = item {
@@ -58,8 +53,10 @@ impl ModulesContext {
Err(eyre!("Config not found"))
}
}
pub fn get_config<'a, V: Deserialize<'a> + Debug>(&self, path: &str) -> Result<V> {
let item = self.get_config_raw(path)?;
pub fn get_config<'a, V: Deserialize<'a> + Debug>(&self, path: &str) -> Result<Option<V>> {
let Ok(item) = self.get_config_raw(path) else {
return Ok(None);
};
let value = item
.clone()
.into_value()
@@ -67,26 +64,28 @@ impl ModulesContext {
let deserializer = value.into_deserializer();
let config = V::deserialize(deserializer)?;
tracing::debug!(?config, "{}", path);
Ok(config)
Ok(Some(config))
}
pub fn get_config_auto<'a, V: Deserialize<'a> + Clone + Send + Debug + 'static>(
pub fn get_config_auto<'a, V: Deserialize<'a> + Clone + Send + Debug + Default + 'static>(
&mut self,
path: &str,
) -> Result<()> {
let value: V = self.get_config::<V>(path)?;
self.get_anymap_mut().insert(value);
let value: Option<V> = self.get_config::<V>(path)?;
if value.is_none() {
tracing::warn!(?path, "Config not found, using default");
}
self.get_anymap_mut().insert(value.unwrap_or_default());
Ok(())
}
// pub fn get_anymap(&self) -> MutexGuard<Map> {
// let v = self.anymap.lock().unwrap();
// v
// }
pub fn get_anymap(&self) -> &AnyCloneMap {
&self.anymap
}
pub fn get_anymap_mut(&mut self) -> &mut AnyCloneMap {
&mut self.anymap
}
pub fn get_file_manager(&mut self) -> &mut FileManager {
&mut self.file_manager
}
}
#[async_trait::async_trait]
pub trait Module: Debug {
@@ -105,7 +104,9 @@ impl ModuleManager {
let modules: Vec<Box<dyn Module>> = vec![
Box::new(TemplateModule),
Box::new(DiscoveryModule),
Box::new(SeaOrmModule), //Box::new(ModelsModule)
Box::new(SeaOrmModule),
Box::new(ModelsModule),
Box::new(AnnotateModule::default()),
];
Self {
modules,

View File

@@ -1,27 +1,167 @@
use super::{Module, ModulesContext};
use color_eyre::Result;
use serde::Deserialize;
use std::path::PathBuf;
use crate::generator::file::pathbuf_to_rust_path;
use super::{
discovery::DiscoveredSchema, sea_orm::SeaOrmConfig, templates::TemplateConfig, Module,
ModulesContext,
};
use color_eyre::{
eyre::{eyre, Context, ContextCompat},
Result,
};
use handlebars::Handlebars;
use heck::ToPascalCase;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct ModelsConfig {
pub enable: bool,
pub database_schema: String,
pub max_connections: u32,
pub acquire_timeout: u32,
pub path: Option<PathBuf>,
pub prelude: bool,
}
// #[derive(Debug)]
// pub struct ModelsModule;
//
// #[async_trait::async_trait]
// impl Module for ModelsModule {
// fn init(&self, ctx: &mut ModulesContext) -> Result<()> {
// Ok(())
// }
//
// async fn validate(&self, ctx: &mut ModulesContext) -> Result<bool> {
// Ok(false)
// }
// }
impl Default for ModelsConfig {
fn default() -> Self {
Self {
enable: false,
path: None,
prelude: true,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ModelTemplateContext {
entities_path: String,
model_path: String,
model_name: String,
entity_name: String,
active_model_name: String,
prelude_path: String,
}
impl ModelTemplateContext {
pub fn new(entities_path: String, model_name: String, prelude_path: String) -> Self {
let model_path = model_name.clone();
let active_model_name = format!("{}ActiveModel", model_name).to_pascal_case();
let model_name = format!("{}Model", model_name).to_pascal_case();
let entity_name = model_path.clone().to_pascal_case();
Self {
entities_path,
model_path,
model_name,
active_model_name,
prelude_path,
entity_name,
}
}
}
#[derive(Debug)]
pub struct ModelsModule;
#[async_trait::async_trait]
impl Module for ModelsModule {
fn init(&mut self, ctx: &mut ModulesContext) -> Result<()> {
ctx.get_config_auto::<ModelsConfig>("modules.model")?;
Ok(())
}
async fn validate(&mut self, ctx: &mut ModulesContext) -> Result<bool> {
let map = ctx.get_anymap();
if let (Some(config), Some(template_config), Some(sea_orm_config)) = (
map.get::<ModelsConfig>(),
map.get::<TemplateConfig>(),
map.get::<SeaOrmConfig>(),
) {
if config.enable && !template_config.enable {
return Err(eyre!(
"\"modules.template.enable\" must be enabled to use \"modules.model.enable\""
));
}
if config.enable && !sea_orm_config.enable {
return Err(eyre!(
"\"modules.sea_orm.enable\" must be enabled to use \"modules.model.enable\""
));
}
if config.enable && config.path.is_none() {
return Err(eyre!(
"\"modules.model.path\" must be set to use \"modules.model.enable\""
));
}
Ok(config.enable && template_config.enable)
} else {
// One or both keys are missing
Ok(false)
}
}
async fn execute(&mut self, ctx: &mut ModulesContext) -> Result<()> {
let mut files: Vec<(PathBuf, String)> = Vec::new();
let map = ctx.get_anymap();
if let (Some(config), Some(templates), Some(sea_orm_config), Some(schema)) = (
map.get::<ModelsConfig>(),
map.get::<Handlebars<'static>>(),
map.get::<SeaOrmConfig>(),
map.get::<DiscoveredSchema>(),
) {
let models_path = config.path.clone().unwrap();
tracing::info!(?models_path, "Models path");
let entities_path = sea_orm_config.path.clone().unwrap();
let mod_path = models_path.join("mod.rs");
if config.prelude {
files.push((mod_path.clone(), "pub mod prelude;".to_string()));
}
for table in &schema.tables {
tracing::debug!(?table, "Generating model for table");
let path = models_path.join(format!("{}.rs", table.name));
let relative_entities_path = pathdiff::diff_paths(&entities_path, &path)
.context("Failed to calculate relative path")?;
let relative_entities_rust_path = pathbuf_to_rust_path(relative_entities_path);
let context = ModelTemplateContext::new(
relative_entities_rust_path.clone(),
table.name.clone(),
"super::prelude".to_string(),
);
if config.prelude {
let prelude_part = templates
.render("model_prelude_part", &context)
.context("Failed to render model prelude part")?;
files.push((models_path.join("prelude.rs"), prelude_part.clone()));
}
files.push((mod_path.clone(), format!("pub mod {};", table.name)));
if path.exists() {
tracing::debug!(?path, "Model file already exists");
continue;
}
if config.prelude {
let content = templates
.render("model_prelude", &context)
.context("Failed to render model prelude")?;
files.push((path.clone(), content.clone()));
} else {
let content = templates
.render("model", &context)
.context("Failed to render model")?;
files.push((path.clone(), content.clone()));
}
}
} else {
// One or both keys are missing
}
tracing::info!(?files, "Generated model files");
let file_manager = ctx.get_file_manager();
for (output_path, content) in files {
file_manager.insert(&output_path, &content, None)?;
}
Ok(())
}
}
//
//
// use crate::{

View File

@@ -1,5 +1,5 @@
use serde::{Deserialize, Deserializer, Serialize};
use serde_yaml::Value;
use toml::Value;
use sea_orm_codegen::{DateTimeCrate as CodegenDateTimeCrate, WithPrelude, WithSerde};
@@ -43,8 +43,8 @@ impl<'de> Deserialize<'de> for SerdeEnable {
match value {
Value::String(s) if s == "serialize" => Ok(SerdeEnable::Serialize),
Value::String(s) if s == "deserialize" => Ok(SerdeEnable::Deserialize),
Value::Bool(true) => Ok(SerdeEnable::Both),
Value::Bool(false) => Ok(SerdeEnable::None),
Value::Boolean(true) => Ok(SerdeEnable::Both),
Value::Boolean(false) => Ok(SerdeEnable::None),
_ => Err(serde::de::Error::custom(
"expected 'serialize', 'deserialize', 'true' or 'false'",
)),
@@ -72,8 +72,8 @@ impl<'de> Deserialize<'de> for Prelude {
let value = Value::deserialize(deserializer)?;
match value {
Value::Bool(true) => Ok(Prelude::Enabled),
Value::Bool(false) => Ok(Prelude::Disabled),
Value::Boolean(true) => Ok(Prelude::Enabled),
Value::Boolean(false) => Ok(Prelude::Disabled),
Value::String(s) if s == "allow_unused_imports" => Ok(Prelude::AllowUnusedImports),
_ => Err(serde::de::Error::custom(
"expected 'true', 'false', or 'allow_unused_imports'",

View File

@@ -66,6 +66,7 @@ impl Module for SeaOrmModule {
}
async fn execute(&mut self, ctx: &mut ModulesContext) -> Result<()> {
let map = ctx.get_anymap();
let mut outputs = vec![];
if let (Some(statements), Some(config), Some(discovery_config)) = (
map.get::<RawDiscoveredStatements>(),
map.get::<SeaOrmConfig>(),
@@ -86,18 +87,20 @@ impl Module for SeaOrmModule {
config.entity.extra_derives.eenum.clone(),
config.entity.extra_attributes.eenum.clone(),
false,
true,
false,
);
let output = EntityTransformer::transform(statements.statements.clone())?
.generate(&writer_context);
for file in output.files {
outputs.extend(output.files.into_iter().map(|file| {
let file_path = config.path.clone().unwrap_or_default();
let file_path = file_path.join(file.name);
tracing::info!(?file_path, "Generating file");
(file_path, file.content)
}));
}
// let mut file_generator = crate::generator::file::FileGenerator::new(file_path);
// file_generator.write(file.content)?;
}
let file_manager = ctx.get_file_manager();
for (output_path, content) in outputs {
file_manager.insert(&output_path, &content, None)?;
}
Ok(())

View File

@@ -1,16 +1,84 @@
use crate::generator::DatabaseUrl;
use std::{collections::HashMap, path::PathBuf};
use super::{Module, ModulesContext};
use color_eyre::Result;
use color_eyre::{
eyre::{eyre, ContextCompat},
Result,
};
use handlebars::Handlebars;
use serde::Deserialize;
use serde_inline_default::serde_inline_default;
#[serde_inline_default]
use include_dir::{include_dir, Dir, DirEntry};
use tokio::fs;
static TEMPLATE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates");
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct TemplateConfig {
#[serde_inline_default(false)]
pub enable: bool,
pub path: Option<PathBuf>,
#[serde(flatten)]
pub tables: HashMap<String, MapString>,
}
impl TemplateConfig {
pub fn to_paths(&self) -> Vec<(String, PathBuf)> {
let mut paths = Vec::new();
for (key, value) in &self.tables {
let map_string = value.clone();
paths.extend(map_string.into_paths(key.clone()));
}
let root = self.path.clone().unwrap_or_default();
paths
.into_iter()
.map(|(key, path)| {
let new_path = root.clone().join(path);
(key, new_path)
})
.collect::<Vec<_>>()
}
}
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
#[serde(untagged)]
pub enum MapString {
Map(HashMap<String, MapString>),
PathBuf(PathBuf),
}
impl MapString {
pub fn into_paths(self, prefix: String) -> Vec<(String, PathBuf)> {
fn write_path(prefix: String, string: MapString) -> Vec<(String, PathBuf)> {
let mut strings = Vec::new();
match string {
MapString::Map(inner) => {
for (key, value) in inner {
let new_prefix = if prefix.is_empty() {
key
} else {
format!("{}.{}", prefix, key)
};
strings.extend(write_path(new_prefix, value));
}
}
MapString::PathBuf(pathbuf) => {
strings.push((prefix, pathbuf));
}
}
strings.sort();
strings
}
write_path(prefix, self)
}
}
impl Default for TemplateConfig {
fn default() -> Self {
Self {
enable: true,
tables: HashMap::new(),
path: None,
}
}
}
#[derive(Debug)]
pub struct TemplateModule;
@@ -18,22 +86,118 @@ pub struct TemplateModule;
#[async_trait::async_trait]
impl Module for TemplateModule {
fn init(&mut self, ctx: &mut ModulesContext) -> Result<()> {
let registry: Handlebars<'static> = Handlebars::new();
ctx.get_anymap_mut().insert(registry);
ctx.get_config_auto::<TemplateConfig>("modules.template")?;
Ok(())
}
async fn validate(&mut self, ctx: &mut ModulesContext) -> Result<bool> {
// let map = ctx.get_anymap();
let map = ctx.get_anymap_mut();
//
// if let (Some(config), Some(_)) = (map.get::<DiscoveryConfig>(), map.get::<DatabaseUrl>()) {
// Ok(config.enable)
// } else {
// // One or both keys are missing
// Ok(false)
// }
Ok(true)
if let Some(config) = map.get::<TemplateConfig>() {
if config.enable {
for templates in config.to_paths() {
let path = templates.1;
if !path.exists() {
return Err(eyre!("Template path does not exist: {}", path.display()));
}
}
}
Ok(config.enable)
} else {
Ok(false)
}
}
async fn execute(&mut self, ctx: &mut ModulesContext) -> Result<()> {
let mut registry: Handlebars<'static> = Handlebars::new();
if let Some(config) = ctx.get_anymap().get::<TemplateConfig>() {
for (templates, path) in config.to_paths() {
tracing::debug!(?templates, ?path, "Registering template");
let content = fs::read_to_string(path).await?;
registry
.register_template_string(templates.as_str(), content)
.map_err(|_| eyre!("Failed to register template"))?;
}
Self::register_default_templates(TEMPLATE_DIR.entries(), &mut registry).await?;
}
ctx.get_anymap_mut().insert(registry);
Ok(())
}
}
impl TemplateModule {
async fn register_default_templates<'a>(
entries: &[DirEntry<'a>],
handlebars: &mut Handlebars<'a>,
) -> Result<()> {
for entry in entries.iter().filter(|file| {
file.path()
.extension()
.is_some_and(|f| f.to_str().is_some_and(|f| f == "hbs"))
|| file.as_dir().is_some()
}) {
match entry {
DirEntry::File(file) => {
let path = file.path().with_extension("");
let name = path
.to_str()
.context("Failed to convert path to str")?
.replace("/", ".");
let content = file
.contents_utf8()
.context(format!("Template {} failed to parse", name))?;
tracing::debug!(?name, "Registering template");
if !handlebars.has_template(&name) {
handlebars.register_template_string(&name, content)?;
} else {
tracing::debug!(?name, "Template already registered, skipping");
}
}
DirEntry::Dir(dir) => {
Box::pin(Self::register_default_templates(dir.entries(), handlebars)).await?;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod test {
// #[test]
// fn test_map_string() {
// use super::MapString;
// let map = MapString::Map(
// vec![
// ("a".to_string(), MapString::PathBuf("b".to_string())),
// ("c".to_string(), MapString::PathBuf("d".to_string())),
// (
// "e".to_string(),
// MapString::Map(
// vec![("f".to_string(), MapString::String("g".to_string()))]
// .into_iter()
// .collect(),
// ),
// ),
// ]
// .into_iter()
// .collect(),
// );
// let paths = map.into_paths("".to_string());
// assert_eq!(
// paths,
// vec![
// ("a".to_string(), "b".to_string()),
// ("c".to_string(), "d".to_string()),
// ("e.f".to_string(), "g".to_string())
// ]
// );
// }
// #[test]
// fn test_map_string2() {
// use super::MapString;
// let map = MapString::String("a".to_string());
// let paths = map.to_paths();
// assert_eq!(paths, vec!["a"]);
// }
}

View File

@@ -1,14 +1,9 @@
mod config;
// mod config;
mod generator;
mod templates;
use clap::Parser;
use color_eyre::{eyre::eyre, Result};
use config::Config;
use figment::{
providers::{Format, Serialized, Yaml},
Figment,
};
use handlebars::Handlebars;
use tokio::{fs, io::AsyncWriteExt, process::Command};
use toml_edit::DocumentMut;
@@ -44,7 +39,7 @@ async fn main() -> Result<()> {
let config = fs::read_to_string(args.config).await?;
let root_config = config.parse::<DocumentMut>()?;
let outputs = generator::generate(&args.database_url, root_config).await?;
generator::generate(&args.database_url, root_config).await?;
//
// // tracing::info!(?outputs, "Generated files");
// for output in outputs.iter() {

View File

@@ -1,67 +1,28 @@
use crate::config::Config;
use color_eyre::eyre::{ContextCompat, Result};
use handlebars::Handlebars;
use include_dir::{include_dir, Dir, DirEntry};
use serde_yaml::Value;
use std::path::PathBuf;
use tokio::fs;
static TEMPLATE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates");
// static TEMPLATE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates");
async fn handle_direntries<'a>(
entries: &[DirEntry<'a>],
handlebars: &mut Handlebars<'a>,
) -> Result<()> {
for entry in entries.iter().filter(|file| {
file.path()
.extension()
.is_some_and(|f| f.to_str().is_some_and(|f| f == "hbs"))
|| file.as_dir().is_some()
}) {
match entry {
DirEntry::File(file) => {
let path = file.path().with_extension("");
let name = path
.to_str()
.context("Failed to convert path to str")?
.replace("/", ".");
let content = file
.contents_utf8()
.context(format!("Template {} failed to parse", name))?;
tracing::debug!(?name, "Registering template");
if !handlebars.has_template(&name) {
handlebars.register_template_string(&name, content)?;
} else {
tracing::debug!(?name, "Template already registered, skipping");
}
}
DirEntry::Dir(dir) => {
Box::pin(handle_direntries(dir.entries(), handlebars)).await?;
}
}
}
Ok(())
}
pub async fn register_templates(handlebars: &mut Handlebars<'_>, config: &Config) -> Result<()> {
if let Some(templates) = &config.templates {
for (name, value) in templates.iter() {
let Value::String(name) = name else {
return Err(color_eyre::eyre::eyre!("Invalid template name"));
};
let Value::String(path) = value else {
return Err(color_eyre::eyre::eyre!("Invalid template value"));
};
let mut path = PathBuf::from(path);
if let Some(templates_dir) = &config.templates_dir {
path = templates_dir.join(path);
}
tracing::info!(?name, ?path, "Registering template");
let content = fs::read_to_string(path).await?;
handlebars.register_template_string(name, content)?;
}
}
handle_direntries(TEMPLATE_DIR.entries(), handlebars).await?;
Ok(())
}
// pub async fn register_templates(handlebars: &mut Handlebars<'_>, config: &Config) -> Result<()> {
// // if let Some(templates) = &config.templates {
// // for (name, value) in templates.iter() {
// // let Value::String(name) = name else {
// // return Err(color_eyre::eyre::eyre!("Invalid template name"));
// // };
// // let Value::String(path) = value else {
// // return Err(color_eyre::eyre::eyre!("Invalid template value"));
// // };
// // let mut path = PathBuf::from(path);
// // if let Some(templates_dir) = &config.templates_dir {
// // path = templates_dir.join(path);
// // }
// // tracing::info!(?name, ?path, "Registering template");
// // let content = fs::read_to_string(path).await?;
// // handlebars.register_template_string(name, content)?;
// // }
// // }
// handle_direntries(TEMPLATE_DIR.entries(), handlebars).await?;
// Ok(())
// }

View File

@@ -0,0 +1 @@
pub use {{entities_path}}::{{model_name}}::{ActiveModel as {{active_model_name}}, Model as {{model_name}}, Entity as {{entity_name}}};

View File

@@ -0,0 +1,3 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
pub mod user ;

View File

@@ -0,0 +1,9 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
use sea_orm :: entity :: prelude :: * ;
# [derive (Clone , Debug , PartialEq , DeriveEntityModel , Eq)] # [sea_orm (table_name = "user")] pub struct Model { # [sea_orm (primary_key)] pub id : i32 , # [sea_orm (unique)] pub username : String , # [sea_orm (unique)] pub email : String , pub password : String , # [sea_orm (unique)] pub test : String , }
# [derive (Copy , Clone , Debug , EnumIter , DeriveRelation)] pub enum Relation { }

1
tests/src/models/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod prelude;pub mod user;

View File

@@ -0,0 +1 @@
pub use super::_entities::UserModel::{ActiveModel as UserActiveModel, Model as UserModel, Entity as User};

9
tests/src/models/user.rs Normal file
View File

@@ -0,0 +1,9 @@
use super::prelude::*;
use sea_orm::ActiveModelBehavior;
#[async_trait::async_trait]
impl ActiveModelBehavior for UserActiveModel {}
impl UserModel {}
impl UserActiveModel {}