diff --git a/Cargo.lock b/Cargo.lock index a555e9a..b9351d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "ahash" version = "0.8.11" @@ -14,12 +24,32 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atoi" version = "2.0.0" @@ -282,7 +312,9 @@ dependencies = [ name = "ezpg" version = "0.0.0" dependencies = [ + "async-trait", "ezpg_macros", + "futures", "sqlx", "uuid", ] @@ -291,6 +323,7 @@ dependencies = [ name = "ezpg_macros" version = "0.0.0" dependencies = [ + "Inflector", "darling", "itertools", "proc-macro2", @@ -330,6 +363,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -374,6 +422,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -392,8 +451,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -957,6 +1018,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rsa" version = "0.9.7" diff --git a/crates/ezpg-macros/Cargo.toml b/crates/ezpg-macros/Cargo.toml index ec8d7cc..203b088 100644 --- a/crates/ezpg-macros/Cargo.toml +++ b/crates/ezpg-macros/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" proc-macro = true [dependencies] +Inflector = "0.11.4" darling = "0.20.10" itertools = "0.13.0" proc-macro2 = "1.0.92" diff --git a/crates/ezpg-macros/src/column.rs b/crates/ezpg-macros/src/column.rs index c8832c7..8ecef4e 100644 --- a/crates/ezpg-macros/src/column.rs +++ b/crates/ezpg-macros/src/column.rs @@ -23,8 +23,7 @@ impl Column { /// returns the column's name, either the `rename` attribute, or the /// column name itself. pub fn name(&self) -> String { - self - .rename + self.rename .as_ref() .cloned() .unwrap_or(self.ident().to_string()) diff --git a/crates/ezpg-macros/src/derive.rs b/crates/ezpg-macros/src/derive.rs index c84c535..280828a 100644 --- a/crates/ezpg-macros/src/derive.rs +++ b/crates/ezpg-macros/src/derive.rs @@ -1,8 +1,9 @@ +use crate::Column; use darling::{ast::Data, FromDeriveInput}; +use inflector::Inflector; use itertools::Itertools; use quote::quote; use syn::Ident; -use crate::Column; #[derive(FromDeriveInput)] #[darling(attributes(sqlx), supports(struct_named))] @@ -17,7 +18,7 @@ impl RecordDerive { self.rename .as_ref() .cloned() - .unwrap_or(self.ident.to_string().to_lowercase()) + .unwrap_or(self.ident.to_string().to_plural().to_snake_case()) } // returns all columns that are not marked `skip`. @@ -52,16 +53,17 @@ impl RecordDerive { // e.g., "select id, name, weight from items limit $1 offset $2" let index_query = - format!("select id, {column_names_join} from {table_name} limit $1 offset $2"); + format!("select {column_names_join} from {table_name} limit $1 offset $2"); // e.g., "select id, name, weight from items where id = $1" - let read_query = format!("select id, {column_names_join} from {table_name} where id = $1"); + let read_query = format!("select {column_names_join} from {table_name} where id = $1"); let update_query = { let value_pairs = self .columns() .enumerate() - .map(|(i, col)| format!("{} = ${}", col.name(), i + 2)) // enum starts at 0, id is 1, other columns start at 2. + .filter(|(_, c)| c.name() != "id") + .map(|(i, col)| format!("{} = ${}", col.name(), i + 1)) // enum starts at 0, id is 1, other columns start at 2. .join(","); format!("update {table_name} set {value_pairs} where id = $1") @@ -70,6 +72,12 @@ impl RecordDerive { let delete_query = format!("delete from {table_name} where id = $1"); quote! { + impl ::ezpg::Record for #ident { + fn id(&self) -> uuid::Uuid { + self.id + } + } + impl ::ezpg::WithQueries for #ident { const COLUMNS: &'static [&'static str] = &[#(#column_names,)*]; const CREATE_QUERY: &'static str = #create_query; diff --git a/crates/ezpg-macros/src/lib.rs b/crates/ezpg-macros/src/lib.rs index 0ac65f6..4067655 100644 --- a/crates/ezpg-macros/src/lib.rs +++ b/crates/ezpg-macros/src/lib.rs @@ -3,8 +3,8 @@ pub(crate) use column::Column; mod derive; use darling::FromDeriveInput; use proc_macro::TokenStream; -use syn::DeriveInput; use quote::quote; +use syn::DeriveInput; #[proc_macro_derive(Record, attributes(sqlx))] pub fn derive_record(input: TokenStream) -> TokenStream { @@ -17,6 +17,7 @@ pub fn derive_record(input: TokenStream) -> TokenStream { quote! { #queries_impl + #column_impl } .into() diff --git a/crates/ezpg/Cargo.toml b/crates/ezpg/Cargo.toml index 204446f..aaab4b6 100644 --- a/crates/ezpg/Cargo.toml +++ b/crates/ezpg/Cargo.toml @@ -4,6 +4,8 @@ version = "0.0.0" edition = "2021" [dependencies] +async-trait = "0.1.83" ezpg_macros = { version = "0.0.0", path = "../ezpg-macros" } +futures = "0.3.31" sqlx = { version = "0.8.2", features = ["macros", "postgres", "uuid"] } uuid = { version = "1.11.0", features = ["v7"] } diff --git a/crates/ezpg/src/crud_traits.rs b/crates/ezpg/src/crud_traits.rs index 8c49c6e..3060381 100644 --- a/crates/ezpg/src/crud_traits.rs +++ b/crates/ezpg/src/crud_traits.rs @@ -1,15 +1,17 @@ //! traits used for c/r/u/d behaviours. +use crate::Record; +use async_trait::async_trait; use sqlx::{PgPool, Postgres}; use uuid::Uuid; -use crate::Record; - /// [c]reate +#[async_trait] pub trait Create<'a, R> { async fn create(&self, pool: &PgPool) -> sqlx::Result; } +#[async_trait] impl<'a, R> Create<'a, R> for R where R: Record, @@ -35,11 +37,13 @@ where } /// [r]ead +#[async_trait] pub trait Read<'a, R>: Record { async fn index(offset: i64, limit: i64, pool: &PgPool) -> sqlx::Result>; - async fn read(id: Uuid, pool: &PgPool) -> sqlx::Result; + async fn find_by_id(id: Uuid, pool: &PgPool) -> sqlx::Result; } +#[async_trait] impl<'a, R> Read<'a, R> for R where R: Record, @@ -53,7 +57,7 @@ where .await } - async fn read(id: Uuid, pool: &PgPool) -> sqlx::Result { + async fn find_by_id(id: Uuid, pool: &PgPool) -> sqlx::Result { sqlx::query_as::(R::READ_QUERY) .bind(id) .fetch_one(pool) @@ -62,10 +66,12 @@ where } /// [c]reate +#[async_trait] pub trait Update<'a, R> { async fn update(&self, pool: &PgPool) -> sqlx::Result; } +#[async_trait] impl<'a, R> Update<'a, R> for R where R: Record, @@ -74,19 +80,24 @@ where async fn update(&self, pool: &PgPool) -> sqlx::Result { R::COLUMNS .iter() - .fold(sqlx::query_as(R::UPDATE_QUERY), |q, c| { - self.bind_column(q, c) - }) + .fold(sqlx::query(R::UPDATE_QUERY), |q, c| self.bind_column(q, c)) + .execute(pool) + .await?; + + sqlx::query_as::(R::READ_QUERY) + .bind(self.id()) .fetch_one(pool) .await } } /// [d]elete +#[async_trait] pub trait Delete<'a, R> { async fn delete(&self, pool: &PgPool) -> sqlx::Result<()>; } +#[async_trait] impl<'a, R> Delete<'a, R> for R where R: Record, diff --git a/crates/ezpg/src/lib.rs b/crates/ezpg/src/lib.rs index 53c2a6b..db83f8d 100644 --- a/crates/ezpg/src/lib.rs +++ b/crates/ezpg/src/lib.rs @@ -1,11 +1,15 @@ mod crud_traits; +pub use crud_traits::{Create, Delete, Read, Update}; -use sqlx::{postgres::{PgArguments, PgRow}, query::{Query, QueryAs}, Encode, FromRow, Postgres, Type}; +use sqlx::{ + postgres::{PgArguments, PgRow}, + query::{Query, QueryAs}, + Encode, FromRow, Postgres, Type, +}; use uuid::Uuid; // re-export the record macro. pub use ezpg_macros::Record; - // the following were stolen from [miniorm by meuter](https://github.com/meuter/miniorm-rs): pub trait QueryBind<'q> { fn bind(self, value: T) -> Self @@ -49,6 +53,8 @@ pub trait BindColumn { } /// a record stored within the postgres db. -pub trait Record: WithQueries + BindColumn + for<'r> FromRow<'r, PgRow> + Send + Unpin { +pub trait Record: + WithQueries + BindColumn + for<'r> FromRow<'r, PgRow> + Send + Sync + Unpin +{ fn id(&self) -> Uuid; }