From 17c81f4da10ffa2bb37d8f31fde531762d7a1a77 Mon Sep 17 00:00:00 2001 From: Martin Berg Alstad Date: Sat, 24 Aug 2024 19:29:54 +0200 Subject: [PATCH] Rewritten diesel_crud to be more flexible --- .env | 1 + Cargo.lock | 75 ++++++--- Cargo.toml | 3 +- crates/diesel_crud_derive/src/attributes.rs | 73 +++++++++ crates/diesel_crud_derive/src/common.rs | 16 +- crates/diesel_crud_derive/src/create.rs | 48 ++---- crates/diesel_crud_derive/src/delete.rs | 65 ++++---- crates/diesel_crud_derive/src/lib.rs | 164 +++++++++----------- crates/diesel_crud_derive/src/list.rs | 31 ++-- crates/diesel_crud_derive/src/read.rs | 46 +++--- crates/diesel_crud_derive/src/update.rs | 40 +++-- crates/diesel_crud_trait/src/lib.rs | 78 ++++++---- crates/tests/Cargo.toml | 4 + crates/tests/tests/diesel_crud_derive.rs | 88 ++++------- src/serde/response.rs | 1 + 15 files changed, 385 insertions(+), 348 deletions(-) create mode 100644 .env create mode 100644 crates/diesel_crud_derive/src/attributes.rs diff --git a/.env b/.env new file mode 100644 index 0000000..0d36dd0 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL="postgres://postgres:postgres@localhost:32768/postgres" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 76125e0..fc45cc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,7 +55,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -256,7 +256,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn", + "syn 2.0.75", ] [[package]] @@ -267,7 +267,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -320,7 +320,7 @@ dependencies = [ "deluxe-macros", "once_cell", "proc-macro2", - "syn", + "syn 2.0.75", ] [[package]] @@ -333,7 +333,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn", + "syn 2.0.75", ] [[package]] @@ -348,7 +348,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -368,7 +368,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -406,7 +406,7 @@ dependencies = [ "deluxe", "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -430,7 +430,7 @@ dependencies = [ "dsl_auto_type", "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -439,7 +439,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" dependencies = [ - "syn", + "syn 2.0.75", ] [[package]] @@ -453,6 +453,24 @@ dependencies = [ "subtle", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dotenvy_macro" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424" +dependencies = [ + "dotenvy", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "dsl_auto_type" version = "0.1.2" @@ -464,7 +482,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -533,7 +551,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -753,7 +771,7 @@ name = "into-response-derive" version = "1.1.0" dependencies = [ "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -1017,7 +1035,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -1143,7 +1161,7 @@ version = "0.1.0" dependencies = [ "quote", "regex", - "syn", + "syn 2.0.75", ] [[package]] @@ -1244,7 +1262,7 @@ checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -1373,6 +1391,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.75" @@ -1402,7 +1431,9 @@ version = "0.1.0" dependencies = [ "diesel", "diesel-async", + "dotenvy_macro", "lib", + "tokio", ] [[package]] @@ -1422,7 +1453,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -1474,7 +1505,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -1608,7 +1639,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -1731,7 +1762,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.75", "wasm-bindgen-shared", ] @@ -1753,7 +1784,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1916,5 +1947,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] diff --git a/Cargo.toml b/Cargo.toml index b3b45b8..4dcbc30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ serde = { version = "1.0", optional = true, features = ["derive"] } # Time chrono = { version = "0.4", optional = true, features = ["serde"] } # Utils -derive_more = { version = "1.0", features = ["from", "constructor"] } +derive_more = { workspace = true, features = ["from", "constructor"] } [workspace.dependencies] syn = "2.0" @@ -54,6 +54,7 @@ deluxe = "0.5" proc-macro2 = "1.0" diesel = "2.2" diesel-async = "0.5" +derive_more = "1.0" [features] axum = ["dep:axum", "dep:tower", "dep:tower-http", "dep:thiserror", "dep:tracing", "dep:tracing-subscriber", "dep:tokio"] diff --git a/crates/diesel_crud_derive/src/attributes.rs b/crates/diesel_crud_derive/src/attributes.rs new file mode 100644 index 0000000..43352d1 --- /dev/null +++ b/crates/diesel_crud_derive/src/attributes.rs @@ -0,0 +1,73 @@ +use crate::common::PrimaryKey; +use deluxe::{extract_attributes, ExtractAttributes}; +use proc_macro2::Ident; +use quote::quote; +use std::collections::HashMap; +use syn::spanned::Spanned; +use syn::{Data, DeriveInput, Expr, Path, Type}; + +#[derive(ExtractAttributes)] +#[deluxe(attributes(diesel))] +pub(crate) struct DieselStructAttributes { + table_name: Option, + #[deluxe(rest)] + _rest: HashMap, +} + +#[derive(ExtractAttributes)] +#[deluxe(attributes(diesel_crud))] +pub(crate) struct StructAttributes { + table: Option, + #[deluxe(default)] + insert: Option, + #[deluxe(default)] + update: Option, +} + +#[derive(ExtractAttributes)] +#[deluxe(attributes(diesel_crud))] +pub(crate) struct FieldAttributes(#[allow(unused)] Expr); + +pub(crate) struct Attributes { + pub struct_ident: Ident, + pub table: Expr, + pub insert: Type, + pub update: Type, + pub pk: Option, +} + +pub(crate) fn extract_attrs(ast: &mut DeriveInput) -> deluxe::Result { + let struct_attributes: StructAttributes = extract_attributes(ast)?; + let diesel_attributes: DieselStructAttributes = extract_attributes(ast)?; + Ok(Attributes { + struct_ident: ast.ident.clone(), + table: diesel_attributes.table_name.unwrap_or_else(|| { + struct_attributes + .table + .expect("Table name should be provided on either diesel or diesel_crud attribute") + }), + insert: struct_attributes + .insert + .unwrap_or_else(|| Type::Verbatim(quote! { Self })), + update: struct_attributes + .update + .unwrap_or_else(|| Type::Verbatim(quote! { Self })), + pk: extract_field_attrs(ast).ok(), + }) +} + +fn extract_field_attrs(ast: &mut DeriveInput) -> deluxe::Result { + if let Data::Struct(data_struct) = &mut ast.data { + for field in data_struct.fields.iter_mut() { + if let Ok(FieldAttributes(_)) = extract_attributes(field) { + return Ok(PrimaryKey { + ident: field.ident.clone().unwrap(), + ty: field.ty.clone(), + }); + } + } + } else { + return Err(deluxe::Error::new(ast.span(), "Expected a struct")); + }; + Err(deluxe::Error::new(ast.span(), "Primary key not found")) +} diff --git a/crates/diesel_crud_derive/src/common.rs b/crates/diesel_crud_derive/src/common.rs index 32906ef..c1f0e1e 100644 --- a/crates/diesel_crud_derive/src/common.rs +++ b/crates/diesel_crud_derive/src/common.rs @@ -1,16 +1,10 @@ +use proc_macro2::Ident; use quote::quote; +use syn::Type; -pub(crate) fn function_body(body: proc_macro2::TokenStream) -> proc_macro2::TokenStream { - quote! { - let connection = self.pool.get().await; - match connection { - Ok(mut connection) => { - use diesel::associations::HasTable; - #body - } - Err(error) => Err(lib::diesel_crud_trait::CrudError::PoolError(error.to_string())), - } - } +pub(crate) struct PrimaryKey { + pub ident: Ident, + pub ty: Type, } pub(crate) fn return_type(output: proc_macro2::TokenStream) -> proc_macro2::TokenStream { diff --git a/crates/diesel_crud_derive/src/create.rs b/crates/diesel_crud_derive/src/create.rs index 38359eb..3480c8e 100644 --- a/crates/diesel_crud_derive/src/create.rs +++ b/crates/diesel_crud_derive/src/create.rs @@ -1,49 +1,35 @@ -use crate::{common, StructAttributes}; -use proc_macro2::Ident; +use crate::{common, Attributes}; use quote::quote; -use syn::Expr; pub(crate) fn derive_diesel_crud_create_impl( - StructAttributes { + Attributes { + struct_ident, table, - entity, - create, + insert, .. - }: &StructAttributes, - identifier: &Ident, + }: &Attributes, ) -> proc_macro2::TokenStream { - let body = function_body(table); - let return_type = common::return_type(quote! { Self::Entity }); + let return_type = common::return_type(quote! { Self }); quote! { #[automatically_derived] - impl<'insertable, 'entity> lib::diesel_crud_trait::DieselCrudCreate<'insertable, 'entity, #table::table> for #identifier - where - 'entity: 'insertable, - { - type Create = #create; - type Entity = #entity; - fn create<'a, 'b>(&'a self, create: &'insertable Self::Create) -> #return_type + impl lib::diesel_crud_trait::DieselCrudCreate<#table::table> for #struct_ident { + type Insert = #insert; + fn create<'a, 'b>(insert: Self::Insert, conn: &'a mut diesel_async::AsyncPgConnection) -> #return_type where - Self: Sync + 'a, + Self: Sized + Sync + 'a, 'a: 'b, - 'insertable: 'b { Box::pin(async move { - #body + use diesel::associations::HasTable; + diesel_async::RunQueryDsl::get_result( + diesel::dsl::insert_into(#table::table::table()).values(insert), + conn + ) + .await + .map_err(Into::into) }) } } } } - -fn function_body(table: &Expr) -> proc_macro2::TokenStream { - common::function_body(quote! { - diesel_async::RunQueryDsl::get_result( - diesel::dsl::insert_into(#table::table::table()).values(create), - &mut connection - ) - .await - .map_err(Into::into) - }) -} diff --git a/crates/diesel_crud_derive/src/delete.rs b/crates/diesel_crud_derive/src/delete.rs index c586cdb..540446b 100644 --- a/crates/diesel_crud_derive/src/delete.rs +++ b/crates/diesel_crud_derive/src/delete.rs @@ -1,55 +1,46 @@ -use crate::{common, StructAttributes}; -use proc_macro2::Ident; -use quote::{quote, ToTokens}; -use syn::Expr; +use crate::{common, Attributes, PrimaryKey}; +use quote::quote; pub(crate) fn derive_diesel_crud_delete_impl( - StructAttributes { + Attributes { + struct_ident, table, pk, - pk_field, .. - }: &StructAttributes, - identifier: &Ident, + }: &Attributes, ) -> proc_macro2::TokenStream { - let body = function_body( - table, - pk_field - .clone() - .map(Expr::into_token_stream) - .unwrap_or_else(|| quote! { id }), - ); - let return_type = common::return_type(quote! { usize }); + if pk.is_none() { + panic!("Please specify a primary key using #[diesel_crud(pk)]"); + } + let PrimaryKey { + ident: pk_ident, + ty: pk_type, + } = pk.as_ref().unwrap(); + let return_type = common::return_type(quote! { Self }); quote! { #[automatically_derived] - impl lib::diesel_crud_trait::DieselCrudDelete for #identifier { - type PK = #pk; - fn delete<'a, 'pk, 'b>(&'a self, pk: &'pk Self::PK) -> #return_type + impl lib::diesel_crud_trait::DieselCrudDelete for #struct_ident { + type PK = #pk_type; + fn delete<'a, 'b>(pk: Self::PK, conn: &'a mut diesel_async::AsyncPgConnection) -> #return_type where - Self: Sync + 'a, + Self: Sized + Sync + 'a, 'a: 'b, - 'pk: 'b, { Box::pin(async move { - #body + use diesel::QueryDsl; + use diesel::associations::HasTable; + diesel_async::RunQueryDsl::get_result( + diesel::delete( + #table::table + .filter(diesel::expression_methods::ExpressionMethods::eq(#table::#pk_ident, pk)) + ), + conn, + ) + .await + .map_err(Into::into) }) } } } } - -fn function_body(table: &Expr, pk_field: proc_macro2::TokenStream) -> proc_macro2::TokenStream { - common::function_body(quote! { - use diesel::QueryDsl; - diesel_async::RunQueryDsl::execute( - diesel::delete( - #table::table - .filter(diesel::expression_methods::ExpressionMethods::eq(#table::#pk_field, pk)) - ), - &mut connection, - ) - .await - .map_err(Into::into) - }) -} diff --git a/crates/diesel_crud_derive/src/lib.rs b/crates/diesel_crud_derive/src/lib.rs index 450feb7..420c6f9 100644 --- a/crates/diesel_crud_derive/src/lib.rs +++ b/crates/diesel_crud_derive/src/lib.rs @@ -1,14 +1,16 @@ extern crate proc_macro; +use crate::attributes::{extract_attrs, Attributes}; +use crate::common::PrimaryKey; use crate::create::derive_diesel_crud_create_impl; use crate::delete::derive_diesel_crud_delete_impl; use crate::list::derive_diesel_crud_list_impl; use crate::read::derive_diesel_crud_read_impl; use crate::update::derive_diesel_crud_update_impl; -use deluxe::{extract_attributes, ExtractAttributes}; use quote::quote; -use syn::{parse_macro_input, DeriveInput, Expr, Type}; +use syn::{parse_macro_input, DeriveInput}; +mod attributes; mod common; mod create; mod delete; @@ -16,24 +18,6 @@ mod list; mod read; mod update; -#[derive(ExtractAttributes)] -#[deluxe(attributes(diesel_crud))] -pub(crate) struct StructAttributes { - table: Expr, - #[deluxe(default)] - entity: Option, - #[deluxe(default)] - pk: Option, - #[deluxe(default)] - pk_field: Option, - #[deluxe(default)] - create: Option, // TODO if None, use entity? - #[deluxe(default)] - update: Option, // TODO if None, use entity? -} - -// TODO get pool field automatically or by attribute - /// Derives 5 functions for CRUD operations /// 1. create /// 2. read @@ -43,16 +27,19 @@ pub(crate) struct StructAttributes { #[proc_macro_derive(DieselCrud, attributes(diesel_crud))] pub fn derive_diesel_crud(item: proc_macro::TokenStream) -> proc_macro::TokenStream { let mut item = parse_macro_input!(item as DeriveInput); - let struct_attributes = extract_attributes(&mut item).unwrap(); - let identifier = item.ident; + let attrs = extract_attrs(&mut item).unwrap(); - let create = derive_diesel_crud_create_impl(&struct_attributes, &identifier); - let read = derive_diesel_crud_read_impl(&struct_attributes, &identifier); - let update = derive_diesel_crud_update_impl(&struct_attributes, &identifier); - let delete = derive_diesel_crud_delete_impl(&struct_attributes, &identifier); - let list = derive_diesel_crud_list_impl(&struct_attributes, &identifier); + let create = derive_diesel_crud_create_impl(&attrs); + let read = derive_diesel_crud_read_impl(&attrs); + let update = derive_diesel_crud_update_impl(&attrs); + let delete = derive_diesel_crud_delete_impl(&attrs); + let list = derive_diesel_crud_list_impl(&attrs); - let table = struct_attributes.table; + let Attributes { + table, + struct_ident, + .. + } = attrs; let expanded = quote! { #create #read @@ -60,124 +47,119 @@ pub fn derive_diesel_crud(item: proc_macro::TokenStream) -> proc_macro::TokenStr #delete #list - impl<'insertable, 'entity> lib::diesel_crud_trait::DieselCrud<'insertable, 'entity, #table::table> for #identifier - where - 'entity: 'insertable - {} + impl lib::diesel_crud_trait::DieselCrud<#table::table> for #struct_ident {} }; expanded.into() } /// Derives the create function for CRUD operations. -/// Must be used on a struct with a field named `pool`, containing a `Pool`. +/// Must be used on a struct. /// # Struct Attributes -/// - table: Ident - The schema struct for the table -/// - result: Type - The resulting model -/// - create: Type - The insertable model +/// - table: Expr - The schema struct for the table (can be provided on either diesel or diesel_crud attribute) +/// - insert: Type - The insertable model (Optional, defaults to `Self`) /// # Example /// ```ignore -/// use diesel_async::{AsyncPgConnection, pooled_connection::deadpool::Pool}; -/// -/// #[derive(diesel_crud_derive::DieselCrudCreate)] -/// #[diesel_crud(table = crate::schema::user, result = crate::models::User, create = crate::models::InsertUser)] -/// struct TestServiceCreate { -/// pool: Pool, +/// #[derive(Queryable, diesel_crud_derive::DieselCrudCreate)] +/// #[diesel_crud(create = crate::models::InsertUser)] +/// #[diesel(table_name = crate::schema::user)] +/// struct User { +/// #[diesel_crud(pk)] +/// email: String, +/// password: String, /// } /// ``` #[proc_macro_derive(DieselCrudCreate, attributes(diesel_crud))] pub fn derive_diesel_crud_create(item: proc_macro::TokenStream) -> proc_macro::TokenStream { let mut item = syn::parse_macro_input!(item as DeriveInput); - let struct_attributes = extract_attributes(&mut item).unwrap(); - derive_diesel_crud_create_impl(&struct_attributes, &item.ident).into() + let attrs = extract_attrs(&mut item).unwrap(); + derive_diesel_crud_create_impl(&attrs).into() } /// Derives the read function for CRUD operations. -/// Must be used on a struct with a field named `pool`, containing a `Pool`. +/// Must be used on a struct with one field marked as the primary key. /// # Struct Attributes -/// - table: Ident - The schema struct for the table -/// - pk: Type - The primary key type -/// - result: Type - The resulting model -/// - pk_field (optional): Expr - The field to use as the primary key. Defaults to `id` +/// - table: Expr - The schema struct for the table (can be provided on either diesel or diesel_crud attribute) +/// # Field Attributes +/// - pk: Ident - The primary key field (Only one field should be marked as the primary key) /// # Example /// ```ignore -/// use diesel_async::{AsyncPgConnection, pooled_connection::deadpool::Pool}; -/// -/// #[derive(diesel_crud_derive::DieselCrudRead)] -/// #[diesel_crud(table = crate::schema::user, result = crate::models::User, pk = String)] -/// struct TestServiceRead { -/// pool: Pool, +/// #[derive(Queryable, diesel_crud_derive::DieselCrudRead)] +/// #[diesel(table_name = crate::schema::user)] +/// struct User { +/// #[diesel_crud(pk)] +/// email: String, +/// password: String, /// } /// ``` #[proc_macro_derive(DieselCrudRead, attributes(diesel_crud))] pub fn derive_diesel_crud_read(item: proc_macro::TokenStream) -> proc_macro::TokenStream { let mut item = syn::parse_macro_input!(item as DeriveInput); - let struct_attributes = extract_attributes(&mut item).unwrap(); - derive_diesel_crud_read_impl(&struct_attributes, &item.ident).into() + let attrs = extract_attrs(&mut item).unwrap(); + derive_diesel_crud_read_impl(&attrs).into() } /// Derives the update function for CRUD operations. -/// Must be used on a struct with a field named `pool`, containing a `Pool`. +/// Must be used on a struct. /// # Struct Attributes -/// - table: Ident - The schema struct for the table -/// - update: Type - The update model +/// - table: Expr - The schema struct for the table (can be provided on either diesel or diesel_crud attribute) +/// - update: Type - The update model (Optional, defaults to `Self`) /// # Example /// ```ignore -/// use diesel_async::{AsyncPgConnection, pooled_connection::deadpool::Pool}; -/// -/// #[derive(diesel_crud_derive::DieselCrudUpdate)] -/// #[diesel_crud(table = crate::schema::user, update = crate::models::User)] -/// struct TestServiceUpdate { -/// pool: Pool, +/// #[derive(Queryable, diesel_crud_derive::DieselCrudUpdate)] +/// #[diesel(table_name = crate::schema::user)] +/// struct User { +/// #[diesel_crud(pk)] +/// email: String, +/// password: String, /// } /// ``` #[proc_macro_derive(DieselCrudUpdate, attributes(diesel_crud))] pub fn derive_diesel_crud_update(item: proc_macro::TokenStream) -> proc_macro::TokenStream { let mut item = syn::parse_macro_input!(item as DeriveInput); - let struct_attributes = extract_attributes(&mut item).unwrap(); - derive_diesel_crud_update_impl(&struct_attributes, &item.ident).into() + let attrs = extract_attrs(&mut item).unwrap(); + derive_diesel_crud_update_impl(&attrs).into() } /// Derives the delete function for CRUD operations. -/// Must be used on a struct with a field named `pool`, containing a `Pool`. +/// Must be used on a struct with a field marked as primary key. /// # Struct Attributes -/// - table: Ident - The schema struct for the table -/// - pk: Type - The primary key type -/// - pk_field (optional): Expr - The field to use as the primary key. Defaults to `id` +/// - table: Expr - The schema struct for the table (can be provided on either diesel or diesel_crud attribute) +/// # Field Attributes +/// - pk: Ident - The primary key field (Only one field should be marked as the primary key) /// # Example /// ```ignore -/// use diesel_async::{AsyncPgConnection, pooled_connection::deadpool::Pool}; -/// -/// #[derive(diesel_crud_derive::DieselCrudDelete)] -/// #[diesel_crud(table = crate::schema::user, pk = String)] -/// struct TestServiceDelete { -/// pool: Pool, +/// #[derive(Queryable, diesel_crud_derive::DieselCrudDelete)] +/// #[diesel(table_name = crate::schema::user)] +/// struct User { +/// #[diesel_crud(pk)] +/// email: String, +/// password: String, /// } /// ``` #[proc_macro_derive(DieselCrudDelete, attributes(diesel_crud))] pub fn derive_diesel_crud_delete(item: proc_macro::TokenStream) -> proc_macro::TokenStream { let mut item = syn::parse_macro_input!(item as DeriveInput); - let struct_attributes = extract_attributes(&mut item).unwrap(); - derive_diesel_crud_delete_impl(&struct_attributes, &item.ident).into() + let attrs = extract_attrs(&mut item).unwrap(); + derive_diesel_crud_delete_impl(&attrs).into() } /// Derives the list function for CRUD operations. -/// Must be used on a struct with a field named `pool`, containing a `Pool`. +/// Must be used on a struct. /// # Struct Attributes -/// - table: Ident - The schema struct for the table -/// - result: Type - The resulting model +/// - table: Expr - The schema struct for the table (can be provided on either diesel or diesel_crud attribute) /// # Example /// ```ignore -/// use diesel_async::{AsyncPgConnection, pooled_connection::deadpool::Pool}; -/// -/// #[derive(diesel_crud_derive::DieselCrudList)] -/// #[diesel_crud(table = crate::schema::user, result = crate::models::User)] -/// struct TestServiceList { -/// pool: Pool, +/// #[derive(Queryable, diesel_crud_derive::DieselCrudList)] +/// #[diesel(table_name = crate::schema::user)] +/// struct User { +/// #[diesel_crud(pk)] +/// email: String, +/// password: String, /// } /// ``` #[proc_macro_derive(DieselCrudList, attributes(diesel_crud))] pub fn derive_diesel_crud_list(item: proc_macro::TokenStream) -> proc_macro::TokenStream { let mut item = syn::parse_macro_input!(item as DeriveInput); - let struct_attributes = extract_attributes(&mut item).unwrap(); - derive_diesel_crud_list_impl(&struct_attributes, &item.ident).into() + let attrs = extract_attrs(&mut item).unwrap(); + derive_diesel_crud_list_impl(&attrs).into() } diff --git a/crates/diesel_crud_derive/src/list.rs b/crates/diesel_crud_derive/src/list.rs index 88ab9e9..0871239 100644 --- a/crates/diesel_crud_derive/src/list.rs +++ b/crates/diesel_crud_derive/src/list.rs @@ -1,35 +1,28 @@ -use proc_macro2::Ident; +use crate::{common, Attributes}; use quote::quote; -use syn::Expr; - -use crate::{common, StructAttributes}; pub(crate) fn derive_diesel_crud_list_impl( - StructAttributes { table, entity, .. }: &StructAttributes, - identifier: &Ident, + Attributes { + struct_ident, + table, + .. + }: &Attributes, ) -> proc_macro2::TokenStream { - let body = function_body(table); - let return_type = common::return_type(quote! { Vec }); + let return_type = common::return_type(quote! { Vec }); quote! { #[automatically_derived] - impl lib::diesel_crud_trait::DieselCrudList for #identifier { - type Entity = #entity; - fn list<'a, 'b>(&'a self) -> #return_type + impl lib::diesel_crud_trait::DieselCrudList for #struct_ident { + fn list<'a, 'b>(conn: &'a mut diesel_async::AsyncPgConnection) -> #return_type where - Self: Sync + 'a, + Self: Sized + Sync + 'a, 'a: 'b { Box::pin(async move { - #body + use diesel::associations::HasTable; + diesel_async::RunQueryDsl::get_results(#table::table::table(), conn).await.map_err(Into::into) }) } } } } - -fn function_body(table: &Expr) -> proc_macro2::TokenStream { - common::function_body(quote! { - diesel_async::RunQueryDsl::get_results(#table::table::table(), &mut connection).await.map_err(Into::into) - }) -} diff --git a/crates/diesel_crud_derive/src/read.rs b/crates/diesel_crud_derive/src/read.rs index 067c75c..1fe6c71 100644 --- a/crates/diesel_crud_derive/src/read.rs +++ b/crates/diesel_crud_derive/src/read.rs @@ -1,38 +1,40 @@ -use proc_macro2::Ident; +use crate::common::PrimaryKey; +use crate::{common, Attributes}; use quote::quote; -use syn::Expr; - -use crate::{common, StructAttributes}; pub(crate) fn derive_diesel_crud_read_impl( - StructAttributes { - table, entity, pk, .. - }: &StructAttributes, - identifier: &Ident, + Attributes { + struct_ident, + table, + pk, + .. + }: &Attributes, ) -> proc_macro2::TokenStream { - let body = function_body(table); - let return_type = common::return_type(quote! { Self::Entity }); + if pk.is_none() { + panic!("Please specify a primary key using #[diesel_crud(pk)]"); + } + let PrimaryKey { ty: pk_type, .. } = pk.as_ref().unwrap(); + let return_type = common::return_type(quote! { Self }); quote! { #[automatically_derived] - impl lib::diesel_crud_trait::DieselCrudRead for #identifier { - type PK = #pk; - type Entity = #entity; - fn read<'a, 'b>(&'a self, pk: Self::PK) -> #return_type + impl lib::diesel_crud_trait::DieselCrudRead for #struct_ident { + type PK = #pk_type; + fn read<'a, 'b>(pk: Self::PK, conn: &'a mut diesel_async::AsyncPgConnection) -> #return_type where - Self: Sync + 'a, + Self: Sized + Sync + 'a, 'a: 'b { Box::pin(async move { - #body + use diesel::associations::HasTable; + diesel_async::RunQueryDsl::get_result( + diesel::QueryDsl::find(#table::table::table(), pk), + conn + ) + .await + .map_err(Into::into) }) } } } } - -fn function_body(table: &Expr) -> proc_macro2::TokenStream { - common::function_body(quote! { - diesel_async::RunQueryDsl::get_result(diesel::QueryDsl::find(#table::table::table(), pk), &mut connection).await.map_err(Into::into) - }) -} diff --git a/crates/diesel_crud_derive/src/update.rs b/crates/diesel_crud_derive/src/update.rs index a959bd7..790a2ed 100644 --- a/crates/diesel_crud_derive/src/update.rs +++ b/crates/diesel_crud_derive/src/update.rs @@ -1,39 +1,35 @@ -use crate::{common, StructAttributes}; -use proc_macro2::Ident; +use crate::{common, Attributes}; use quote::quote; -use syn::Expr; pub(crate) fn derive_diesel_crud_update_impl( - StructAttributes { table, update, .. }: &StructAttributes, - identifier: &Ident, + Attributes { + struct_ident, + table, + update, + .. + }: &Attributes, ) -> proc_macro2::TokenStream { - let body = function_body(table); - let return_type = common::return_type(quote! { usize }); + let return_type = common::return_type(quote! { Self }); quote! { #[automatically_derived] - impl lib::diesel_crud_trait::DieselCrudUpdate for #identifier { + impl lib::diesel_crud_trait::DieselCrudUpdate for #struct_ident { type Update = #update; - fn update<'a, 'b>(&'a self, update: Self::Update) -> #return_type + fn update<'a, 'b>(update: Self::Update, conn: &'a mut diesel_async::AsyncPgConnection) -> #return_type where - Self: Sync + 'a, + Self: Sized + Sync + 'a, 'a: 'b, { Box::pin(async move { - #body + use diesel::associations::HasTable; + diesel_async::RunQueryDsl::get_result( + diesel::dsl::update(#table::table::table()).set(update), + conn, + ) + .await + .map_err(Into::into) }) } } } } - -fn function_body(table: &Expr) -> proc_macro2::TokenStream { - common::function_body(quote! { - diesel_async::RunQueryDsl::execute( - diesel::dsl::update(#table::table::table()).set(update), - &mut connection, - ) - .await - .map_err(Into::into) - }) -} diff --git a/crates/diesel_crud_trait/src/lib.rs b/crates/diesel_crud_trait/src/lib.rs index 028ee84..3c5bf0a 100644 --- a/crates/diesel_crud_trait/src/lib.rs +++ b/crates/diesel_crud_trait/src/lib.rs @@ -1,18 +1,19 @@ mod error; -pub use error::CrudError; - use async_trait::async_trait; use diesel::{AsChangeset, Insertable}; +use diesel_async::AsyncPgConnection; +pub use error::CrudError; -pub trait DieselCrud<'insertable, 'entity, Table>: - DieselCrudCreate<'insertable, 'entity, Table> - + DieselCrudRead - + DieselCrudUpdate - + DieselCrudDelete - + DieselCrudList -where - 'entity: 'insertable, +/// Combines all CRUD operations into a single trait +/// Includes: +/// - Create +/// - Read +/// - Update +/// - Delete +/// - List +pub trait DieselCrud: + DieselCrudCreate
+ DieselCrudRead + DieselCrudUpdate + DieselCrudDelete + DieselCrudList { } @@ -21,20 +22,19 @@ where /// /// Implementing the trait requires the `async_trait` macro. /// # Associations -/// - `Create` - The type to insert -/// - `Entity` - The type that will be returned +/// - `Insert` - The type to insert, must implement `Insertable
` /// # Parameters -/// - `create` - The entity to insert +/// - `insert` - The entity to insert +/// - `conn` - The database connection /// # Returns /// A result containing the inserted entity or a `CrudError` #[async_trait] -pub trait DieselCrudCreate<'insertable, 'entity, Table> +pub trait DieselCrudCreate
where - 'entity: 'insertable, + Self: Sized, { - type Create: Insertable
; - type Entity: 'entity; - async fn create(&self, create: &'insertable Self::Create) -> Result; + type Insert: Insertable
; + async fn create(insert: Self::Insert, conn: &mut AsyncPgConnection) -> Result; } /// Gets an entity from the database @@ -42,17 +42,19 @@ where /// Implementing the trait requires the `async_trait` macro. /// # Associations /// - `PK` - The primary key of the entity -/// - `Entity` - The type that will be returned /// # Parameters /// - `pk` - The primary key of the entity +/// - `conn` - The database connection /// # Returns /// A result containing the entity or a `CrudError`. /// If the entity is not found, the error should be `CrudError::NotFound`. #[async_trait] -pub trait DieselCrudRead { +pub trait DieselCrudRead +where + Self: Sized, +{ type PK; - type Entity; - async fn read(&self, pk: Self::PK) -> Result; + async fn read(pk: Self::PK, conn: &mut AsyncPgConnection) -> Result; } /// Updates an entity in the database @@ -63,13 +65,17 @@ pub trait DieselCrudRead { /// - `Update` - The type to update /// # Parameters /// - `update` - The update to apply +/// - `conn` - The database connection /// # Returns -/// A result containing the number of rows updated or a `CrudError`. +/// A result containing the old entry of the entity if successful or a `CrudError`. /// If the entity is not found, the error should be `CrudError::NotFound`. #[async_trait] -pub trait DieselCrudUpdate { +pub trait DieselCrudUpdate +where + Self: Sized, +{ type Update: AsChangeset; - async fn update(&self, update: Self::Update) -> Result; + async fn update(update: Self::Update, conn: &mut AsyncPgConnection) -> Result; } /// Deletes an entity from the database @@ -79,24 +85,30 @@ pub trait DieselCrudUpdate { /// - `PK` - The primary key of the entity /// # Parameters /// - `pk` - The primary key of the entity +/// - `conn` - The database connection /// # Returns -/// A result containing the number of rows deleted or a `CrudError`. +/// A result containing the deleted entity or a `CrudError`. /// If the entity is not found, the error should be `CrudError::NotFound`. #[async_trait] -pub trait DieselCrudDelete { +pub trait DieselCrudDelete +where + Self: Sized, +{ type PK; - async fn delete(&self, pk: &Self::PK) -> Result; + async fn delete(pk: Self::PK, conn: &mut AsyncPgConnection) -> Result; } /// Lists all entities in the table /// /// Implementing the trait requires the `async_trait` macro. -/// # Associations -/// - `Entity` - The type that will be returned in a Vec +/// # Parameters +/// - `conn` - The database connection /// # Returns /// A result containing a Vec of entities or a `CrudError`. #[async_trait] -pub trait DieselCrudList { - type Entity; - async fn list(&self) -> Result, CrudError>; +pub trait DieselCrudList +where + Self: Sized, +{ + async fn list(conn: &mut AsyncPgConnection) -> Result, CrudError>; } diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index 56ad739..b97968d 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -10,3 +10,7 @@ homepage.workspace = true diesel = { workspace = true } diesel-async = { workspace = true } lib = { path = "../../../lib", features = ["diesel", "derive"] } + +[dev-dependencies] +tokio = { version = "1.39", features = ["macros"] } +dotenvy_macro = "0.15" diff --git a/crates/tests/tests/diesel_crud_derive.rs b/crates/tests/tests/diesel_crud_derive.rs index 5224f02..bd15aa2 100644 --- a/crates/tests/tests/diesel_crud_derive.rs +++ b/crates/tests/tests/diesel_crud_derive.rs @@ -1,11 +1,8 @@ -#![allow(unused)] use diesel::{AsChangeset, Insertable, Queryable, Selectable}; -use diesel_async::pooled_connection::deadpool::Pool; -use diesel_async::pooled_connection::AsyncDieselConnectionManager; -use diesel_async::AsyncPgConnection; +use diesel_async::{AsyncConnection, AsyncPgConnection}; +use dotenvy_macro::dotenv; use lib::diesel_crud_derive::{ - DieselCrud, DieselCrudCreate, DieselCrudDelete, DieselCrudList, DieselCrudRead, - DieselCrudUpdate, + DieselCrudCreate, DieselCrudDelete, DieselCrudList, DieselCrudRead, DieselCrudUpdate, }; use lib::diesel_crud_trait::DieselCrudCreate; @@ -16,67 +13,40 @@ diesel::table! { } } -#[derive(Queryable, Selectable, Insertable, AsChangeset)] +#[derive( + Queryable, + Selectable, + Insertable, + AsChangeset, + DieselCrudCreate, + DieselCrudDelete, + DieselCrudList, + DieselCrudRead, + DieselCrudUpdate, +)] +#[diesel_crud(insert = InsertUser)] #[diesel(table_name = user)] struct User { + #[diesel_crud(pk)] email: String, } -#[derive(Insertable)] +#[derive(Clone, Insertable)] #[diesel(table_name = user)] struct InsertUser { email: String, } -#[derive(DieselCrud)] -#[diesel_crud(table = user, entity = User, pk = String, pk_field = email, create = InsertUser, update = User)] -struct TestService { - pool: Pool, -} - -#[derive(DieselCrudCreate, DieselCrudRead, DieselCrudUpdate, DieselCrudDelete, DieselCrudList)] -#[diesel_crud(table = user, entity = User, pk = String, pk_field = email, create = InsertUser, update = User)] -struct TestServiceSeparate { - pool: Pool, -} - -#[derive(DieselCrudCreate)] -#[diesel_crud(table = user, entity = User, create = InsertUser)] -struct TestServiceCreate { - pool: Pool, -} - -#[derive(DieselCrudRead)] -#[diesel_crud(table = user, entity = User, pk = String)] -struct TestServiceRead { - pool: Pool, -} - -#[derive(DieselCrudUpdate)] -#[diesel_crud(table = user, update = User)] -struct TestServiceUpdate { - pool: Pool, -} - -#[derive(DieselCrudDelete)] -#[diesel_crud(table = user, pk = String, pk_field = email)] -struct TestServiceDelete { - pool: Pool, -} - -#[derive(DieselCrudList)] -#[diesel_crud(table = user, entity = User)] -struct TestServiceList { - pool: Pool, -} - -#[test] -fn test_insert_user() { - let config = AsyncDieselConnectionManager::::new(""); - let pool = Pool::builder(config).max_size(10).build().unwrap(); - - let service = TestServiceCreate { pool }; - service.create(&InsertUser { - email: "test".to_string(), - }); +#[tokio::test] +async fn test_insert_user() { + let database_url = dotenv!("DATABASE_URL"); + let mut conn = AsyncPgConnection::establish(database_url).await.unwrap(); + conn.begin_test_transaction().await.unwrap(); + let _user = User::create( + InsertUser { + email: "test".to_string(), + }, + &mut conn, + ) + .await; } diff --git a/src/serde/response.rs b/src/serde/response.rs index 042c4c4..d14464e 100644 --- a/src/serde/response.rs +++ b/src/serde/response.rs @@ -16,6 +16,7 @@ impl BaseResponse { } } +// TODO version should reference the version in caller's Cargo.toml #[macro_export] macro_rules! from { ($body:expr) => {