This commit is contained in:
elliot speck 2024-12-29 14:08:28 +11:00
parent d3a5a8d74c
commit a16ad316a3
No known key found for this signature in database
8 changed files with 136 additions and 18 deletions

90
Cargo.lock generated
View file

@ -2,6 +2,16 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.11" version = "0.8.11"
@ -14,12 +24,32 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.21" version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 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]] [[package]]
name = "atoi" name = "atoi"
version = "2.0.0" version = "2.0.0"
@ -282,7 +312,9 @@ dependencies = [
name = "ezpg" name = "ezpg"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"async-trait",
"ezpg_macros", "ezpg_macros",
"futures",
"sqlx", "sqlx",
"uuid", "uuid",
] ]
@ -291,6 +323,7 @@ dependencies = [
name = "ezpg_macros" name = "ezpg_macros"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"Inflector",
"darling", "darling",
"itertools", "itertools",
"proc-macro2", "proc-macro2",
@ -330,6 +363,21 @@ dependencies = [
"percent-encoding", "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]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.31"
@ -374,6 +422,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 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]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.31" version = "0.3.31"
@ -392,8 +451,10 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr", "memchr",
@ -957,6 +1018,35 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.7" version = "0.9.7"

View file

@ -7,6 +7,7 @@ edition = "2021"
proc-macro = true proc-macro = true
[dependencies] [dependencies]
Inflector = "0.11.4"
darling = "0.20.10" darling = "0.20.10"
itertools = "0.13.0" itertools = "0.13.0"
proc-macro2 = "1.0.92" proc-macro2 = "1.0.92"

View file

@ -23,8 +23,7 @@ impl Column {
/// returns the column's name, either the `rename` attribute, or the /// returns the column's name, either the `rename` attribute, or the
/// column name itself. /// column name itself.
pub fn name(&self) -> String { pub fn name(&self) -> String {
self self.rename
.rename
.as_ref() .as_ref()
.cloned() .cloned()
.unwrap_or(self.ident().to_string()) .unwrap_or(self.ident().to_string())

View file

@ -1,8 +1,9 @@
use crate::Column;
use darling::{ast::Data, FromDeriveInput}; use darling::{ast::Data, FromDeriveInput};
use inflector::Inflector;
use itertools::Itertools; use itertools::Itertools;
use quote::quote; use quote::quote;
use syn::Ident; use syn::Ident;
use crate::Column;
#[derive(FromDeriveInput)] #[derive(FromDeriveInput)]
#[darling(attributes(sqlx), supports(struct_named))] #[darling(attributes(sqlx), supports(struct_named))]
@ -17,7 +18,7 @@ impl RecordDerive {
self.rename self.rename
.as_ref() .as_ref()
.cloned() .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`. // 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" // e.g., "select id, name, weight from items limit $1 offset $2"
let index_query = 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" // 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 update_query = {
let value_pairs = self let value_pairs = self
.columns() .columns()
.enumerate() .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(","); .join(",");
format!("update {table_name} set {value_pairs} where id = $1") 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"); let delete_query = format!("delete from {table_name} where id = $1");
quote! { quote! {
impl ::ezpg::Record for #ident {
fn id(&self) -> uuid::Uuid {
self.id
}
}
impl ::ezpg::WithQueries for #ident { impl ::ezpg::WithQueries for #ident {
const COLUMNS: &'static [&'static str] = &[#(#column_names,)*]; const COLUMNS: &'static [&'static str] = &[#(#column_names,)*];
const CREATE_QUERY: &'static str = #create_query; const CREATE_QUERY: &'static str = #create_query;

View file

@ -3,8 +3,8 @@ pub(crate) use column::Column;
mod derive; mod derive;
use darling::FromDeriveInput; use darling::FromDeriveInput;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use syn::DeriveInput;
use quote::quote; use quote::quote;
use syn::DeriveInput;
#[proc_macro_derive(Record, attributes(sqlx))] #[proc_macro_derive(Record, attributes(sqlx))]
pub fn derive_record(input: TokenStream) -> TokenStream { pub fn derive_record(input: TokenStream) -> TokenStream {
@ -17,6 +17,7 @@ pub fn derive_record(input: TokenStream) -> TokenStream {
quote! { quote! {
#queries_impl #queries_impl
#column_impl #column_impl
} }
.into() .into()

View file

@ -4,6 +4,8 @@ version = "0.0.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
async-trait = "0.1.83"
ezpg_macros = { version = "0.0.0", path = "../ezpg-macros" } ezpg_macros = { version = "0.0.0", path = "../ezpg-macros" }
futures = "0.3.31"
sqlx = { version = "0.8.2", features = ["macros", "postgres", "uuid"] } sqlx = { version = "0.8.2", features = ["macros", "postgres", "uuid"] }
uuid = { version = "1.11.0", features = ["v7"] } uuid = { version = "1.11.0", features = ["v7"] }

View file

@ -1,15 +1,17 @@
//! traits used for c/r/u/d behaviours. //! traits used for c/r/u/d behaviours.
use crate::Record;
use async_trait::async_trait;
use sqlx::{PgPool, Postgres}; use sqlx::{PgPool, Postgres};
use uuid::Uuid; use uuid::Uuid;
use crate::Record;
/// [c]reate /// [c]reate
#[async_trait]
pub trait Create<'a, R> { pub trait Create<'a, R> {
async fn create(&self, pool: &PgPool) -> sqlx::Result<R>; async fn create(&self, pool: &PgPool) -> sqlx::Result<R>;
} }
#[async_trait]
impl<'a, R> Create<'a, R> for R impl<'a, R> Create<'a, R> for R
where where
R: Record, R: Record,
@ -35,11 +37,13 @@ where
} }
/// [r]ead /// [r]ead
#[async_trait]
pub trait Read<'a, R>: Record { pub trait Read<'a, R>: Record {
async fn index(offset: i64, limit: i64, pool: &PgPool) -> sqlx::Result<Vec<R>>; async fn index(offset: i64, limit: i64, pool: &PgPool) -> sqlx::Result<Vec<R>>;
async fn read(id: Uuid, pool: &PgPool) -> sqlx::Result<R>; async fn find_by_id(id: Uuid, pool: &PgPool) -> sqlx::Result<R>;
} }
#[async_trait]
impl<'a, R> Read<'a, R> for R impl<'a, R> Read<'a, R> for R
where where
R: Record, R: Record,
@ -53,7 +57,7 @@ where
.await .await
} }
async fn read(id: Uuid, pool: &PgPool) -> sqlx::Result<R> { async fn find_by_id(id: Uuid, pool: &PgPool) -> sqlx::Result<R> {
sqlx::query_as::<Postgres, R>(R::READ_QUERY) sqlx::query_as::<Postgres, R>(R::READ_QUERY)
.bind(id) .bind(id)
.fetch_one(pool) .fetch_one(pool)
@ -62,10 +66,12 @@ where
} }
/// [c]reate /// [c]reate
#[async_trait]
pub trait Update<'a, R> { pub trait Update<'a, R> {
async fn update(&self, pool: &PgPool) -> sqlx::Result<R>; async fn update(&self, pool: &PgPool) -> sqlx::Result<R>;
} }
#[async_trait]
impl<'a, R> Update<'a, R> for R impl<'a, R> Update<'a, R> for R
where where
R: Record, R: Record,
@ -74,19 +80,24 @@ where
async fn update(&self, pool: &PgPool) -> sqlx::Result<R> { async fn update(&self, pool: &PgPool) -> sqlx::Result<R> {
R::COLUMNS R::COLUMNS
.iter() .iter()
.fold(sqlx::query_as(R::UPDATE_QUERY), |q, c| { .fold(sqlx::query(R::UPDATE_QUERY), |q, c| self.bind_column(q, c))
self.bind_column(q, c) .execute(pool)
}) .await?;
sqlx::query_as::<Postgres, R>(R::READ_QUERY)
.bind(self.id())
.fetch_one(pool) .fetch_one(pool)
.await .await
} }
} }
/// [d]elete /// [d]elete
#[async_trait]
pub trait Delete<'a, R> { pub trait Delete<'a, R> {
async fn delete(&self, pool: &PgPool) -> sqlx::Result<()>; async fn delete(&self, pool: &PgPool) -> sqlx::Result<()>;
} }
#[async_trait]
impl<'a, R> Delete<'a, R> for R impl<'a, R> Delete<'a, R> for R
where where
R: Record, R: Record,

View file

@ -1,11 +1,15 @@
mod crud_traits; 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; use uuid::Uuid;
// re-export the record macro. // re-export the record macro.
pub use ezpg_macros::Record; pub use ezpg_macros::Record;
// the following were stolen from [miniorm by meuter](https://github.com/meuter/miniorm-rs): // the following were stolen from [miniorm by meuter](https://github.com/meuter/miniorm-rs):
pub trait QueryBind<'q> { pub trait QueryBind<'q> {
fn bind<T>(self, value: T) -> Self fn bind<T>(self, value: T) -> Self
@ -49,6 +53,8 @@ pub trait BindColumn {
} }
/// a record stored within the postgres db. /// 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; fn id(&self) -> Uuid;
} }