From 5f45671b74865e06cb3c75a496e865ea99fffa53 Mon Sep 17 00:00:00 2001 From: Matthieu Bessat Date: Tue, 11 Nov 2025 17:10:47 +0100 Subject: [PATCH] feat: one-to-many relation helper Allow one to specify that a field of a model is a foreign key. It will generate a bunch of helper methods to query related entities from one entity. --- Cargo.lock | 92 +++++++++++----- Cargo.toml | 1 + DRAFT.md | 39 +++++++ docs/tutorials/quick_start.md | 8 ++ lib/sandbox/.gitignore | 1 + lib/sandbox/Cargo.toml | 4 + lib/sandbox/justfile | 8 +- lib/sandbox/src/db.rs | 33 +++++- lib/sandbox/src/main.rs | 92 +++++++++++++++- lib/sandbox/src/migrations/all.sql | 8 +- lib/sandbox/src/models/user.rs | 16 +-- .../src/repositories/user_token_repository.rs | 43 ++++++-- lib/sqlxgentools_attrs/Cargo.toml | 2 + lib/sqlxgentools_attrs/src/lib.rs | 30 ++++- lib/sqlxgentools_cli/Cargo.toml | 26 ++--- .../migrations.rs} | 0 lib/sqlxgentools_cli/src/generators/mod.rs | 20 ++++ .../repositories/base.rs} | 96 +++++++--------- .../src/generators/repositories/mod.rs | 31 ++++++ .../src/generators/repositories/relations.rs | 71 ++++++++++++ lib/sqlxgentools_cli/src/main.rs | 21 ++-- lib/sqlxgentools_cli/src/models.rs | 46 +++++++- lib/sqlxgentools_cli/src/parse_models.rs | 104 ++++++++++++++++-- lib/sqlxgentools_misc/Cargo.toml | 20 ++++ lib/sqlxgentools_misc/src/lib.rs | 92 ++++++++++++++++ 25 files changed, 764 insertions(+), 140 deletions(-) create mode 100644 DRAFT.md create mode 100644 docs/tutorials/quick_start.md create mode 100644 lib/sandbox/.gitignore rename lib/sqlxgentools_cli/src/{gen_migrations.rs => generators/migrations.rs} (100%) create mode 100644 lib/sqlxgentools_cli/src/generators/mod.rs rename lib/sqlxgentools_cli/src/{gen_repositories.rs => generators/repositories/base.rs} (78%) create mode 100644 lib/sqlxgentools_cli/src/generators/repositories/mod.rs create mode 100644 lib/sqlxgentools_cli/src/generators/repositories/relations.rs create mode 100644 lib/sqlxgentools_misc/Cargo.toml create mode 100644 lib/sqlxgentools_misc/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b04e0b4..f6c5dfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "argh" @@ -64,7 +64,7 @@ dependencies = [ "argh_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -96,7 +96,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -112,7 +112,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn", + "syn 2.0.92", ] [[package]] @@ -317,7 +317,7 @@ checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -340,7 +340,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -419,7 +419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd8cb48eceb4e8b471af6a8e4e223cbe1286552791b9ab274512ba9cfd754df" dependencies = [ "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -718,7 +718,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -848,7 +848,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -1059,7 +1059,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.92", ] [[package]] @@ -1110,7 +1110,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -1194,11 +1194,14 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" name = "sandbox" version = "0.0.0" dependencies = [ + "anyhow", "chrono", "fully_pub", "serde", "sqlx", "sqlxgentools_attrs", + "sqlxgentools_misc", + "tokio", ] [[package]] @@ -1224,7 +1227,7 @@ checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -1395,7 +1398,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.92", ] [[package]] @@ -1418,7 +1421,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.92", "tokio", "url", ] @@ -1538,6 +1541,8 @@ version = "0.1.0" dependencies = [ "attribute-derive", "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -1556,7 +1561,17 @@ dependencies = [ "serde", "serde_json", "structmeta", - "syn", + "syn 2.0.92", +] + +[[package]] +name = "sqlxgentools_misc" +version = "0.1.0" +dependencies = [ + "fully_pub", + "serde", + "sqlx-core", + "sqlx-sqlite", ] [[package]] @@ -1585,7 +1600,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn", + "syn 2.0.92", ] [[package]] @@ -1596,7 +1611,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -1605,6 +1620,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.92" @@ -1624,7 +1650,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -1644,7 +1670,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -1686,9 +1712,21 @@ dependencies = [ "pin-project-lite", "slab", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.92", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -1720,7 +1758,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -1845,7 +1883,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.92", "wasm-bindgen-shared", ] @@ -1867,7 +1905,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2083,7 +2121,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", "synstructure", ] @@ -2105,7 +2143,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] [[package]] @@ -2125,7 +2163,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", "synstructure", ] @@ -2154,5 +2192,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.92", ] diff --git a/Cargo.toml b/Cargo.toml index fda6ce5..25718bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "lib/sqlxgentools_attrs", "lib/sqlxgentools_cli", + "lib/sqlxgentools_misc", "lib/sandbox" ] diff --git a/DRAFT.md b/DRAFT.md new file mode 100644 index 0000000..62681ef --- /dev/null +++ b/DRAFT.md @@ -0,0 +1,39 @@ +# Design draft + +This document is at the attention of the developers of sqlxgentools. + +## Implementing basic relation ship + +### Issues + +Problems with Struct non-flexibility + +### Turning the problem around: Views + +### hasMany / belongsTo relationship + +So we can implements a method + +```rs +use repositories::impls::post::RelationShips; + +let post = PostRepository::new(db).get_one_by_id("id_machin")?; +post.first_name // OK +let authors = post.get_authors()? // we need to require the implementation + +``` + +.relations() => give you a RelationFetcherBuilder +.of(entity) => give you a RelationFetcher +.author + +```rs + +let post_repo = PostRepository::new(db); +let author: User = post_repo.relations() + .of(post) + .author().ok_or(Err)?; +let comments: Vec = post_repo.relations() + .of(post) + .comments().ok_or(Err)?; +``` diff --git a/docs/tutorials/quick_start.md b/docs/tutorials/quick_start.md new file mode 100644 index 0000000..c94322a --- /dev/null +++ b/docs/tutorials/quick_start.md @@ -0,0 +1,8 @@ +# Quick start with sqlxgentools + +Steps: +- Install the crate +- Declare your models +- Generate migrations +- Generate repositories +- Use repositories in your code diff --git a/lib/sandbox/.gitignore b/lib/sandbox/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/lib/sandbox/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/lib/sandbox/Cargo.toml b/lib/sandbox/Cargo.toml index 90908af..f0ba1d4 100644 --- a/lib/sandbox/Cargo.toml +++ b/lib/sandbox/Cargo.toml @@ -8,8 +8,12 @@ name = "sandbox" path = "src/main.rs" [dependencies] +anyhow = "1.0.100" chrono = "0.4.39" fully_pub = "0.1.4" serde = "1.0.216" +tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros"] } sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "chrono", "uuid", "migrate"] } sqlxgentools_attrs = { path = "../sqlxgentools_attrs" } +sqlxgentools_misc = { path = "../sqlxgentools_misc" } + diff --git a/lib/sandbox/justfile b/lib/sandbox/justfile index 701610f..621b7ba 100644 --- a/lib/sandbox/justfile +++ b/lib/sandbox/justfile @@ -1,8 +1,12 @@ # Children justfile reset-db *args: - rm sandbox.db - sqlite3 {{args}} sandbox.db < src/migrations/all.sql + touch tmp/db.db + rm tmp/db.db + sqlite3 {{args}} tmp/db.db < src/migrations/all.sql + +seed-db: + sqlite3 tmp/db.db < src/migrations/all.sql gen-sqlx: ../../target/release/sqlx-generator \ diff --git a/lib/sandbox/src/db.rs b/lib/sandbox/src/db.rs index b3d7895..32c9d8e 100644 --- a/lib/sandbox/src/db.rs +++ b/lib/sandbox/src/db.rs @@ -1,6 +1,11 @@ +use anyhow::Context; +use std::str::FromStr; +use std::path::PathBuf; +use anyhow::Result; + use fully_pub::fully_pub; use sqlx::{ - Pool, Sqlite, + Pool, Sqlite, sqlite::{SqliteConnectOptions, SqlitePoolOptions}, }; /// database storage interface @@ -8,3 +13,29 @@ use sqlx::{ #[derive(Clone, Debug)] struct Database(Pool); + +/// Initialize database +pub async fn provide_database(sqlite_db_path: &str) -> Result { + let path = PathBuf::from(sqlite_db_path); + let is_db_initialization = !path.exists(); + // // database does not exists, trying to create it + // if path + // .parent() + // .filter(|pp| pp.exists()) + // Err(anyhow!("Could not find parent directory of the db location."))); + + let conn_str = format!("sqlite://{sqlite_db_path}"); + + let pool = SqlitePoolOptions::new() + .max_connections(50) + .connect_with(SqliteConnectOptions::from_str(&conn_str)?.create_if_missing(true)) + .await + .context("could not connect to database_url")?; + // if is_db_initialization { + // initialize_db(Database(pool.clone())).await?; + // } + + Ok(Database(pool)) +} + + diff --git a/lib/sandbox/src/main.rs b/lib/sandbox/src/main.rs index 5fef682..d198fb3 100644 --- a/lib/sandbox/src/main.rs +++ b/lib/sandbox/src/main.rs @@ -1,7 +1,95 @@ +use anyhow::{Context, Result}; + +use chrono::Utc; +use sqlx::types::Json; +use sqlxgentools_misc::ForeignRef; + +use crate::{db::provide_database, models::user::{User, UserToken}, repositories::user_token_repository::UserTokenRepository}; + pub mod models; pub mod repositories; pub mod db; -fn main() { - println!("Sandbox") + +#[tokio::main] +async fn main() -> Result<()> { + println!("Sandbox"); + + let users = vec![ + User { + id: "idu1".into(), + handle: "john.doe".into(), + full_name: None, + prefered_color: None, + last_login_at: None, + status: models::user::UserStatus::Invited, + groups: Json(vec![]), + avatar_bytes: None + }, + User { + id: "idu2".into(), + handle: "richard".into(), + full_name: None, + prefered_color: None, + last_login_at: None, + status: models::user::UserStatus::Invited, + groups: Json(vec![]), + avatar_bytes: None + }, + User { + id: "idu3".into(), + handle: "manned".into(), + full_name: None, + prefered_color: None, + last_login_at: None, + status: models::user::UserStatus::Invited, + groups: Json(vec![]), + avatar_bytes: None + } + ]; + let user_token = UserToken { + id: "idtoken1".into(), + secret: "4LP5A3F3XBV5NM8VXRGZG3QDXO9PNAC0".into(), + last_use_time: None, + creation_time: Utc::now(), + expiration_time: Utc::now(), + user_id: ForeignRef::new(&users.get(0).unwrap()) + }; + + let db = provide_database("tmp/db.db").await?; + + let user_token_repo = UserTokenRepository::new(db); + user_token_repo.insert_many(&vec![ + UserToken { + id: "idtoken2".into(), + secret: "4LP5A3F3XBV5NM8VXRGZG3QDXO9PNAC0".into(), + last_use_time: None, + creation_time: Utc::now(), + expiration_time: Utc::now(), + user_id: ForeignRef::new(&users.get(0).unwrap()) + }, + UserToken { + id: "idtoken3".into(), + secret: "CBHR6G41KSEMR1AI".into(), + last_use_time: None, + creation_time: Utc::now(), + expiration_time: Utc::now(), + user_id: ForeignRef::new(&users.get(1).unwrap()) + }, + UserToken { + id: "idtoken4".into(), + secret: "CBHR6G41KSEMR1AI".into(), + last_use_time: None, + creation_time: Utc::now(), + expiration_time: Utc::now(), + user_id: ForeignRef::new(&users.get(1).unwrap()) + } + ]).await?; + let user_tokens = user_token_repo.get_many_user_tokens_by_usersss( + vec!["idu2".into()] + ).await?; + dbg!(&user_tokens); + + Ok(()) } + diff --git a/lib/sandbox/src/migrations/all.sql b/lib/sandbox/src/migrations/all.sql index 4a9b2cc..6e2ea30 100644 --- a/lib/sandbox/src/migrations/all.sql +++ b/lib/sandbox/src/migrations/all.sql @@ -1,3 +1,5 @@ +-- DO NOT EDIT THIS FILE. +-- Generated by sqlxgentools from models files. CREATE TABLE usersss ( id TEXT NOT NULL PRIMARY KEY, handle TEXT NOT NULL UNIQUE, @@ -6,13 +8,13 @@ CREATE TABLE usersss ( last_login_at DATETIME, status TEXT NOT NULL, groups TEXT NOT NULL, - avatar_bytes BLOB NOT NULL + avatar_bytes TEXT ); CREATE TABLE user_tokens ( id TEXT NOT NULL PRIMARY KEY, - user_id TEXT NOT NULL, secret TEXT NOT NULL, last_use_time DATETIME, creation_time DATETIME NOT NULL, - expiration_time DATETIME NOT NULL + expiration_time DATETIME NOT NULL, + user_id TEXT NOT NULL ); diff --git a/lib/sandbox/src/models/user.rs b/lib/sandbox/src/models/user.rs index 1781e42..7f70cb8 100644 --- a/lib/sandbox/src/models/user.rs +++ b/lib/sandbox/src/models/user.rs @@ -2,7 +2,8 @@ use chrono::{DateTime, Utc}; use sqlx::types::Json; use fully_pub::fully_pub; -use sqlxgentools_attrs::{sql_generator_model, SqlGeneratorDerive}; +use sqlxgentools_attrs::{SqlGeneratorDerive, SqlGeneratorModelWithId, sql_generator_model}; +use sqlxgentools_misc::{DatabaseLine, ForeignRef}; #[derive(sqlx::Type, Clone, Debug, PartialEq)] #[fully_pub] @@ -13,7 +14,7 @@ enum UserStatus { Archived } -#[derive(SqlGeneratorDerive, sqlx::FromRow, Debug, Clone)] +#[derive(SqlGeneratorDerive, SqlGeneratorModelWithId, sqlx::FromRow, Debug, Clone)] #[sql_generator_model(table_name="usersss")] #[fully_pub] struct User { @@ -26,20 +27,21 @@ struct User { last_login_at: Option>, status: UserStatus, groups: Json>, - avatar_bytes: Vec + avatar_bytes: Option> } -#[derive(SqlGeneratorDerive, sqlx::FromRow, Debug, Clone)] + +#[derive(SqlGeneratorDerive, SqlGeneratorModelWithId, sqlx::FromRow, Debug, Clone)] #[sql_generator_model(table_name="user_tokens")] #[fully_pub] struct UserToken { #[sql_generator_field(is_primary=true)] id: String, - // #[sql_generator_field(foreign_key=Relation::BelongsTo(User))] - user_id: String, secret: String, last_use_time: Option>, creation_time: DateTime, - expiration_time: DateTime + expiration_time: DateTime, + #[sql_generator_field(reverse_relation_name="user_tokens")] // to generate get_user_tokens_of_user(&user_id) + user_id: ForeignRef } diff --git a/lib/sandbox/src/repositories/user_token_repository.rs b/lib/sandbox/src/repositories/user_token_repository.rs index 9ee5edd..1962898 100644 --- a/lib/sandbox/src/repositories/user_token_repository.rs +++ b/lib/sandbox/src/repositories/user_token_repository.rs @@ -40,14 +40,14 @@ impl UserTokenRepository { } pub async fn insert(&self, entity: &UserToken) -> Result<(), sqlx::Error> { sqlx::query( - "INSERT INTO user_tokens (id, user_id, secret, last_use_time, creation_time, expiration_time) VALUES ($1, $2, $3, $4, $5, $6)", + "INSERT INTO user_tokens (id, secret, last_use_time, creation_time, expiration_time, user_id) VALUES ($1, $2, $3, $4, $5, $6)", ) .bind(&entity.id) - .bind(&entity.user_id) .bind(&entity.secret) .bind(&entity.last_use_time) .bind(&entity.creation_time) .bind(&entity.expiration_time) + .bind(&entity.user_id.target_id) .execute(&self.db.0) .await?; Ok(()) @@ -69,18 +69,18 @@ impl UserTokenRepository { .collect::>() .join(", "); let query_sql = format!( - "INSERT INTO user_tokens (id, user_id, secret, last_use_time, creation_time, expiration_time) VALUES {} ON CONFLICT DO NOTHING", + "INSERT INTO user_tokens (id, secret, last_use_time, creation_time, expiration_time, user_id) VALUES {} ON CONFLICT DO NOTHING", values_templates ); let mut query = sqlx::query(&query_sql); for entity in entities { query = query .bind(&entity.id) - .bind(&entity.user_id) .bind(&entity.secret) .bind(&entity.last_use_time) .bind(&entity.creation_time) - .bind(&entity.expiration_time); + .bind(&entity.expiration_time) + .bind(&entity.user_id.target_id); } query.execute(&self.db.0).await?; Ok(()) @@ -91,15 +91,15 @@ impl UserTokenRepository { entity: &UserToken, ) -> Result<(), sqlx::Error> { sqlx::query( - "UPDATE user_tokens SET id = $2, user_id = $3, secret = $4, last_use_time = $5, creation_time = $6, expiration_time = $7 WHERE id = $1", + "UPDATE user_tokens SET id = $2, secret = $3, last_use_time = $4, creation_time = $5, expiration_time = $6, user_id = $7 WHERE id = $1", ) .bind(item_id) .bind(&entity.id) - .bind(&entity.user_id) .bind(&entity.secret) .bind(&entity.last_use_time) .bind(&entity.creation_time) .bind(&entity.expiration_time) + .bind(&entity.user_id.target_id) .execute(&self.db.0) .await?; Ok(()) @@ -111,4 +111,33 @@ impl UserTokenRepository { .await?; Ok(()) } + pub async fn get_many_user_tokens_by_user( + &self, + item_id: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as::<_, UserToken>("SELECT * FROM user_tokens WHERE user_id = $1") + .bind(item_id) + .fetch_all(&self.db.0) + .await + } + pub async fn get_many_user_tokens_by_usersss( + &self, + items_ids: Vec, + ) -> Result, sqlx::Error> { + if items_ids.is_empty() { + return Ok(vec![]); + } + let placeholder_params: String = (1..=(items_ids.len())) + .map(|i| format!("${i}")) + .collect::>() + .join(","); + let query_tmpl = format!( + "SELECT * FROM user_tokens WHERE user_id IN ({})", placeholder_params + ); + let mut query = sqlx::query_as::<_, UserToken>(&query_tmpl); + for id in items_ids { + query = query.bind(id); + } + query.fetch_all(&self.db.0).await + } } diff --git a/lib/sqlxgentools_attrs/Cargo.toml b/lib/sqlxgentools_attrs/Cargo.toml index d5c5104..62ae259 100644 --- a/lib/sqlxgentools_attrs/Cargo.toml +++ b/lib/sqlxgentools_attrs/Cargo.toml @@ -11,6 +11,8 @@ repository.workspace = true [dependencies] attribute-derive = "0.10.3" proc-macro2 = "1.0.92" +quote = "1.0" +syn = { version = "1.0" } [lib] proc-macro = true diff --git a/lib/sqlxgentools_attrs/src/lib.rs b/lib/sqlxgentools_attrs/src/lib.rs index e4a6c25..5dd1506 100644 --- a/lib/sqlxgentools_attrs/src/lib.rs +++ b/lib/sqlxgentools_attrs/src/lib.rs @@ -1,4 +1,6 @@ use proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, Fields, parse_macro_input}; #[proc_macro_attribute] pub fn sql_generator_model(_attr: TokenStream, item: TokenStream) -> TokenStream { @@ -6,8 +8,34 @@ pub fn sql_generator_model(_attr: TokenStream, item: TokenStream) -> TokenStream } #[proc_macro_derive(SqlGeneratorDerive, attributes(sql_generator_field))] -pub fn sql_generator_field(_item: TokenStream) -> TokenStream { +pub fn derive_sql_generator_model(_input: TokenStream) -> TokenStream { TokenStream::new() } +#[proc_macro_derive(SqlGeneratorModelWithId, attributes(sql_generator_field))] +pub fn derive_sql_generator_model_with_id(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + + // Extract the fields of the struct + if let syn::Data::Struct(data) = input.data { + if let Fields::Named(fields) = data.fields { + for field in fields.named { + if field.ident.as_ref().map_or(false, |ident| ident == "id") { + let expanded = quote! { + impl DatabaseLine for #name { + fn id(&self) -> String { + self.id.clone() + } + } + }; + return TokenStream::from(expanded); + } + } + } + } + + // If `id` field is not found, return an error + panic!("Expected struct with a named field `id` of type String") +} diff --git a/lib/sqlxgentools_cli/Cargo.toml b/lib/sqlxgentools_cli/Cargo.toml index afe4ad7..74aceea 100644 --- a/lib/sqlxgentools_cli/Cargo.toml +++ b/lib/sqlxgentools_cli/Cargo.toml @@ -13,19 +13,19 @@ name = "sqlx-generator" path = "src/main.rs" [dependencies] -anyhow = "1.0.95" -argh = "0.1.13" -attribute-derive = "0.10.3" -convert_case = "0.6.0" -fully_pub = "0.1.4" -heck = "0.5.0" -prettyplease = "0.2.25" -proc-macro2 = "1.0.92" -quote = "1.0.38" -serde = "1.0.216" -serde_json = "1.0.134" -structmeta = "0.3.0" -syn = { version = "2.0.92", features = ["extra-traits", "full", "parsing"] } +anyhow = "1.0" +argh = "0.1" +attribute-derive = "0.10" +convert_case = "0.6" +fully_pub = "0.1" +heck = "0.5" +prettyplease = "0.2" +proc-macro2 = "1.0" +quote = "1.0" +serde = "1.0" +serde_json = "1.0" +structmeta = "0.3" +syn = { version = "2.0", features = ["extra-traits", "full", "parsing"] } [lints.clippy] uninlined_format_args = "allow" diff --git a/lib/sqlxgentools_cli/src/gen_migrations.rs b/lib/sqlxgentools_cli/src/generators/migrations.rs similarity index 100% rename from lib/sqlxgentools_cli/src/gen_migrations.rs rename to lib/sqlxgentools_cli/src/generators/migrations.rs diff --git a/lib/sqlxgentools_cli/src/generators/mod.rs b/lib/sqlxgentools_cli/src/generators/mod.rs new file mode 100644 index 0000000..ae0ab5f --- /dev/null +++ b/lib/sqlxgentools_cli/src/generators/mod.rs @@ -0,0 +1,20 @@ +use fully_pub::fully_pub; +use serde::Serialize; + +pub mod migrations; +pub mod repositories; + +#[derive(Serialize, Debug)] +#[fully_pub] +enum SourceNode { + File(String), + Directory(Vec) +} + +#[derive(Serialize, Debug)] +#[fully_pub] +struct SourceNodeContainer { + name: String, + inner: SourceNode +} + diff --git a/lib/sqlxgentools_cli/src/gen_repositories.rs b/lib/sqlxgentools_cli/src/generators/repositories/base.rs similarity index 78% rename from lib/sqlxgentools_cli/src/gen_repositories.rs rename to lib/sqlxgentools_cli/src/generators/repositories/base.rs index dba6328..51f61cb 100644 --- a/lib/sqlxgentools_cli/src/gen_repositories.rs +++ b/lib/sqlxgentools_cli/src/generators/repositories/base.rs @@ -1,12 +1,11 @@ use anyhow::Result; -use fully_pub::fully_pub; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use serde::Serialize; use syn::File; use heck::ToSnakeCase; -use crate::models::Model; +use crate::{generators::repositories::relations::{gen_get_many_by_related_entities_method, gen_get_many_by_related_entity_method}, models::{Field, FieldForeignMode, Model}}; +use crate::generators::{SourceNode, SourceNodeContainer}; fn gen_get_all_method(model: &Model) -> TokenStream { @@ -73,6 +72,18 @@ fn gen_get_many_by_id_method(model: &Model) -> TokenStream { } } +fn get_mutation_fields(model: &Model) -> (Vec, Vec) { + let normal_field_names: Vec = model.fields.iter() + .filter(|f| match f.foreign_mode { FieldForeignMode::NotRef => true, FieldForeignMode::ForeignRef(_) => false }) + .map(|f| format_ident!("{}", &f.name)) + .collect(); + let foreign_keys_field_names: Vec = model.fields.iter() + .filter(|f| match f.foreign_mode { FieldForeignMode::NotRef => false, FieldForeignMode::ForeignRef(_) => true }) + .map(|f| format_ident!("{}", &f.name)) + .collect(); + (normal_field_names, foreign_keys_field_names) +} + fn gen_insert_method(model: &Model) -> TokenStream { let resource_ident = format_ident!("{}", &model.name); let sql_columns = model.fields.iter() @@ -89,14 +100,13 @@ fn gen_insert_method(model: &Model) -> TokenStream { sql_columns, value_templates ); - let field_names: Vec = model.fields.iter() - .map(|f| format_ident!("{}", &f.name)) - .collect(); + let (normal_field_names, foreign_keys_field_names) = get_mutation_fields(model); quote! { pub async fn insert(&self, entity: &#resource_ident) -> Result<(), sqlx::Error> { sqlx::query(#insert_query) - #( .bind( &entity.#field_names ) )* + #( .bind( &entity.#normal_field_names ) )* + #( .bind( &entity.#foreign_keys_field_names.target_id) )* .execute(&self.db.0) .await?; @@ -116,10 +126,8 @@ fn gen_insert_many_method(model: &Model) -> TokenStream { model.table_name, sql_columns ); - let field_names: Vec = model.fields.iter() - .map(|f| format_ident!("{}", &f.name)) - .collect(); - let fields_count: usize = model.fields.len(); + let (normal_field_names, foreign_keys_field_names) = get_mutation_fields(model); + let fields_count = model.fields.len(); quote! { pub async fn insert_many(&self, entities: &Vec<#resource_ident>) -> Result<(), sqlx::Error> { @@ -141,7 +149,8 @@ fn gen_insert_many_method(model: &Model) -> TokenStream { let mut query = sqlx::query(&query_sql); for entity in entities { query = query - #( .bind( &entity.#field_names ) )*; + #( .bind( &entity.#normal_field_names ) )* + #( .bind( &entity.#foreign_keys_field_names.target_id) )*; } query .execute(&self.db.0) @@ -171,15 +180,14 @@ fn gen_update_by_id_method(model: &Model) -> TokenStream { set_statements, primary_key ); - let field_names: Vec = model.fields.iter() - .map(|f| format_ident!("{}", &f.name)) - .collect(); + let (normal_field_names, foreign_keys_field_names) = get_mutation_fields(model); quote! { pub async fn #func_name_ident(&self, item_id: &str, entity: &#resource_ident) -> Result<(), sqlx::Error> { sqlx::query(#update_query) .bind(item_id) - #( .bind( &entity.#field_names ) )* + #( .bind( &entity.#normal_field_names ) )* + #( .bind( &entity.#foreign_keys_field_names.target_id) )* .execute(&self.db.0) .await?; @@ -214,7 +222,7 @@ fn gen_delete_by_id_method(model: &Model) -> TokenStream { } -fn generate_repository_file(model: &Model) -> Result { +pub fn generate_repository_file(all_models: &[Model], model: &Model) -> Result { let resource_name = model.name.clone(); let resource_module_ident = format_ident!("{}", &model.module_path.first().unwrap()); @@ -230,8 +238,18 @@ fn generate_repository_file(model: &Model) -> Result { let update_by_id_method_code = gen_update_by_id_method(model); let delete_by_id_method_code = gen_delete_by_id_method(model); - // TODO: add import line + let fields_with_foreign_refs: Vec<&Field> = model.fields.iter().filter(|f| + match f.foreign_mode { FieldForeignMode::ForeignRef(_) => true, FieldForeignMode::NotRef => false } + ).collect(); + let related_entity_methods_codes: Vec = fields_with_foreign_refs.iter().map(|field| + gen_get_many_by_related_entity_method(model, &field) + ).collect(); + let related_entities_methods_codes: Vec = fields_with_foreign_refs.iter().map(|field| + gen_get_many_by_related_entities_method(all_models, model, &field) + ).collect(); + + // TODO: add import line let base_repository_code: TokenStream = quote! { use crate::models::#resource_module_ident::#resource_ident; use crate::db::Database; @@ -260,6 +278,10 @@ fn generate_repository_file(model: &Model) -> Result { #update_by_id_method_code #delete_by_id_method_code + + #(#related_entity_methods_codes)* + + #(#related_entities_methods_codes)* } }; // convert TokenStream into rust code as string @@ -271,41 +293,3 @@ fn generate_repository_file(model: &Model) -> Result { inner: SourceNode::File(pretty) }) } - -#[derive(Serialize, Debug)] -#[fully_pub] -enum SourceNode { - File(String), - Directory(Vec) -} - -#[derive(Serialize, Debug)] -#[fully_pub] -struct SourceNodeContainer { - name: String, - inner: SourceNode -} - -/// Generate base repositories for all models -pub fn generate_repositories_source_files(models: &[Model]) -> Result { - let mut nodes: Vec = vec![]; - for model in models.iter() { - let snc = generate_repository_file(model)?; - nodes.push(snc) - } - - let mut mod_index_code: String = String::new(); - for node in &nodes { - let module_name = node.name.replace(".rs", ""); - mod_index_code.push_str(&format!("pub mod {module_name};\n")); - } - nodes.push(SourceNodeContainer { - name: "mod.rs".into(), - inner: SourceNode::File(mod_index_code.to_string()) - }); - Ok(SourceNodeContainer { - name: "".into(), - inner: SourceNode::Directory(nodes) - }) -} - diff --git a/lib/sqlxgentools_cli/src/generators/repositories/mod.rs b/lib/sqlxgentools_cli/src/generators/repositories/mod.rs new file mode 100644 index 0000000..2f45efe --- /dev/null +++ b/lib/sqlxgentools_cli/src/generators/repositories/mod.rs @@ -0,0 +1,31 @@ +pub mod base; +pub mod relations; + +use anyhow::Result; + +use crate::generators::{SourceNode, SourceNodeContainer}; +use crate::models::Model; + +/// Generate base repositories for all models +pub fn generate_repositories_source_files(models: &[Model]) -> Result { + let mut nodes: Vec = vec![]; + for model in models.iter() { + nodes.push(base::generate_repository_file(models, model)?); + // nodes.push(relations::generate_repository_file(model)?); + } + + let mut mod_index_code: String = String::new(); + for node in &nodes { + let module_name = node.name.replace(".rs", ""); + mod_index_code.push_str(&format!("pub mod {module_name};\n")); + } + nodes.push(SourceNodeContainer { + name: "mod.rs".into(), + inner: SourceNode::File(mod_index_code.to_string()) + }); + Ok(SourceNodeContainer { + name: "".into(), + inner: SourceNode::Directory(nodes) + }) +} + diff --git a/lib/sqlxgentools_cli/src/generators/repositories/relations.rs b/lib/sqlxgentools_cli/src/generators/repositories/relations.rs new file mode 100644 index 0000000..29f31b2 --- /dev/null +++ b/lib/sqlxgentools_cli/src/generators/repositories/relations.rs @@ -0,0 +1,71 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use crate::models::{Field, FieldForeignMode, Model}; + +pub fn gen_get_many_by_related_entity_method(model: &Model, foreign_key_field: &Field) -> TokenStream { + let resource_ident = format_ident!("{}", &model.name); + + let foreign_ref_params = match &foreign_key_field.foreign_mode { + FieldForeignMode::ForeignRef(params) => params, + FieldForeignMode::NotRef => { + panic!("Expected foreign key"); + } + }; + + let select_query = format!("SELECT * FROM {} WHERE {} = $1", model.table_name, foreign_key_field.name); + + let func_name_ident = format_ident!("get_many_{}_by_{}", foreign_ref_params.reverse_relation_name, foreign_ref_params.target_resource_name); + + quote! { + pub async fn #func_name_ident(&self, item_id: &str) -> Result, sqlx::Error> { + sqlx::query_as::<_, #resource_ident>(#select_query) + .bind(item_id) + .fetch_all(&self.db.0) + .await + } + } +} + +pub fn gen_get_many_by_related_entities_method(all_models: &[Model], model: &Model, foreign_key_field: &Field) -> TokenStream { + let resource_ident = format_ident!("{}", &model.name); + + let foreign_ref_params = match &foreign_key_field.foreign_mode { + FieldForeignMode::ForeignRef(params) => params, + FieldForeignMode::NotRef => { + panic!("Expected foreign key"); + } + }; + + let select_query = format!("SELECT * FROM {} WHERE {} IN ({{}})", model.table_name, foreign_key_field.name); + + let target_resource = all_models.iter() + .find(|m| m.name.to_lowercase() == foreign_ref_params.target_resource_name.to_lowercase()) + .expect("Could not find foreign ref target type associated resource"); + + let func_name_ident = format_ident!("get_many_{}_by_{}", foreign_ref_params.reverse_relation_name, target_resource.table_name); + + quote! { + pub async fn #func_name_ident(&self, items_ids: Vec) -> Result, sqlx::Error> { + if items_ids.is_empty() { + return Ok(vec![]) + } + let placeholder_params: String = (1..=(items_ids.len())) + .map(|i| format!("${i}")) + .collect::>() + .join(","); + let query_tmpl = format!( + #select_query, + placeholder_params + ); + let mut query = sqlx::query_as::<_, #resource_ident>(&query_tmpl); + for id in items_ids { + query = query.bind(id) + } + + query + .fetch_all(&self.db.0) + .await + } + } +} diff --git a/lib/sqlxgentools_cli/src/main.rs b/lib/sqlxgentools_cli/src/main.rs index bd72c2e..17cfc13 100644 --- a/lib/sqlxgentools_cli/src/main.rs +++ b/lib/sqlxgentools_cli/src/main.rs @@ -3,13 +3,15 @@ use attribute_derive::FromAttr; use argh::FromArgs; use anyhow::{Result, anyhow}; -use gen_migrations::generate_create_table_sql; -use gen_repositories::{generate_repositories_source_files, SourceNodeContainer}; + +use crate::generators::{SourceNode, SourceNodeContainer}; + +// use gen_migrations::generate_create_table_sql; +// use gen_repositories::{generate_repositories_source_files, SourceNodeContainer}; pub mod models; pub mod parse_models; -pub mod gen_migrations; -pub mod gen_repositories; +pub mod generators; #[derive(FromAttr, PartialEq, Debug, Default)] #[attribute(ident = sql_generator_model)] @@ -21,7 +23,8 @@ pub struct SqlGeneratorModelAttr { #[attribute(ident = sql_generator_field)] pub struct SqlGeneratorFieldAttr { is_primary: Option, - is_unique: Option + is_unique: Option, + reverse_relation_name: Option } @@ -68,11 +71,11 @@ struct GeneratorArgs { fn write_source_code(base_path: &Path, snc: SourceNodeContainer) -> Result<()> { let path = base_path.join(snc.name); match snc.inner { - gen_repositories::SourceNode::File(code) => { + SourceNode::File(code) => { println!("writing file {:?}", path); std::fs::write(path, code)?; }, - gen_repositories::SourceNode::Directory(dir) => { + SourceNode::Directory(dir) => { for node in dir { write_source_code(&path, node)?; } @@ -126,13 +129,13 @@ pub fn main() -> Result<()> { if !repositories_mod_path.exists() { return Err(anyhow!("Could not resolve repositories modules.")); } - let snc = generate_repositories_source_files(&models)?; + let snc = generators::repositories::generate_repositories_source_files(&models)?; dbg!(&snc); write_source_code(&repositories_mod_path, snc)?; }, GeneratorArgsSubCommands::GenerateMigration(opts) => { eprintln!("Generating migrations…"); - let sql_code = generate_create_table_sql(&models)?; + let sql_code = generators::migrations::generate_create_table_sql(&models)?; if let Some(out_location) = opts.output { let output_path = Path::new(&out_location); let write_res = std::fs::write(output_path, sql_code); diff --git a/lib/sqlxgentools_cli/src/models.rs b/lib/sqlxgentools_cli/src/models.rs index 0d31561..e16af70 100644 --- a/lib/sqlxgentools_cli/src/models.rs +++ b/lib/sqlxgentools_cli/src/models.rs @@ -10,12 +10,54 @@ struct Model { fields: Vec } -#[derive(Debug)] +impl Model { + // pub fn concrete_fields(&self) -> Vec { + // self.fields.iter().map(|f| { + // if f.is_foreign_ref { + // Field { + // name: f.name.clone(), + // rust_type: "String".into(), + // is_nullable: f.is_nullable.clone(), + // is_unique: f.is_unique.clone(), + // is_primary: false, + // is_foreign_ref: false + // } + // } else { + // f.clone() + // } + // }).collect() + // } +} + + +#[derive(Debug, Clone)] +#[fully_pub] +struct ForeignRefParams { + /// eg. "tokens" + reverse_relation_name: String, + /// eg. "user" + target_resource_name: String, + // /// eg. "users" + // target_resource_name_plural: String +} + + +#[derive(Debug, Clone)] +#[fully_pub] +enum FieldForeignMode { + ForeignRef(ForeignRefParams), + NotRef +} + +#[derive(Debug, Clone)] #[fully_pub] struct Field { name: String, rust_type: String, is_nullable: bool, is_unique: bool, - is_primary: bool + is_primary: bool, + foreign_mode: FieldForeignMode, + // is_foreign_ref: bool, + // reverse_relation_name: Option } diff --git a/lib/sqlxgentools_cli/src/parse_models.rs b/lib/sqlxgentools_cli/src/parse_models.rs index 65bfe0b..0f8b5d8 100644 --- a/lib/sqlxgentools_cli/src/parse_models.rs +++ b/lib/sqlxgentools_cli/src/parse_models.rs @@ -3,13 +3,13 @@ use attribute_derive::FromAttr; use anyhow::{Result, anyhow}; use convert_case::{Case, Casing}; -use syn::Type; +use syn::{GenericArgument, PathArguments, Type}; -use crate::{models::{Field, Model}, SqlGeneratorFieldAttr, SqlGeneratorModelAttr}; +use crate::{SqlGeneratorFieldAttr, SqlGeneratorModelAttr, models::{Field, FieldForeignMode, ForeignRefParams, Model}}; -fn extract_generic_type(base_segments: Vec, ty: &syn::Type) -> Option<&syn::Type> { +fn extract_generic_type(base_segments: Vec, ty: &Type) -> Option<&Type> { // If it is not `TypePath`, it is not possible to be `Option`, return `None` - if let syn::Type::Path(syn::TypePath { qself: None, path }) = ty { + if let Type::Path(syn::TypePath { qself: None, path }) = ty { // We have limited the 5 ways to write `Option`, and we can see that after `Option`, // there will be no `PathSegment` of the same level // Therefore, we only need to take out the highest level `PathSegment` and splice it into a string @@ -41,7 +41,7 @@ fn extract_generic_type(base_segments: Vec, ty: &syn::Type) -> Option<&s // If it is not a type, it is not possible to be `Option`, return `None` // But this situation may not occur .and_then(|generic_arg| match generic_arg { - syn::GenericArgument::Type(ty) => Some(ty), + GenericArgument::Type(ty) => Some(ty), _ => None, }); // Return `T` in `Option` @@ -52,7 +52,7 @@ fn extract_generic_type(base_segments: Vec, ty: &syn::Type) -> Option<&s fn get_type_first_ident(inp: &Type) -> Option { match inp { - syn::Type::Path(field_type_path) => { + Type::Path(field_type_path) => { Some(field_type_path.path.segments.get(0).unwrap().ident.to_string()) }, _ => { @@ -61,6 +61,31 @@ fn get_type_first_ident(inp: &Type) -> Option { } } +fn get_first_generic_arg_type_ident(inp: &Type) -> Option { + if let Type::Path(field_type_path) = inp { + if let PathArguments::AngleBracketed(args) = &field_type_path.path.segments.get(0).unwrap().arguments { + if args.args.is_empty() { + None + } else { + if let GenericArgument::Type(arg_type) = args.args.get(0).unwrap() { + if let Type::Path(arg_type_path) = arg_type { + Some(arg_type_path.path.segments.get(0).unwrap().ident.to_string()) + } else { + None + } + } else { + None + } + } + } else { + None + } + } else { + None + } +} + + fn parse_model_attribute(item: &syn::ItemStruct) -> Result> { for attr in item.attrs.iter() { let attr_ident = match attr.path().get_ident() { @@ -140,14 +165,15 @@ pub fn parse_models(source_code_path: &Path) -> Result> { for field in itemval.fields.iter() { let field_name = field.ident.clone().unwrap().to_string(); let field_type = field.ty.clone(); - // println!("field {}", field_name); + println!("field {} {:?}", field_name, field_type); let mut output_field = Field { name: field_name, rust_type: "Unknown".into(), is_nullable: false, is_primary: false, - is_unique: false + is_unique: false, + foreign_mode: FieldForeignMode::NotRef }; let first_type: String = match get_type_first_ident(&field_type) { @@ -194,8 +220,40 @@ pub fn parse_models(source_code_path: &Path) -> Result> { } output_field.rust_type = final_type; + + let field_attrs_opt = parse_field_attribute(field)?; + if first_type == "ForeignRef" { + let attrs = match &field_attrs_opt { + Some(attrs) => attrs, + None => { + return Err(anyhow!("Found a ForeignRef type but did not found attributes.")) + } + }; + let rrn = match &attrs.reverse_relation_name { + Some(rrn) => rrn.clone(), + None => { + return Err(anyhow!("Found a ForeignRef type but did not found reverse_relation_name attribute.")) + } + }; + + let extract_res = extract_generic_type(vec!["ForeignRef".into()], &field_type) + .and_then(|t| get_type_first_ident(t)); + let target_type_name = match extract_res { + Some(v) => v, + None => { + return Err(anyhow!("Could not extract inner type from ForeignRef.")); + } + }; + output_field.foreign_mode = FieldForeignMode::ForeignRef( + ForeignRefParams { + reverse_relation_name: rrn, + target_resource_name: target_type_name.to_lowercase() + } + ); + } + // parse attribute - if let Some(field_attr) = parse_field_attribute(field)? { + if let Some(field_attr) = field_attrs_opt { output_field.is_primary = field_attr.is_primary.unwrap_or_default(); output_field.is_unique = field_attr.is_unique.unwrap_or_default(); } @@ -217,7 +275,7 @@ pub fn parse_models(source_code_path: &Path) -> Result> { } /// Scan for models struct in a rust file and return a struct representing the model -pub fn parse_models_from_module(module_path: &Path) -> Result> { +fn parse_models_from_module_inner(module_path: &Path) -> Result> { let mut models: Vec = vec![]; if module_path.is_file() { @@ -234,3 +292,29 @@ pub fn parse_models_from_module(module_path: &Path) -> Result> { Ok(models) } + +// fn complete_models(original_models: Vec) -> Result> { +// let mut new_models: Vec = vec![]; +// for model in original_models { +// for original_field in model.fields { +// let mut field = original_field +// match original_field.foreign_mode { +// FieldForeignMode::NotRef => {}, +// FieldForeignMode::ForeignRef(ref_params) => { + +// } +// } + +// } +// } +// Ok(new_models) +// } + +/// Scan for models struct in a rust file and return a struct representing the model +pub fn parse_models_from_module(module_path: &Path) -> Result> { + let models = parse_models_from_module_inner(module_path)?; + + // let models = complete_models(models)?; + + Ok(models) +} diff --git a/lib/sqlxgentools_misc/Cargo.toml b/lib/sqlxgentools_misc/Cargo.toml new file mode 100644 index 0000000..dfbfcb6 --- /dev/null +++ b/lib/sqlxgentools_misc/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "sqlxgentools_misc" +description = "Various misc class to use in applications that use sqlxgentools" +publish = true +edition.workspace = true +authors.workspace = true +version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +sqlx-core = { version = "=0.8.6" } +sqlx-sqlite = { version = "=0.8.6", features = ["offline"] } +fully_pub = "0.1" +serde = { version = "1.0", features = ["derive"] } + +[lib] + +[lints.clippy] +uninlined_format_args = "allow" diff --git a/lib/sqlxgentools_misc/src/lib.rs b/lib/sqlxgentools_misc/src/lib.rs new file mode 100644 index 0000000..f20a305 --- /dev/null +++ b/lib/sqlxgentools_misc/src/lib.rs @@ -0,0 +1,92 @@ +use std::error::Error; +use std::marker::PhantomData; + +use fully_pub::fully_pub; + +use serde::{Serialize, Serializer}; +use sqlx_core::any::{Any, AnyArgumentBuffer}; +use sqlx_core::database::Database; +use sqlx_core::decode::Decode; +use sqlx_core::encode::{Encode, IsNull}; +use sqlx_core::error::BoxDynError; +use sqlx_core::types::Type; +use sqlx_sqlite::{Sqlite, SqliteArgumentValue}; + + +#[fully_pub] +trait DatabaseLine { + fn id(&self) -> String; +} + + +/// Wrapper to mark a model field as foreign +/// You can use a generic argument inside ForeignRef to point to the target model +#[derive(Clone, Debug)] +#[fully_pub] +struct ForeignRef { + pub target_type: PhantomData, + pub target_id: String +} + + +// Implement serde Serialize for ForeignRef +impl Serialize for ForeignRef { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Serialize only the target_id as a string + serializer.serialize_str(&self.target_id) + } +} + + +impl ForeignRef { + pub fn new(entity: &T) -> ForeignRef { + ForeignRef { + target_type: PhantomData, + target_id: entity.id() + } + } +} + + +impl<'r, DB: Database, T: Sized + DatabaseLine> Decode<'r, DB> for ForeignRef +where + // we want to delegate some of the work to string decoding so let's make sure strings + // are supported by the database + &'r str: Decode<'r, DB> +{ + fn decode( + value: ::ValueRef<'r>, + ) -> Result, Box> { + let value = <&str as Decode>::decode(value)?; + + let ref_val: String = value.parse()?; + + Ok(ForeignRef:: { + target_type: PhantomData, + target_id: ref_val + }) + } +} + +impl Encode<'_, Any> for ForeignRef { + fn encode_by_ref(&self, buf: &mut AnyArgumentBuffer) -> Result { + >::encode_by_ref(&self.target_id.to_string(), buf) + } +} + +impl Type for ForeignRef { + fn type_info() -> ::TypeInfo { + >::type_info() + } +} + +impl Encode<'_, Sqlite> for ForeignRef { + fn encode_by_ref(&self, args: &mut Vec>) -> Result { + args.push(SqliteArgumentValue::Text(self.target_id.clone().into())); + Ok(IsNull::No) + } +} +