From 80f4af9087bee73888a7befba5237f66da7bd993 Mon Sep 17 00:00:00 2001 From: Martin Berg Alstad Date: Sun, 8 Sep 2024 17:27:20 +0200 Subject: [PATCH] Moved more code from hotel_service to lib --- Cargo.lock | 60 ++++++++++++++++++++++++++++-- Cargo.toml | 11 +++++- examples/multipart_file/Cargo.lock | 1 + src/axum/app.rs | 8 ++-- src/axum/builder.rs | 14 +++++++ src/axum/mod.rs | 3 ++ src/axum/response.rs | 19 +++++++++- src/axum/traits.rs | 7 ++++ src/diesel/get_connection.rs | 29 +++++++++++++++ src/diesel/migration.rs | 9 +++++ src/diesel/mod.rs | 2 + src/lib.rs | 2 + src/serde/mod.rs | 1 + src/serde/traits.rs | 7 ++++ src/test/diesel_pool.rs | 45 ++++++++++++++++++++++ src/test/mod.rs | 3 ++ src/test/test_containers.rs | 39 +++++++++++++++++++ src/vector/map.rs | 9 ++--- 18 files changed, 254 insertions(+), 15 deletions(-) create mode 100644 src/axum/builder.rs create mode 100644 src/axum/traits.rs create mode 100644 src/diesel/get_connection.rs create mode 100644 src/diesel/migration.rs create mode 100644 src/serde/traits.rs create mode 100644 src/test/diesel_pool.rs create mode 100644 src/test/mod.rs create mode 100644 src/test/test_containers.rs diff --git a/Cargo.lock b/Cargo.lock index 4ed2732..cc93fff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,6 +509,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "diesel_async_migrations" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99915cbb9455e8fd56f12edc58f92bbdf8161e4363cb2000cf4308aa6358ff4" +dependencies = [ + "diesel", + "diesel-async", + "diesel_async_migrations_macros", + "scoped-futures", + "tracing", +] + [[package]] name = "diesel_async_migrations_macros" version = "0.12.0" @@ -1061,6 +1074,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" name = "lib" version = "1.4.3" dependencies = [ + "async-trait", "axum", "chrono", "deadpool-diesel", @@ -1069,11 +1083,14 @@ dependencies = [ "diesel-async", "diesel-crud-derive", "diesel-crud-trait", + "diesel_async_migrations 0.15.0", "into-response-derive", "mime", "nom", "read-files", "serde", + "serde_json", + "testcontainers-modules 0.10.0", "thiserror", "tokio", "tokio-util", @@ -1954,13 +1971,50 @@ dependencies = [ "url", ] +[[package]] +name = "testcontainers" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef8374cea2c164699681ecc39316c3e1d953831a7a5721e36c7736d974e15fa" +dependencies = [ + "async-trait", + "bollard", + "bollard-stubs", + "bytes", + "dirs", + "docker_credential", + "either", + "futures", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + [[package]] name = "testcontainers-modules" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "868e8e818fe37b8ed4c21ac72185206b48e8767b5ad3836d7ec0e5c9386e19a2" dependencies = [ - "testcontainers", + "testcontainers 0.21.1", +] + +[[package]] +name = "testcontainers-modules" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "359d9a225791e1b9f60aab01f9ae9471898b9b9904b5db192104a71e96785079" +dependencies = [ + "testcontainers 0.22.0", ] [[package]] @@ -1970,10 +2024,10 @@ dependencies = [ "derive_more", "diesel", "diesel-async", - "diesel_async_migrations", + "diesel_async_migrations 0.14.0", "dotenvy_macro", "lib", - "testcontainers-modules", + "testcontainers-modules 0.9.0", "thiserror", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 4254e27..c0b333e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ tower = { version = "0.5", optional = true } tower-http = { version = "0.5", optional = true, features = ["trace", "cors", "normalize-path"] } mime = { version = "0.3", optional = true } # Async +async-trait = { workspace = true } tokio = { workspace = true, optional = true, features = ["fs", "rt-multi-thread"] } tokio-util = { version = "0.7", optional = true, features = ["io"] } # Database @@ -33,6 +34,7 @@ diesel = { workspace = true, optional = true, features = ["postgres"] } diesel-async = { workspace = true, optional = true, features = ["postgres", "deadpool"] } diesel-crud-derive = { path = "crates/diesel_crud_derive", optional = true } diesel-crud-trait = { path = "crates/diesel_crud_trait", optional = true } +diesel_async_migrations = { version = "0.15", optional = true } deadpool-diesel = { workspace = true, optional = true, features = ["postgres"] } # Error handling thiserror = { workspace = true, optional = true } @@ -46,6 +48,9 @@ into-response-derive = { path = "crates/into_response_derive", optional = true } read-files = { path = "crates/read_files", optional = true } # Serialization / Deserialization serde = { version = "1.0", optional = true, features = ["derive"] } +serde_json = { version = "1.0", optional = true } +# Test +testcontainers-modules = { version = "0.10", features = ["postgres"], optional = true } # Time chrono = { version = "0.4", optional = true, features = ["serde"] } # Utils @@ -54,6 +59,7 @@ derive_more = { workspace = true, features = ["from", "constructor"] } [workspace.dependencies] # Async tokio = "1.40" +async-trait = "0.1" # Database diesel = "2.2" diesel-async = "0.5" @@ -70,11 +76,12 @@ derive_more = "1.0" [features] axum = ["dep:axum", "dep:tower", "dep:tower-http", "dep:thiserror", "dep:tracing", "dep:tracing-subscriber", "dep:tokio", "dep:mime"] -diesel = ["dep:diesel-crud-trait", "dep:diesel", "dep:diesel-async", "dep:deadpool-diesel"] +diesel = ["dep:diesel-crud-trait", "dep:diesel", "dep:diesel-async", "dep:deadpool-diesel", "dep:diesel_async_migrations"] io = ["dep:tokio", "dep:tokio-util"] iter = [] nom = ["dep:nom"] -serde = ["dep:serde"] +serde = ["dep:serde", "dep:serde_json"] derive = ["dep:into-response-derive", "dep:diesel-crud-derive"] read-files = ["dep:read-files"] time = ["dep:chrono"] +test = ["dep:testcontainers-modules"] diff --git a/examples/multipart_file/Cargo.lock b/examples/multipart_file/Cargo.lock index a61d4de..c3d2e3e 100644 --- a/examples/multipart_file/Cargo.lock +++ b/examples/multipart_file/Cargo.lock @@ -308,6 +308,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" name = "lib" version = "1.4.3" dependencies = [ + "async-trait", "axum", "derive_more", "mime", diff --git a/src/axum/app.rs b/src/axum/app.rs index 97c5356..1640b83 100644 --- a/src/axum/app.rs +++ b/src/axum/app.rs @@ -152,11 +152,13 @@ impl AppBuilder { let _ = fmt_trace(); // Allowed to fail let listener = self.listener().await?; - if self.normalize_path.unwrap_or(true) { - let app = NormalizePathLayer::trim_trailing_slash().layer(self.build()); + let should_normalize = self.normalize_path.unwrap_or(true); + let app = self.build(); + + if should_normalize { + let app = NormalizePathLayer::trim_trailing_slash().layer(app); axum::serve(listener, ServiceExt::::into_make_service(app)).await?; } else { - let app = self.build(); axum::serve(listener, app.into_make_service()).await?; }; Ok(()) diff --git a/src/axum/builder.rs b/src/axum/builder.rs new file mode 100644 index 0000000..92f911b --- /dev/null +++ b/src/axum/builder.rs @@ -0,0 +1,14 @@ +use crate::axum::traits::BuildJson; +use axum::body::Body; +use axum::http::header::CONTENT_TYPE; +use axum::http::Request; +use mime::APPLICATION_JSON; +use serde::Serialize; +use serde_json::json; + +impl BuildJson for axum::http::request::Builder { + fn json(self, body: T) -> Result, axum::http::Error> { + self.header(CONTENT_TYPE, APPLICATION_JSON.as_ref()) + .body(Body::new(json!(body).to_string())) + } +} diff --git a/src/axum/mod.rs b/src/axum/mod.rs index 3985f79..ccc919d 100644 --- a/src/axum/mod.rs +++ b/src/axum/mod.rs @@ -1,8 +1,11 @@ pub mod app; +#[cfg(feature = "serde")] +pub mod builder; pub mod extractor; pub mod load; #[cfg(feature = "serde")] pub mod response; pub mod router; +pub mod traits; #[cfg(feature = "serde")] pub mod wrappers; diff --git a/src/axum/response.rs b/src/axum/response.rs index a197c9e..2e44d5e 100644 --- a/src/axum/response.rs +++ b/src/axum/response.rs @@ -1,10 +1,15 @@ use { - crate::serde::response::BaseResponse, + crate::{serde::response::BaseResponse, serde::traits::DeserializeInto}, + async_trait::async_trait, axum::{ + body::to_bytes, response::{IntoResponse, Response}, Json, }, - serde::Serialize, + serde::{ + de::{DeserializeOwned, Error}, + Serialize, + }, }; impl IntoResponse for BaseResponse { @@ -13,6 +18,16 @@ impl IntoResponse for BaseResponse { } } +#[async_trait] +impl DeserializeInto for Response { + async fn deserialize_into(self) -> Result { + let body = to_bytes(self.into_body(), usize::MAX).await.map_err(|e| { + serde_json::Error::custom(format!("Failed to read response body: {}", e)) + })?; + serde_json::from_slice(&body) + } +} + #[cfg(test)] mod tests { use axum::http::header::CONTENT_TYPE; diff --git a/src/axum/traits.rs b/src/axum/traits.rs new file mode 100644 index 0000000..8410bf7 --- /dev/null +++ b/src/axum/traits.rs @@ -0,0 +1,7 @@ +use axum::body::Body; +use axum::http::Request; +use serde::Serialize; + +pub trait BuildJson { + fn json(self, body: T) -> Result, axum::http::Error>; +} diff --git a/src/diesel/get_connection.rs b/src/diesel/get_connection.rs new file mode 100644 index 0000000..d58938f --- /dev/null +++ b/src/diesel/get_connection.rs @@ -0,0 +1,29 @@ +use axum::async_trait; +use deadpool_diesel::Status; +use derive_more::From; +use diesel_async::pooled_connection::deadpool::{Object, PoolError}; +use diesel_async::AsyncPgConnection; +use lib::diesel::pool::PgPool; + +#[async_trait] +pub trait GetConnection: Clone + Send + Sync { + async fn get(&self) -> Result, GetConnectionError>; + fn status(&self) -> Status; +} + +#[async_trait] +impl GetConnection for PgPool { + async fn get(&self) -> Result, GetConnectionError> { + self.get().await.map_err(Into::into) + } + #[inline] + fn status(&self) -> Status { + self.status() + } +} + +#[derive(Debug, From)] +pub enum GetConnectionError { + PoolError(PoolError), + DieselError(diesel::result::Error), +} diff --git a/src/diesel/migration.rs b/src/diesel/migration.rs new file mode 100644 index 0000000..8f70910 --- /dev/null +++ b/src/diesel/migration.rs @@ -0,0 +1,9 @@ +use diesel_async::AsyncPgConnection; +use diesel_async_migrations::EmbeddedMigrations; + +pub async fn run_migrations( + migrations: &EmbeddedMigrations, + conn: &mut AsyncPgConnection, +) -> Result<(), diesel::result::Error> { + migrations.run_pending_migrations(conn).await +} diff --git a/src/diesel/mod.rs b/src/diesel/mod.rs index d117bc2..b0497e7 100644 --- a/src/diesel/mod.rs +++ b/src/diesel/mod.rs @@ -1,3 +1,5 @@ +pub mod get_connection; +pub mod migration; pub mod pool; /// Re-export diesel::result::Error as DieselError diff --git a/src/lib.rs b/src/lib.rs index ab25c26..85b4d19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,8 @@ pub mod io; pub mod nom; #[cfg(feature = "serde")] pub mod serde; +#[cfg(feature = "test")] +pub mod test; #[cfg(feature = "time")] pub mod time; pub mod traits; diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 4c6f2cd..17a4b66 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -1 +1,2 @@ pub mod response; +pub mod traits; diff --git a/src/serde/traits.rs b/src/serde/traits.rs new file mode 100644 index 0000000..6109e67 --- /dev/null +++ b/src/serde/traits.rs @@ -0,0 +1,7 @@ +use async_trait::async_trait; +use serde::de::DeserializeOwned; + +#[async_trait] +pub trait DeserializeInto { + async fn deserialize_into(self) -> Result; +} diff --git a/src/test/diesel_pool.rs b/src/test/diesel_pool.rs new file mode 100644 index 0000000..019c895 --- /dev/null +++ b/src/test/diesel_pool.rs @@ -0,0 +1,45 @@ +use crate::diesel::get_connection::{GetConnection, GetConnectionError}; +use crate::diesel::pool::PgPool; +use crate::diesel::DieselError; +use axum::async_trait; +use deadpool_diesel::postgres::BuildError; +use deadpool_diesel::Status; +use derive_more::From; +use diesel_async::pooled_connection::deadpool::Object; +use diesel_async::{AsyncConnection, AsyncPgConnection}; +use lib::diesel::pool::create_pool_from_url_with_size; + +#[derive(Clone)] +pub struct PoolStub(PgPool); + +#[derive(Debug, PartialEq, From)] +pub enum Error { + Connection(diesel::ConnectionError), + Database(DieselError), +} + +pub async fn setup_test_transaction(url: impl AsRef) -> Result { + let mut conn = AsyncPgConnection::establish(url.as_ref()).await?; + conn.begin_test_transaction().await?; + Ok(conn) +} + +pub async fn create_test_pool_url_with_size( + url: impl Into, + size: usize, +) -> Result { + let pool = create_pool_from_url_with_size(url, size)?; + Ok(PoolStub(pool)) +} + +#[async_trait] +impl GetConnection for PoolStub { + async fn get(&self) -> Result, GetConnectionError> { + let mut conn = self.0.get().await?; + conn.begin_test_transaction().await?; + Ok(conn) + } + fn status(&self) -> Status { + unimplemented!("PoolStub does not support status") + } +} diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 0000000..e2dd405 --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1,3 @@ +#[cfg(feature = "diesel")] +pub mod diesel_pool; +pub mod test_containers; diff --git a/src/test/test_containers.rs b/src/test/test_containers.rs new file mode 100644 index 0000000..98f3d17 --- /dev/null +++ b/src/test/test_containers.rs @@ -0,0 +1,39 @@ +use crate::diesel::pool::{create_pool_from_url, PgPool}; +use deadpool_diesel::postgres::BuildError; +use derive_more::{Constructor, From}; +use diesel_async::pooled_connection::deadpool::PoolError; +use lib::diesel::DieselError; +use testcontainers_modules::postgres::Postgres; +use testcontainers_modules::testcontainers::runners::AsyncRunner; +use testcontainers_modules::testcontainers::{ContainerAsync, TestcontainersError}; + +/// When the TestContainer is dropped, the container will be removed. +/// # Errors +/// If destructed and the container field is dropped, the container will be dropped, and using the pool will cause an error. +#[derive(Constructor)] +pub struct TestContainer { + pub container: ContainerAsync, + pub pool: PgPool, +} + +pub async fn create_test_containers_pool<'a>() -> Result { + let container = create_postgres_container().await?; + let connection_string = format!( + "postgres://postgres:postgres@127.0.0.1:{}/postgres", + container.get_host_port_ipv4(5432).await? + ); + let pool = create_pool_from_url(connection_string)?; + Ok(TestContainer::new(container, pool)) +} + +pub async fn create_postgres_container() -> Result, TestcontainersError> { + Postgres::default().start().await +} + +#[derive(Debug, From)] +pub enum ContainerError { + TestContainers(TestcontainersError), + BuildError(BuildError), + PoolError(PoolError), + DieselError(DieselError), +} diff --git a/src/vector/map.rs b/src/vector/map.rs index 0fa4cc6..891bc47 100644 --- a/src/vector/map.rs +++ b/src/vector/map.rs @@ -23,9 +23,8 @@ #[macro_export] macro_rules! map { () => { std::collections::HashMap::new() }; - ($default:ty; $($key:expr),* $(,)?) => { + ($default:ty; $($key:expr),+ $(,)?) => { { - #[allow(unused_mut)] let mut temp_map = std::collections::HashMap::new(); $( temp_map.insert($key, <$default>::default()); @@ -76,8 +75,8 @@ mod tests { } #[test] - fn test_map_only_keys_0_keys() { - let map: HashMap = map!(usize;); - assert_eq!(map.len(), 0); + fn test_map_only_keys_1_key() { + let map: HashMap = map!(usize; 1); + assert_eq!(map.len(), 1); } }