From 5fcc018f8aeab79909575e7d5a3770e0a680bc17 Mon Sep 17 00:00:00 2001 From: James Holman Date: Thu, 7 Nov 2024 23:22:44 +1100 Subject: [PATCH] fix: syntax for linting errors --- Cargo.lock | 18 +- Cargo.toml | 1 + src/lib.rs | 9 +- .../delete_query_builder.rs | 32 ++- .../test_delete_query_builder.rs | 24 +- .../insert_query_builder.rs | 17 +- .../test_insert_query_builder.rs | 28 +-- .../one_query_builder/one_query_builder.rs | 31 ++- .../test_one_query_builder.rs | 24 +- ...9b1498b8b9de32b6eb6a8b0ba78083fbbf9db.json | 38 ++++ tests/Cargo.lock | 16 ++ tests/src/harness.rs | 59 +++++ tests/src/lib.rs | 205 +----------------- ...er_with_one_key_one_unique_one_optional.rs | 171 +++++++++++++++ 14 files changed, 417 insertions(+), 256 deletions(-) create mode 100644 tests/.sqlx/query-e01ff749bb4aba91d5b052b00b89b1498b8b9de32b6eb6a8b0ba78083fbbf9db.json create mode 100644 tests/src/harness.rs create mode 100644 tests/src/test_user_with_one_key_one_unique_one_optional.rs diff --git a/Cargo.lock b/Cargo.lock index 0b52e05..48626f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,10 +2,20 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "db-set-macros" -version = "0.1.0" +version = "0.1.1" dependencies = [ + "convert_case", "pretty_assertions", "prettyplease", "proc-macro2", @@ -74,6 +84,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index a028231..098bc76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ license = "MIT OR Apache-2.0" readme = "README.md" [dependencies] +convert_case = "0.6.0" pretty_assertions = "1.4.1" prettyplease = "0.2.25" proc-macro2 = "1.0.89" diff --git a/src/lib.rs b/src/lib.rs index 8de22d1..01fca27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +use convert_case::{Case, Casing}; use modules::{delete_query_builder, update_query_builder}; use modules::{insert_query_builder, one_query_builder}; use proc_macro::TokenStream; @@ -29,7 +30,13 @@ pub fn dbset_derive(input: TokenStream) -> TokenStream { let from_row_impl = from_row::get_from_row_impl(&input); let dbset_impl = dbset::get_dbset_impl(&input); - let module_name = quote::format_ident!("{}_module", dbset_name); + let module_name = quote::format_ident!( + "{}_module", + dbset_name + .to_string() + .from_case(Case::Pascal) + .to_case(Case::Snake) + ); let expanded = quote! { diff --git a/src/modules/delete_query_builder/delete_query_builder.rs b/src/modules/delete_query_builder/delete_query_builder.rs index 9dec4a2..e1b666f 100644 --- a/src/modules/delete_query_builder/delete_query_builder.rs +++ b/src/modules/delete_query_builder/delete_query_builder.rs @@ -1,3 +1,4 @@ +use convert_case::{Case, Casing}; use proc_macro2::Ident; use quote::quote; use syn::DeriveInput; @@ -30,8 +31,10 @@ pub fn get_query_builder(input: &DeriveInput) -> proc_macro2::TokenStream { let builder_struct_generics = all_required_insert_fields .clone() .map(|(field_name, _)| { + + let gen_name_pascal = quote::format_ident!("{}", field_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); quote! { - #field_name = NotSet, + #gen_name_pascal = NotSet, } }) .chain(vec![quote! { @@ -55,8 +58,9 @@ pub fn get_query_builder(input: &DeriveInput) -> proc_macro2::TokenStream { }]); let phantom_struct_fields = all_required_insert_fields.clone().map(|(name, _)| { + let gen_name_pascal = quote::format_ident!("{}", name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); let ph_name = quote::format_ident!("_{}", name); - quote! { #ph_name: std::marker::PhantomData::<#name>, } + quote! { #ph_name: std::marker::PhantomData::<#gen_name_pascal>, } }); // Create Builder Struct @@ -140,21 +144,39 @@ pub fn get_query_builder(input: &DeriveInput) -> proc_macro2::TokenStream { let pre_impl_generics_in = all_required_insert_fields.clone().map(|(gen_name, _)|{ if gen_name != field_name { - return quote!{ #gen_name, } + if !is_unique_field { + let gen_name_pascal = quote::format_ident!("{}", gen_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); + return quote!{ #gen_name_pascal, } + } else { + return quote!{ } + + } } quote!{ } + }); let generics_in = all_required_insert_fields.clone().map(|(gen_name, _)|{ if gen_name != field_name { - return quote!{ #gen_name, } + if !is_unique_field { + let gen_name_pascal = quote::format_ident!("{}", gen_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); + return quote!{ #gen_name_pascal, } + } else { + return quote!{ NotSet, } + } } + quote!{ NotSet, } }).chain(vec![quote!{NotSet}]); let generics_out = all_required_insert_fields.clone().map(|(gen_name, _)|{ if gen_name != field_name { - return quote!{ #gen_name, } + if !is_unique_field { + let gen_name_pascal = quote::format_ident!("{}", gen_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); + return quote!{ #gen_name_pascal, } + } else { + return quote!{ NotSet, } + } } quote!{ Set, } }).chain( if is_unique_field { diff --git a/src/modules/delete_query_builder/test_delete_query_builder.rs b/src/modules/delete_query_builder/test_delete_query_builder.rs index 4da243e..d94137a 100644 --- a/src/modules/delete_query_builder/test_delete_query_builder.rs +++ b/src/modules/delete_query_builder/test_delete_query_builder.rs @@ -30,11 +30,11 @@ pub struct User { "#; let output = r#" -pub struct UserDbSetDeleteQueryBuilder { +pub struct UserDbSetDeleteQueryBuilder { id: Option, email: Option, _unique_fields: std::marker::PhantomData, - _id: std::marker::PhantomData, + _id: std::marker::PhantomData, } impl UserDbSetDeleteQueryBuilder { pub fn new() -> UserDbSetDeleteQueryBuilder { @@ -56,8 +56,8 @@ impl UserDbSetDeleteQueryBuilder { } } } -impl UserDbSetDeleteQueryBuilder { - pub fn email_eq(self, email: String) -> UserDbSetDeleteQueryBuilder { +impl UserDbSetDeleteQueryBuilder { + pub fn email_eq(self, email: String) -> UserDbSetDeleteQueryBuilder { UserDbSetDeleteQueryBuilder { email: Some(email), id: self.id, @@ -183,15 +183,15 @@ pub struct FavouritedProduct { let output = r#" pub struct FavouritedProductDbSetDeleteQueryBuilder< - product_id = NotSet, - user_id = NotSet, + ProductId = NotSet, + UserId = NotSet, UniqueFields = NotSet, > { product_id: Option, user_id: Option, _unique_fields: std::marker::PhantomData, - _product_id: std::marker::PhantomData, - _user_id: std::marker::PhantomData, + _product_id: std::marker::PhantomData, + _user_id: std::marker::PhantomData, } impl FavouritedProductDbSetDeleteQueryBuilder { @@ -205,11 +205,11 @@ impl FavouritedProductDbSetDeleteQueryBuilder { } } } -impl FavouritedProductDbSetDeleteQueryBuilder { +impl FavouritedProductDbSetDeleteQueryBuilder { pub fn product_id_eq( self, product_id: uuid::Uuid, - ) -> FavouritedProductDbSetDeleteQueryBuilder { + ) -> FavouritedProductDbSetDeleteQueryBuilder { FavouritedProductDbSetDeleteQueryBuilder { product_id: Some(product_id), user_id: self.user_id, @@ -219,11 +219,11 @@ impl FavouritedProductDbSetDeleteQueryBuilder } } } -impl FavouritedProductDbSetDeleteQueryBuilder { +impl FavouritedProductDbSetDeleteQueryBuilder { pub fn user_id_eq( self, user_id: uuid::Uuid, - ) -> FavouritedProductDbSetDeleteQueryBuilder { + ) -> FavouritedProductDbSetDeleteQueryBuilder { FavouritedProductDbSetDeleteQueryBuilder { user_id: Some(user_id), product_id: self.product_id, diff --git a/src/modules/insert_query_builder/insert_query_builder.rs b/src/modules/insert_query_builder/insert_query_builder.rs index 1e6e5ad..3128bfc 100644 --- a/src/modules/insert_query_builder/insert_query_builder.rs +++ b/src/modules/insert_query_builder/insert_query_builder.rs @@ -1,3 +1,4 @@ +use convert_case::{Casing,Case}; use proc_macro2::Ident; use quote::quote; use syn::{DeriveInput, Type}; @@ -34,8 +35,9 @@ pub fn get_insert_query_builder(input: &DeriveInput) -> proc_macro2::TokenStream // Get builder struct generics let builder_struct_generics = all_required_insert_fields.clone().map(|(field_name, _)| { + let gen_name_pascal = quote::format_ident!("{}", field_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); quote! { - #field_name = NotSet, + #gen_name_pascal = NotSet, } }); @@ -54,8 +56,9 @@ pub fn get_insert_query_builder(input: &DeriveInput) -> proc_macro2::TokenStream }); let phantom_struct_fields = all_required_insert_fields.clone().map(|(name, _)| { + let gen_name_pascal = quote::format_ident!("{}", name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); let ph_name = quote::format_ident!("_{}", name); - quote! { #ph_name: std::marker::PhantomData::<#name>, } + quote! { #ph_name: std::marker::PhantomData::<#gen_name_pascal>, } }); // Create Builder Struct @@ -122,22 +125,26 @@ pub fn get_insert_query_builder(input: &DeriveInput) -> proc_macro2::TokenStream let pre_impl_generics_in = all_required_insert_fields.clone().map(|(gen_name, _)|{ + if gen_name != field_name { - return quote!{ #gen_name, } + let gen_name_pascal = quote::format_ident!("{}", gen_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); + return quote!{ #gen_name_pascal, } } quote!{ } }); let generics_in = all_required_insert_fields.clone().map(|(gen_name, _)|{ if gen_name != field_name { - return quote!{ #gen_name, } + let gen_name_pascal = quote::format_ident!("{}", gen_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); + return quote!{ #gen_name_pascal, } } quote!{ NotSet, } }); let generics_out = all_required_insert_fields.clone().map(|(gen_name, _)|{ if gen_name != field_name { - return quote!{ #gen_name, } + let gen_name_pascal = quote::format_ident!("{}", gen_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); + return quote!{ #gen_name_pascal, } } quote!{ Set, } }); diff --git a/src/modules/insert_query_builder/test_insert_query_builder.rs b/src/modules/insert_query_builder/test_insert_query_builder.rs index ad3df5d..16c8bc9 100644 --- a/src/modules/insert_query_builder/test_insert_query_builder.rs +++ b/src/modules/insert_query_builder/test_insert_query_builder.rs @@ -28,9 +28,9 @@ pub struct Account { "#; let output = r#" -pub struct AccountDbSetInsertBuilder { +pub struct AccountDbSetInsertBuilder { email: Option, - _email: std::marker::PhantomData, + _email: std::marker::PhantomData, } impl AccountDbSetInsertBuilder { pub fn new() -> AccountDbSetInsertBuilder { @@ -82,14 +82,14 @@ pub struct User { "#; let output = r#" -pub struct UserDbSetInsertBuilder { +pub struct UserDbSetInsertBuilder { id: Option, name: Option, details: Option, email: Option, - _id: std::marker::PhantomData, - _name: std::marker::PhantomData, - _email: std::marker::PhantomData, + _id: std::marker::PhantomData, + _name: std::marker::PhantomData, + _email: std::marker::PhantomData, } impl UserDbSetInsertBuilder { pub fn new() -> UserDbSetInsertBuilder { @@ -104,8 +104,8 @@ impl UserDbSetInsertBuilder { } } } -impl UserDbSetInsertBuilder { - pub fn id(self, id: String) -> UserDbSetInsertBuilder { +impl UserDbSetInsertBuilder { + pub fn id(self, id: String) -> UserDbSetInsertBuilder { UserDbSetInsertBuilder { id: Some(id), name: self.name, @@ -117,8 +117,8 @@ impl UserDbSetInsertBuilder { } } } -impl UserDbSetInsertBuilder { - pub fn name(self, name: String) -> UserDbSetInsertBuilder { +impl UserDbSetInsertBuilder { + pub fn name(self, name: String) -> UserDbSetInsertBuilder { UserDbSetInsertBuilder { name: Some(name), id: self.id, @@ -130,8 +130,8 @@ impl UserDbSetInsertBuilder { } } } -impl UserDbSetInsertBuilder { - pub fn details(self, details: String) -> UserDbSetInsertBuilder { +impl UserDbSetInsertBuilder { + pub fn details(self, details: String) -> UserDbSetInsertBuilder { UserDbSetInsertBuilder { details: Some(details), id: self.id, @@ -143,8 +143,8 @@ impl UserDbSetInsertBuilder { } } } -impl UserDbSetInsertBuilder { - pub fn email(self, email: String) -> UserDbSetInsertBuilder { +impl UserDbSetInsertBuilder { + pub fn email(self, email: String) -> UserDbSetInsertBuilder { UserDbSetInsertBuilder { email: Some(email), id: self.id, diff --git a/src/modules/one_query_builder/one_query_builder.rs b/src/modules/one_query_builder/one_query_builder.rs index 1fdbced..e8fa2f4 100644 --- a/src/modules/one_query_builder/one_query_builder.rs +++ b/src/modules/one_query_builder/one_query_builder.rs @@ -1,3 +1,4 @@ +use convert_case::{Case, Casing}; use proc_macro2::Ident; use quote::quote; use syn::DeriveInput; @@ -31,8 +32,9 @@ pub fn get_query_builder(input: &DeriveInput) -> proc_macro2::TokenStream { let builder_struct_generics = all_required_insert_fields .clone() .map(|(field_name, _)| { + let gen_name_pascal = quote::format_ident!("{}", field_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); quote! { - #field_name = NotSet, + #gen_name_pascal = NotSet, } }) .chain(vec![quote! { @@ -56,8 +58,9 @@ pub fn get_query_builder(input: &DeriveInput) -> proc_macro2::TokenStream { }]); let phantom_struct_fields = all_required_insert_fields.clone().map(|(name, _)| { + let gen_name_pascal = quote::format_ident!("{}", name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); let ph_name = quote::format_ident!("_{}", name); - quote! { #ph_name: std::marker::PhantomData::<#name>, } + quote! { #ph_name: std::marker::PhantomData::<#gen_name_pascal>, } }); // Create Builder Struct @@ -81,6 +84,7 @@ pub fn get_query_builder(input: &DeriveInput) -> proc_macro2::TokenStream { let initial_struct_fields = all_query_one_fields .clone() .map(|(name, _)| { + quote! { #name: None, } }) .chain(vec![quote! { @@ -141,21 +145,37 @@ pub fn get_query_builder(input: &DeriveInput) -> proc_macro2::TokenStream { let pre_impl_generics_in = all_required_insert_fields.clone().map(|(gen_name, _)|{ if gen_name != field_name { - return quote!{ #gen_name, } + if !is_unique_field { + let gen_name_pascal = quote::format_ident!("{}", gen_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); + return quote!{ #gen_name_pascal, } + } else { + return quote!{ } + + } } quote!{ } }); let generics_in = all_required_insert_fields.clone().map(|(gen_name, _)|{ if gen_name != field_name { - return quote!{ #gen_name, } + if !is_unique_field { + let gen_name_pascal = quote::format_ident!("{}", gen_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); + return quote!{ #gen_name_pascal, } + } else { + return quote!{ NotSet, } + } } quote!{ NotSet, } }).chain(vec![quote!{NotSet}]); let generics_out = all_required_insert_fields.clone().map(|(gen_name, _)|{ if gen_name != field_name { - return quote!{ #gen_name, } + if !is_unique_field { + let gen_name_pascal = quote::format_ident!("{}", gen_name.to_string().from_case(Case::Snake).to_case(Case::Pascal)); + return quote!{ #gen_name_pascal, } + } else { + return quote!{ NotSet, } + } } quote!{ Set, } }).chain( if is_unique_field { @@ -179,6 +199,7 @@ pub fn get_query_builder(input: &DeriveInput) -> proc_macro2::TokenStream { None => field_type, }; + quote! { impl <#(#pre_impl_generics_in)*> #builder_struct_name <#(#generics_in)*> { pub fn #method_name(self, #field_name: #type_arg) -> #builder_struct_name <#(#generics_out)*> { diff --git a/src/modules/one_query_builder/test_one_query_builder.rs b/src/modules/one_query_builder/test_one_query_builder.rs index 4d7677f..094dec3 100644 --- a/src/modules/one_query_builder/test_one_query_builder.rs +++ b/src/modules/one_query_builder/test_one_query_builder.rs @@ -31,11 +31,11 @@ pub struct User { let output = r#" -pub struct UserDbSetOneQueryBuilder { +pub struct UserDbSetOneQueryBuilder { id: Option, email: Option, _unique_fields: std::marker::PhantomData, - _id: std::marker::PhantomData, + _id: std::marker::PhantomData, } impl UserDbSetOneQueryBuilder { pub fn new() -> UserDbSetOneQueryBuilder { @@ -57,8 +57,8 @@ impl UserDbSetOneQueryBuilder { } } } -impl UserDbSetOneQueryBuilder { - pub fn email_eq(self, email: String) -> UserDbSetOneQueryBuilder { +impl UserDbSetOneQueryBuilder { + pub fn email_eq(self, email: String) -> UserDbSetOneQueryBuilder { UserDbSetOneQueryBuilder { email: Some(email), id: self.id, @@ -134,7 +134,7 @@ pub struct Order { "#; let output = r#" -pub struct OrderDbSetOneQueryBuilder { +pub struct OrderDbSetOneQueryBuilder { id: Option, _id: std::marker::PhantomData, } @@ -250,12 +250,12 @@ pub struct FavouritedProduct { "#; let output = r#" -pub struct FavouritedProductDbSetOneQueryBuilder { +pub struct FavouritedProductDbSetOneQueryBuilder { product_id: Option, user_id: Option, _unique_fields: std::marker::PhantomData, - _product_id: std::marker::PhantomData, - _user_id: std::marker::PhantomData, + _product_id: std::marker::PhantomData, + _user_id: std::marker::PhantomData, } impl FavouritedProductDbSetOneQueryBuilder { pub fn new() -> FavouritedProductDbSetOneQueryBuilder { @@ -269,11 +269,11 @@ impl FavouritedProductDbSetOneQueryBuilder { } } -impl FavouritedProductDbSetOneQueryBuilder { +impl FavouritedProductDbSetOneQueryBuilder { pub fn product_id_eq( self, product_id: uuid::Uuid, - ) -> FavouritedProductDbSetOneQueryBuilder { + ) -> FavouritedProductDbSetOneQueryBuilder { FavouritedProductDbSetOneQueryBuilder { product_id: Some(product_id), user_id: self.user_id, @@ -284,11 +284,11 @@ impl FavouritedProductDbSetOneQueryBuilder { } } } -impl FavouritedProductDbSetOneQueryBuilder { +impl FavouritedProductDbSetOneQueryBuilder { pub fn user_id_eq( self, user_id: uuid::Uuid, - ) -> FavouritedProductDbSetOneQueryBuilder { + ) -> FavouritedProductDbSetOneQueryBuilder { FavouritedProductDbSetOneQueryBuilder { user_id: Some(user_id), product_id: self.product_id, diff --git a/tests/.sqlx/query-e01ff749bb4aba91d5b052b00b89b1498b8b9de32b6eb6a8b0ba78083fbbf9db.json b/tests/.sqlx/query-e01ff749bb4aba91d5b052b00b89b1498b8b9de32b6eb6a8b0ba78083fbbf9db.json new file mode 100644 index 0000000..a1d3dd1 --- /dev/null +++ b/tests/.sqlx/query-e01ff749bb4aba91d5b052b00b89b1498b8b9de32b6eb6a8b0ba78083fbbf9db.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name, email, details FROM users WHERE id = 'id-6';", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "details", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + true + ] + }, + "hash": "e01ff749bb4aba91d5b052b00b89b1498b8b9de32b6eb6a8b0ba78083fbbf9db" +} diff --git a/tests/Cargo.lock b/tests/Cargo.lock index 061adc6..6f02fe0 100644 --- a/tests/Cargo.lock +++ b/tests/Cargo.lock @@ -147,6 +147,15 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cpufeatures" version = "0.2.14" @@ -235,6 +244,7 @@ dependencies = [ name = "db-set-macros" version = "0.1.1" dependencies = [ + "convert_case", "pretty_assertions", "prettyplease", "proc-macro2", @@ -1581,6 +1591,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode_categories" version = "0.1.1" diff --git a/tests/src/harness.rs b/tests/src/harness.rs new file mode 100644 index 0000000..73182a6 --- /dev/null +++ b/tests/src/harness.rs @@ -0,0 +1,59 @@ +use std::{sync::LazyLock, time::Duration}; + +use db_set_macros::DbSet; +use sqlx::{postgres::PgPoolOptions, Pool, Postgres}; +use testcontainers::{clients::Cli, Container}; +use tokio::sync::OnceCell; + +type TestPostgres = testcontainers_modules::postgres::Postgres; + +// LazyLock for testcontainers::Cli, created once and shared +static DOCKER_CLI: LazyLock = LazyLock::new(Cli::default); + +// Global LazyLock holding both the container and database pool. +static DB_RESOURCES: LazyLock, Pool)>> = + LazyLock::new(OnceCell::new); + +async fn prepare_db() -> (Container<'static, TestPostgres>, Pool) { + let container = DOCKER_CLI.run(TestPostgres::default()); + let connection_string = format!( + "postgres://postgres:postgres@127.0.0.1:{}/postgres", + container.get_host_port_ipv4(5432) + ); + + let pool = PgPoolOptions::new() + .max_connections(10) + .acquire_timeout(Duration::from_secs(60)) // Set your preferred timeout + .connect(&connection_string) + .await + .expect("Could not connect to postgres"); + + sqlx::query("CREATE TABLE IF NOT EXISTS users (name text not null, id text not null, email text not null, details text);") + .execute(&pool) + .await + .expect("Could not initialise db"); + + sqlx::query("INSERT INTO users (name, id, email) VALUES ('bob', 'user-1', 'bob@bob.com');") + .execute(&pool) + .await + .expect("Could not initialise db"); + + sqlx::query("INSERT INTO users (name, id, email, details) VALUES ('bob', 'user-2', 'bobo@bob.com', 'the best bob');") + .execute(&pool) + .await + .expect("Could not initialise db"); + + sqlx::query( + "INSERT INTO users (name, id, email) VALUES ('alice', 'user-3', 'alice@alice.com');", + ) + .execute(&pool) + .await + .expect("Could not initialise db"); + + (container, pool) +} + +pub async fn get_db_pool() -> &'static Pool { + let (_, pool) = DB_RESOURCES.get_or_init(prepare_db).await; + pool +} diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 84e26db..eaa9b62 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -1,201 +1,4 @@ -use std::{sync::LazyLock, time::Duration}; - -use db_set_macros::DbSet; -use sqlx::{postgres::PgPoolOptions, Pool, Postgres}; -use testcontainers::{clients::Cli, Container}; -use tokio::sync::OnceCell; - -#[derive(DbSet, Debug, Clone)] -#[dbset(table_name = "users")] -pub struct User { - #[key] - id: String, - name: String, - details: Option, - #[unique] - email: String, -} - -type TestPostgres = testcontainers_modules::postgres::Postgres; - -// LazyLock for testcontainers::Cli, created once and shared -static DOCKER_CLI: LazyLock = LazyLock::new(Cli::default); - -// Global LazyLock holding both the container and database pool. -static DB_RESOURCES: LazyLock, Pool)>> = - LazyLock::new(OnceCell::new); - -async fn prepare_db() -> (Container<'static, TestPostgres>, Pool) { - let container = DOCKER_CLI.run(TestPostgres::default()); - let connection_string = format!( - "postgres://postgres:postgres@127.0.0.1:{}/postgres", - container.get_host_port_ipv4(5432) - ); - - let pool = PgPoolOptions::new() - .max_connections(10) - .acquire_timeout(Duration::from_secs(60)) // Set your preferred timeout - .connect(&connection_string) - .await - .expect("Could not connect to postgres"); - - sqlx::query("CREATE TABLE IF NOT EXISTS users (name text not null, id text not null, email text not null, details text);") - .execute(&pool) - .await - .expect("Could not initialise db"); - - sqlx::query("INSERT INTO users (name, id, email) VALUES ('bob', 'user-1', 'bob@bob.com');") - .execute(&pool) - .await - .expect("Could not initialise db"); - - sqlx::query("INSERT INTO users (name, id, email, details) VALUES ('bob', 'user-2', 'bobo@bob.com', 'the best bob');") - .execute(&pool) - .await - .expect("Could not initialise db"); - - sqlx::query( - "INSERT INTO users (name, id, email) VALUES ('alice', 'user-3', 'alice@alice.com');", - ) - .execute(&pool) - .await - .expect("Could not initialise db"); - - (container, pool) -} - -async fn get_db_pool() -> &'static Pool { - let (_, pool) = DB_RESOURCES.get_or_init(prepare_db).await; - pool -} - -#[tokio::test] -async fn test_fetch_user_by_id() -> Result<(), String> { - let pool = get_db_pool().await; - - let user = UserDbSet::one() - .id_eq("user-1".to_string()) - .fetch_one(pool) - .await - .expect("could not run query"); - - assert_eq!(user.id, "user-1"); - assert_eq!(user.name, "bob"); - Ok(()) -} - -#[tokio::test] -async fn test_query_as_user() -> Result<(), String> { - let pool = get_db_pool().await; - - let user: User = sqlx::query_as!(User, "SELECT id, name, email, details FROM users LIMIT 1;") - .fetch_one(pool) - .await - .expect("Could not fetch one"); - - assert_eq!(user.name, "bob"); - Ok(()) -} - -#[tokio::test] -async fn test_fetch_users_by_name() -> Result<(), String> { - let pool = get_db_pool().await; - - let users = UserDbSet::many() - .name_eq("bob".to_string()) - .fetch_all(pool) - .await - .expect("Could not fetch users"); - - assert_eq!(users.len(), 2); - Ok(()) -} - -#[tokio::test] -async fn test_fetch_users_by_name_and_details() -> Result<(), String> { - let pool = get_db_pool().await; - - let users = UserDbSet::many() - .name_eq("bob".to_string()) - .details_eq("the best bob".to_string()) - .fetch_all(pool) - .await - .expect("Could not fetch users"); - - assert_eq!(users.len(), 1); - Ok(()) -} - -#[tokio::test] -async fn test_fetch_all_users() -> Result<(), String> { - let pool = get_db_pool().await; - - let users = UserDbSet::many() - .fetch_all(pool) - .await - .expect("Could not fetch users"); - - assert_eq!(users.len(), 3); - Ok(()) -} - -// Doesn't work in CI but works locally -#[ignore] -#[tokio::test] -async fn test_insert_users() -> Result<(), String> { - let pool = get_db_pool().await; - - let inserted_user = UserDbSet::insert() - .id("id-3".to_string()) - .email("steven@stevenson.com".to_string()) - .name("steven".to_string()) - .insert(pool) - .await - .expect("Could not insert"); - - let matched_user = sqlx::query_as!( - User, - "SELECT id, name, email, details FROM users WHERE id = 'id-3';" - ) - .fetch_one(pool) - .await - .expect("Could not fetch one"); - - assert_eq!(matched_user.email, inserted_user.email); - - Ok(()) -} - -#[tokio::test] -async fn test_update_users() -> Result<(), String> { - let pool = get_db_pool().await; - - let mut user = sqlx::query_as!( - User, - "SELECT id, name, email, details FROM users WHERE id = 'user-2';" - ) - .fetch_one(pool) - .await - .expect("Could not fetch one"); - - user.details = Some("Updated details!".to_string()); - user.email = String::from("mynewemail@bigpond.com.au"); - UserDbSet::update() - .data(user.clone()) - .update(pool) - .await - .expect("Could not update"); - - let same_user_again = sqlx::query_as!( - User, - "SELECT id, name, email, details FROM users WHERE id = 'user-2';" - ) - .fetch_one(pool) - .await - .expect("Could not fetch one"); - - assert_eq!(user.email, same_user_again.email); - assert_eq!(user.details, same_user_again.details); - - Ok(()) -} +#[cfg(test)] +pub mod harness; +#[cfg(test)] +pub mod test_user_with_one_key_one_unique_one_optional; diff --git a/tests/src/test_user_with_one_key_one_unique_one_optional.rs b/tests/src/test_user_with_one_key_one_unique_one_optional.rs new file mode 100644 index 0000000..b819f50 --- /dev/null +++ b/tests/src/test_user_with_one_key_one_unique_one_optional.rs @@ -0,0 +1,171 @@ +use db_set_macros::DbSet; + +use crate::harness::get_db_pool; + +#[derive(DbSet, Debug, Clone)] +#[dbset(table_name = "users")] +pub struct User { + #[key] + id: String, + name: String, + details: Option, + #[unique] + email: String, +} + +#[tokio::test] +async fn test_fetch_user_by_id() -> Result<(), String> { + let pool = get_db_pool().await; + + let user = UserDbSet::one() + .id_eq("user-1".to_string()) + .fetch_one(pool) + .await + .expect("could not run query"); + + assert_eq!(user.id, "user-1"); + assert_eq!(user.name, "bob"); + Ok(()) +} + +#[tokio::test] +async fn test_query_as_user() -> Result<(), String> { + let pool = get_db_pool().await; + + let user: User = sqlx::query_as!(User, "SELECT id, name, email, details FROM users LIMIT 1;") + .fetch_one(pool) + .await + .expect("Could not fetch one"); + + assert_eq!(user.name, "bob"); + Ok(()) +} + +#[tokio::test] +async fn test_fetch_users_by_name() -> Result<(), String> { + let pool = get_db_pool().await; + + let users = UserDbSet::many() + .name_eq("bob".to_string()) + .fetch_all(pool) + .await + .expect("Could not fetch users"); + + assert_eq!(users.len(), 2); + Ok(()) +} + +#[tokio::test] +async fn test_fetch_users_by_name_and_details() -> Result<(), String> { + let pool = get_db_pool().await; + + let users = UserDbSet::many() + .name_eq("bob".to_string()) + .details_eq("the best bob".to_string()) + .fetch_all(pool) + .await + .expect("Could not fetch users"); + + assert_eq!(users.len(), 1); + Ok(()) +} + +#[tokio::test] +async fn test_fetch_all_users() -> Result<(), String> { + let pool = get_db_pool().await; + + let users = UserDbSet::many() + .fetch_all(pool) + .await + .expect("Could not fetch users"); + + assert_eq!(users.len(), 3); + Ok(()) +} + +#[tokio::test] +async fn test_insert_users() -> Result<(), String> { + let pool = get_db_pool().await; + + let inserted_user = UserDbSet::insert() + .id("id-3".to_string()) + .email("steven@stevenson.com".to_string()) + .name("steven".to_string()) + .insert(pool) + .await + .expect("Could not insert"); + + let matched_user = sqlx::query_as!( + User, + "SELECT id, name, email, details FROM users WHERE id = 'id-3';" + ) + .fetch_one(pool) + .await + .expect("Could not fetch one"); + + assert_eq!(matched_user.email, inserted_user.email); + + Ok(()) +} + +#[tokio::test] +async fn test_update_users() -> Result<(), String> { + let pool = get_db_pool().await; + + let mut user = sqlx::query_as!( + User, + "SELECT id, name, email, details FROM users WHERE id = 'user-2';" + ) + .fetch_one(pool) + .await + .expect("Could not fetch one"); + + user.details = Some("Updated details!".to_string()); + user.email = String::from("mynewemail@bigpond.com.au"); + UserDbSet::update() + .data(user.clone()) + .update(pool) + .await + .expect("Could not update"); + + let same_user_again = sqlx::query_as!( + User, + "SELECT id, name, email, details FROM users WHERE id = 'user-2';" + ) + .fetch_one(pool) + .await + .expect("Could not fetch one"); + + assert_eq!(user.email, same_user_again.email); + assert_eq!(user.details, same_user_again.details); + + Ok(()) +} + +#[tokio::test] +async fn test_delete_users() -> Result<(), String> { + let pool = get_db_pool().await; + + sqlx::query("INSERT INTO users (name, id, email) VALUES ('lana del ete', 'id-6', 'lana@bigpond.com.au');") + .execute(pool) + .await + .expect("Could not initialise db"); + + UserDbSet::delete() + .id_eq("id-6".to_string()) + .delete(pool) + .await + .expect("could not delete"); + + let matched_user = sqlx::query_as!( + User, + "SELECT id, name, email, details FROM users WHERE id = 'id-6';" + ) + .fetch_optional(pool) + .await + .expect("Could not fetch one"); + + assert!(matched_user.is_none()); + + Ok(()) +}