From 7e7a3ccd29313744122da5abde3ca9ec7e819962 Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Tue, 10 Jun 2025 11:42:00 +0200 Subject: [PATCH] refactor(macros): split trait implementations into modular files Move trait implementation code from single monolithic file into separate modules organised by operation type (create, delete, find, update, upsert) for better code organisation and maintainability. --- georm-macros/src/georm/mod.rs | 8 +- .../src/georm/trait_implementation.rs | 268 ------------------ georm-macros/src/georm/traits/create.rs | 27 ++ georm-macros/src/georm/traits/delete.rs | 39 +++ georm-macros/src/georm/traits/find.rs | 47 +++ georm-macros/src/georm/traits/mod.rs | 70 +++++ georm-macros/src/georm/traits/update.rs | 48 ++++ georm-macros/src/georm/traits/upsert.rs | 53 ++++ 8 files changed, 288 insertions(+), 272 deletions(-) delete mode 100644 georm-macros/src/georm/trait_implementation.rs create mode 100644 georm-macros/src/georm/traits/create.rs create mode 100644 georm-macros/src/georm/traits/delete.rs create mode 100644 georm-macros/src/georm/traits/find.rs create mode 100644 georm-macros/src/georm/traits/mod.rs create mode 100644 georm-macros/src/georm/traits/update.rs create mode 100644 georm-macros/src/georm/traits/upsert.rs diff --git a/georm-macros/src/georm/mod.rs b/georm-macros/src/georm/mod.rs index 817d953..63c82b5 100644 --- a/georm-macros/src/georm/mod.rs +++ b/georm-macros/src/georm/mod.rs @@ -1,11 +1,12 @@ -use ir::GeormField; use quote::quote; mod composite_keys; mod defaultable_struct; mod ir; +pub(crate) use ir::GeormField; mod relationships; -mod trait_implementation; +mod traits; +pub(crate) use composite_keys::IdType; fn extract_georm_field_attrs(ast: &mut syn::DeriveInput) -> deluxe::Result> { let syn::Data::Struct(s) = &mut ast.data else { @@ -50,8 +51,7 @@ pub fn georm_derive_macro2( let relationships = relationships::derive_relationships(&ast, &struct_attrs, &fields, &identifier); - let trait_impl = - trait_implementation::derive_trait(&ast, &struct_attrs.table, &fields, &identifier); + let trait_impl = traits::derive_trait(&ast, &struct_attrs.table, &fields, &identifier); let code = quote! { #id_struct diff --git a/georm-macros/src/georm/trait_implementation.rs b/georm-macros/src/georm/trait_implementation.rs deleted file mode 100644 index 9b9e277..0000000 --- a/georm-macros/src/georm/trait_implementation.rs +++ /dev/null @@ -1,268 +0,0 @@ -use super::composite_keys::IdType; -use super::ir::GeormField; -use quote::quote; - -fn generate_find_all_query(table: &str) -> proc_macro2::TokenStream { - let find_string = format!("SELECT * FROM {table}"); - quote! { - async fn find_all(pool: &::sqlx::PgPool) -> ::sqlx::Result> { - ::sqlx::query_as!(Self, #find_string).fetch_all(pool).await - } - } -} - -fn generate_find_query(table: &str, id: &IdType) -> proc_macro2::TokenStream { - match id { - IdType::Simple { - field_name, - field_type, - } => { - let find_string = format!("SELECT * FROM {table} WHERE {} = $1", field_name); - quote! { - async fn find(pool: &::sqlx::PgPool, id: &#field_type) -> ::sqlx::Result> { - ::sqlx::query_as!(Self, #find_string, id) - .fetch_optional(pool) - .await - } - } - } - IdType::Composite { fields, field_type } => { - let id_match_string = fields - .iter() - .enumerate() - .map(|(i, field)| format!("{} = ${}", field.name, i + 1)) - .collect::>() - .join(" AND "); - let id_members: Vec = - fields.iter().map(|field| field.name.clone()).collect(); - let find_string = format!("SELECT * FROM {table} WHERE {id_match_string}"); - quote! { - async fn find(pool: &::sqlx::PgPool, id: &#field_type) -> ::sqlx::Result> { - ::sqlx::query_as!(Self, #find_string, #(id.#id_members),*) - .fetch_optional(pool) - .await - } - } - } - } -} - -fn generate_create_query(table: &str, fields: &[GeormField]) -> proc_macro2::TokenStream { - let inputs: Vec = (1..=fields.len()).map(|num| format!("${num}")).collect(); - let create_string = format!( - "INSERT INTO {table} ({}) VALUES ({}) RETURNING *", - fields - .iter() - .map(|f| f.ident.to_string()) - .collect::>() - .join(", "), - inputs.join(", ") - ); - let field_idents: Vec = fields.iter().map(|f| f.ident.clone()).collect(); - quote! { - async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { - ::sqlx::query_as!( - Self, - #create_string, - #(self.#field_idents),* - ) - .fetch_one(pool) - .await - } - } -} - -fn generate_update_query( - table: &str, - fields: &[GeormField], - id: &IdType, -) -> proc_macro2::TokenStream { - let non_id_fields: Vec = fields - .iter() - .filter_map(|f| if f.id { None } else { Some(f.ident.clone()) }) - .collect(); - let update_columns = non_id_fields - .iter() - .enumerate() - .map(|(i, field)| format!("{} = ${}", field, i + 1)) - .collect::>() - .join(", "); - let mut all_fields = non_id_fields.clone(); - let where_clause = match id { - IdType::Simple { field_name, .. } => { - let where_clause = format!("{} = ${}", field_name, non_id_fields.len() + 1); - all_fields.push(field_name.clone()); - where_clause - } - IdType::Composite { fields, .. } => fields - .iter() - .enumerate() - .map(|(i, field)| { - let where_clause = format!("{} = ${}", field.name, non_id_fields.len() + i + 1); - all_fields.push(field.name.clone()); - where_clause - }) - .collect::>() - .join(" AND "), - }; - let update_string = - format!("UPDATE {table} SET {update_columns} WHERE {where_clause} RETURNING *"); - quote! { - async fn update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { - ::sqlx::query_as!( - Self, #update_string, #(self.#all_fields),* - ) - .fetch_one(pool) - .await - } - } -} - -fn generate_delete_query(table: &str, id: &IdType) -> proc_macro2::TokenStream { - let where_clause = match id { - IdType::Simple { field_name, .. } => format!("{} = $1", field_name), - IdType::Composite { fields, .. } => fields - .iter() - .enumerate() - .map(|(i, field)| format!("{} = ${}", field.name, i + 1)) - .collect::>() - .join(" AND "), - }; - let query_args = match id { - IdType::Simple { .. } => quote! { id }, - IdType::Composite { fields, .. } => { - let fields: Vec = fields.iter().map(|f| f.name.clone()).collect(); - quote! { #(id.#fields), * } - } - }; - let id_type = match id { - IdType::Simple { field_type, .. } => quote! { #field_type }, - IdType::Composite { field_type, .. } => quote! { #field_type }, - }; - let delete_string = format!("DELETE FROM {table} WHERE {where_clause}"); - quote! { - async fn delete_by_id(pool: &::sqlx::PgPool, id: &#id_type) -> ::sqlx::Result { - let rows_affected = ::sqlx::query!(#delete_string, #query_args) - .execute(pool) - .await? - .rows_affected(); - Ok(rows_affected) - } - - async fn delete(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { - Self::delete_by_id(pool, &self.get_id()).await - } - } -} - -fn generate_upsert_query( - table: &str, - fields: &[GeormField], - id: &IdType, -) -> proc_macro2::TokenStream { - let inputs: Vec = (1..=fields.len()).map(|num| format!("${num}")).collect(); - let columns = fields - .iter() - .map(|f| f.ident.to_string()) - .collect::>() - .join(", "); - - let primary_key: proc_macro2::TokenStream = match id { - IdType::Simple { field_name, .. } => quote! {#field_name}, - IdType::Composite { fields, .. } => { - let field_names: Vec = fields.iter().map(|f| f.name.clone()).collect(); - quote! { - #(#field_names),* - } - } - }; - - // For ON CONFLICT DO UPDATE, exclude the ID field from updates - let update_assignments = fields - .iter() - .filter(|f| !f.id) - .map(|f| format!("{} = EXCLUDED.{}", f.ident, f.ident)) - .collect::>() - .join(", "); - - let upsert_string = format!( - "INSERT INTO {table} ({columns}) VALUES ({}) ON CONFLICT ({}) DO UPDATE SET {update_assignments} RETURNING *", - inputs.join(", "), - primary_key - ); - - let field_idents: Vec = fields.iter().map(|f| f.ident.clone()).collect(); - - quote! { - async fn create_or_update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { - ::sqlx::query_as!( - Self, - #upsert_string, - #(self.#field_idents),* - ) - .fetch_one(pool) - .await - } - } -} - -fn generate_get_id(id: &IdType) -> proc_macro2::TokenStream { - match id { - IdType::Simple { - field_name, - field_type, - } => { - quote! { - fn get_id(&self) -> #field_type { - self.#field_name.clone() - } - } - } - IdType::Composite { fields, field_type } => { - let field_names: Vec = fields.iter().map(|f| f.name.clone()).collect(); - quote! { - fn get_id(&self) -> #field_type { - #field_type { - #(#field_names: self.#field_names),* - } - } - } - } - } -} - -pub fn derive_trait( - ast: &syn::DeriveInput, - table: &str, - fields: &[GeormField], - id: &IdType, -) -> proc_macro2::TokenStream { - let ty = match id { - IdType::Simple { field_type, .. } => quote! {#field_type}, - IdType::Composite { field_type, .. } => quote! {#field_type}, - }; - - // define impl variables - let ident = &ast.ident; - let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); - - // generate - let get_all = generate_find_all_query(table); - let get_id = generate_get_id(id); - let find_query = generate_find_query(table, id); - let create_query = generate_create_query(table, fields); - let update_query = generate_update_query(table, fields, id); - let upsert_query = generate_upsert_query(table, fields, id); - let delete_query = generate_delete_query(table, id); - quote! { - impl #impl_generics Georm<#ty> for #ident #type_generics #where_clause { - #get_all - #get_id - #find_query - #create_query - #update_query - #upsert_query - #delete_query - } - } -} diff --git a/georm-macros/src/georm/traits/create.rs b/georm-macros/src/georm/traits/create.rs new file mode 100644 index 0000000..774d9e8 --- /dev/null +++ b/georm-macros/src/georm/traits/create.rs @@ -0,0 +1,27 @@ +use crate::georm::GeormField; +use quote::quote; + +pub fn generate_create_query(table: &str, fields: &[GeormField]) -> proc_macro2::TokenStream { + let inputs: Vec = (1..=fields.len()).map(|num| format!("${num}")).collect(); + let create_string = format!( + "INSERT INTO {table} ({}) VALUES ({}) RETURNING *", + fields + .iter() + .map(|f| f.ident.to_string()) + .collect::>() + .join(", "), + inputs.join(", ") + ); + let field_idents: Vec = fields.iter().map(|f| f.ident.clone()).collect(); + quote! { + async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + ::sqlx::query_as!( + Self, + #create_string, + #(self.#field_idents),* + ) + .fetch_one(pool) + .await + } + } +} diff --git a/georm-macros/src/georm/traits/delete.rs b/georm-macros/src/georm/traits/delete.rs new file mode 100644 index 0000000..7cc4ee7 --- /dev/null +++ b/georm-macros/src/georm/traits/delete.rs @@ -0,0 +1,39 @@ +use crate::georm::IdType; +use quote::quote; + +pub fn generate_delete_query(table: &str, id: &IdType) -> proc_macro2::TokenStream { + let where_clause = match id { + IdType::Simple { field_name, .. } => format!("{} = $1", field_name), + IdType::Composite { fields, .. } => fields + .iter() + .enumerate() + .map(|(i, field)| format!("{} = ${}", field.name, i + 1)) + .collect::>() + .join(" AND "), + }; + let query_args = match id { + IdType::Simple { .. } => quote! { id }, + IdType::Composite { fields, .. } => { + let fields: Vec = fields.iter().map(|f| f.name.clone()).collect(); + quote! { #(id.#fields), * } + } + }; + let id_type = match id { + IdType::Simple { field_type, .. } => quote! { #field_type }, + IdType::Composite { field_type, .. } => quote! { #field_type }, + }; + let delete_string = format!("DELETE FROM {table} WHERE {where_clause}"); + quote! { + async fn delete_by_id(pool: &::sqlx::PgPool, id: &#id_type) -> ::sqlx::Result { + let rows_affected = ::sqlx::query!(#delete_string, #query_args) + .execute(pool) + .await? + .rows_affected(); + Ok(rows_affected) + } + + async fn delete(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + Self::delete_by_id(pool, &self.get_id()).await + } + } +} diff --git a/georm-macros/src/georm/traits/find.rs b/georm-macros/src/georm/traits/find.rs new file mode 100644 index 0000000..c47e9f8 --- /dev/null +++ b/georm-macros/src/georm/traits/find.rs @@ -0,0 +1,47 @@ +use crate::georm::IdType; +use quote::quote; + +pub fn generate_find_all_query(table: &str) -> proc_macro2::TokenStream { + let find_string = format!("SELECT * FROM {table}"); + quote! { + async fn find_all(pool: &::sqlx::PgPool) -> ::sqlx::Result> { + ::sqlx::query_as!(Self, #find_string).fetch_all(pool).await + } + } +} + +pub fn generate_find_query(table: &str, id: &IdType) -> proc_macro2::TokenStream { + match id { + IdType::Simple { + field_name, + field_type, + } => { + let find_string = format!("SELECT * FROM {table} WHERE {} = $1", field_name); + quote! { + async fn find(pool: &::sqlx::PgPool, id: &#field_type) -> ::sqlx::Result> { + ::sqlx::query_as!(Self, #find_string, id) + .fetch_optional(pool) + .await + } + } + } + IdType::Composite { fields, field_type } => { + let id_match_string = fields + .iter() + .enumerate() + .map(|(i, field)| format!("{} = ${}", field.name, i + 1)) + .collect::>() + .join(" AND "); + let id_members: Vec = + fields.iter().map(|field| field.name.clone()).collect(); + let find_string = format!("SELECT * FROM {table} WHERE {id_match_string}"); + quote! { + async fn find(pool: &::sqlx::PgPool, id: &#field_type) -> ::sqlx::Result> { + ::sqlx::query_as!(Self, #find_string, #(id.#id_members),*) + .fetch_optional(pool) + .await + } + } + } + } +} diff --git a/georm-macros/src/georm/traits/mod.rs b/georm-macros/src/georm/traits/mod.rs new file mode 100644 index 0000000..493bab4 --- /dev/null +++ b/georm-macros/src/georm/traits/mod.rs @@ -0,0 +1,70 @@ +use super::composite_keys::IdType; +use super::ir::GeormField; +use quote::quote; + +mod create; +mod delete; +mod find; +mod update; +mod upsert; + +fn generate_get_id(id: &IdType) -> proc_macro2::TokenStream { + match id { + IdType::Simple { + field_name, + field_type, + } => { + quote! { + fn get_id(&self) -> #field_type { + self.#field_name.clone() + } + } + } + IdType::Composite { fields, field_type } => { + let field_names: Vec = fields.iter().map(|f| f.name.clone()).collect(); + quote! { + fn get_id(&self) -> #field_type { + #field_type { + #(#field_names: self.#field_names),* + } + } + } + } + } +} + +pub fn derive_trait( + ast: &syn::DeriveInput, + table: &str, + fields: &[GeormField], + id: &IdType, +) -> proc_macro2::TokenStream { + let ty = match id { + IdType::Simple { field_type, .. } => quote! {#field_type}, + IdType::Composite { field_type, .. } => quote! {#field_type}, + }; + + // define impl variables + let ident = &ast.ident; + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); + + // generate + let get_id = generate_get_id(id); + let get_all = find::generate_find_all_query(table); + let find_query = find::generate_find_query(table, id); + let create_query = create::generate_create_query(table, fields); + let update_query = update::generate_update_query(table, fields, id); + let upsert_query = upsert::generate_upsert_query(table, fields, id); + let delete_query = delete::generate_delete_query(table, id); + quote! { + impl #impl_generics Georm<#ty> for #ident #type_generics #where_clause { + #get_all + #get_id + #find_query + #create_query + #update_query + #upsert_query + #delete_query + } + } +} diff --git a/georm-macros/src/georm/traits/update.rs b/georm-macros/src/georm/traits/update.rs new file mode 100644 index 0000000..0059139 --- /dev/null +++ b/georm-macros/src/georm/traits/update.rs @@ -0,0 +1,48 @@ +use crate::georm::{GeormField, IdType}; +use quote::quote; + +pub fn generate_update_query( + table: &str, + fields: &[GeormField], + id: &IdType, +) -> proc_macro2::TokenStream { + let non_id_fields: Vec = fields + .iter() + .filter_map(|f| if f.id { None } else { Some(f.ident.clone()) }) + .collect(); + let update_columns = non_id_fields + .iter() + .enumerate() + .map(|(i, field)| format!("{} = ${}", field, i + 1)) + .collect::>() + .join(", "); + let mut all_fields = non_id_fields.clone(); + let where_clause = match id { + IdType::Simple { field_name, .. } => { + let where_clause = format!("{} = ${}", field_name, non_id_fields.len() + 1); + all_fields.push(field_name.clone()); + where_clause + } + IdType::Composite { fields, .. } => fields + .iter() + .enumerate() + .map(|(i, field)| { + let where_clause = format!("{} = ${}", field.name, non_id_fields.len() + i + 1); + all_fields.push(field.name.clone()); + where_clause + }) + .collect::>() + .join(" AND "), + }; + let update_string = + format!("UPDATE {table} SET {update_columns} WHERE {where_clause} RETURNING *"); + quote! { + async fn update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + ::sqlx::query_as!( + Self, #update_string, #(self.#all_fields),* + ) + .fetch_one(pool) + .await + } + } +} diff --git a/georm-macros/src/georm/traits/upsert.rs b/georm-macros/src/georm/traits/upsert.rs new file mode 100644 index 0000000..a44b288 --- /dev/null +++ b/georm-macros/src/georm/traits/upsert.rs @@ -0,0 +1,53 @@ +use crate::georm::{GeormField, IdType}; +use quote::quote; + +pub fn generate_upsert_query( + table: &str, + fields: &[GeormField], + id: &IdType, +) -> proc_macro2::TokenStream { + let inputs: Vec = (1..=fields.len()).map(|num| format!("${num}")).collect(); + let columns = fields + .iter() + .map(|f| f.ident.to_string()) + .collect::>() + .join(", "); + + let primary_key: proc_macro2::TokenStream = match id { + IdType::Simple { field_name, .. } => quote! {#field_name}, + IdType::Composite { fields, .. } => { + let field_names: Vec = fields.iter().map(|f| f.name.clone()).collect(); + quote! { + #(#field_names),* + } + } + }; + + // For ON CONFLICT DO UPDATE, exclude the ID field from updates + let update_assignments = fields + .iter() + .filter(|f| !f.id) + .map(|f| format!("{} = EXCLUDED.{}", f.ident, f.ident)) + .collect::>() + .join(", "); + + let upsert_string = format!( + "INSERT INTO {table} ({columns}) VALUES ({}) ON CONFLICT ({}) DO UPDATE SET {update_assignments} RETURNING *", + inputs.join(", "), + primary_key + ); + + let field_idents: Vec = fields.iter().map(|f| f.ident.clone()).collect(); + + quote! { + async fn create_or_update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + ::sqlx::query_as!( + Self, + #upsert_string, + #(self.#field_idents),* + ) + .fetch_one(pool) + .await + } + } +}