Skip to content

Commit

Permalink
feat: Provide and ProvideRef traits to provide AppContext objec…
Browse files Browse the repository at this point in the history
…ts (#510)

Also, annotate with `mockall::automock` when the new `testing-mocks`
feature is enabled to allow consumers to easily provide mocks to methods
expecting a trait impl.

This is significantly simpler than mocking the `AppContext` struct
itself, so we will not do that for now. We can consider doing it later
if people really want it.

Closes #495
  • Loading branch information
spencewenski authored Nov 16, 2024
1 parent 928ca3b commit 2246d56
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 6 deletions.
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ otel = ["dep:opentelemetry", "dep:opentelemetry_sdk", "dep:opentelemetry-otlp",
grpc = ["dep:tonic"]
testing = ["dep:insta", "dep:rstest", "dep:testcontainers-modules"]
test-containers = ["testing", "dep:testcontainers-modules"]
testing-mocks = ["testing", "dep:mockall"]
config-yml = ["config/yaml"]

[dependencies]
Expand Down Expand Up @@ -96,6 +97,7 @@ tonic = { workspace = true, optional = true }
insta = { workspace = true, optional = true }
rstest = { workspace = true, optional = true }
testcontainers-modules = { workspace = true, features = ["postgres", "redis"], optional = true }
mockall = { workspace = true, optional = true }

# Others
anyhow = { workspace = true }
Expand Down Expand Up @@ -128,7 +130,7 @@ reqwest = { workspace = true }
[dev-dependencies]
cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] }
insta = { workspace = true, features = ["json"] }
mockall = "0.13.0"
mockall = { workspace = true }
mockall_double = "0.3.1"
rstest = { workspace = true }

Expand Down Expand Up @@ -174,6 +176,7 @@ rusty-sidekiq = { version = "0.11.0", default-features = false }
insta = { version = "1.39.0", features = ["toml", "filters"] }
rstest = { version = "0.23.0", default-features = false }
testcontainers-modules = { version = "0.11.3" }
mockall = "0.13.0"

# Others
# Todo: minimize tokio features included in `roadster`
Expand Down
19 changes: 19 additions & 0 deletions examples/full/src/app_state.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
use axum::extract::FromRef;
use roadster::app::context::AppContext;
use roadster::app::context::{Provide, ProvideRef};

#[derive(Clone, FromRef)]
pub struct AppState {
pub app_context: AppContext,
}

impl<T> Provide<T> for AppState
where
AppContext: Provide<T>,
{
fn provide(&self) -> T {
Provide::provide(&self.app_context)
}
}

impl<T> ProvideRef<T> for AppState
where
AppContext: ProvideRef<T>,
{
fn provide(&self) -> &T {
ProvideRef::provide(&self.app_context)
}
}
143 changes: 138 additions & 5 deletions src/app/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ impl AppContext {
let sidekiq_config = &config.service.sidekiq;
let redis_config = &sidekiq_config.custom.redis;
let redis = sidekiq::RedisConnectionManager::new(redis_config.uri.to_string())?;
let redis_enqueue = {
let redis_enqueue = RedisEnqueue({
let pool = bb8::Pool::builder().min_idle(redis_config.enqueue_pool.min_idle);
let pool = redis_config
.enqueue_pool
.max_connections
.iter()
.fold(pool, |pool, max_conns| pool.max_size(*max_conns));
pool.build(redis.clone()).await?
};
});
let redis_fetch = if redis_config
.fetch_pool
.max_connections
Expand Down Expand Up @@ -136,7 +136,9 @@ impl AppContext {

#[cfg(feature = "sidekiq")]
if let Some(redis) = redis {
inner.expect_redis_enqueue().return_const(redis.clone());
inner
.expect_redis_enqueue()
.return_const(RedisEnqueue(redis.clone()));
inner.expect_redis_fetch().return_const(Some(redis));
} else {
inner.expect_redis_fetch().return_const(None);
Expand Down Expand Up @@ -170,11 +172,13 @@ impl AppContext {
self.inner.db()
}

// todo: In the next breaking version, return the pool wrapped in the `RedisEnqueue` new-type
#[cfg(feature = "sidekiq")]
pub fn redis_enqueue(&self) -> &sidekiq::RedisPool {
self.inner.redis_enqueue()
}

// todo: In the next breaking version, return the pool wrapped in the `RedisFetch` new-type
#[cfg(feature = "sidekiq")]
pub fn redis_fetch(&self) -> &Option<sidekiq::RedisPool> {
self.inner.redis_fetch()
Expand All @@ -196,6 +200,134 @@ impl AppContext {
}
}

#[cfg_attr(any(test, feature = "testing-mocks"), mockall::automock)]
pub trait ProvideRef<T> {
fn provide(&self) -> &T;
}

#[cfg_attr(any(test, feature = "testing-mocks"), mockall::automock)]
pub trait Provide<T> {
fn provide(&self) -> T;
}

impl ProvideRef<AppConfig> for AppContext {
fn provide(&self) -> &AppConfig {
self.config()
}
}

impl Provide<AppConfig> for AppContext {
fn provide(&self) -> AppConfig {
self.config().clone()
}
}

impl ProvideRef<AppMetadata> for AppContext {
fn provide(&self) -> &AppMetadata {
self.metadata()
}
}

impl Provide<AppMetadata> for AppContext {
fn provide(&self) -> AppMetadata {
self.metadata().clone()
}
}

impl Provide<Vec<Arc<dyn HealthCheck>>> for AppContext {
fn provide(&self) -> Vec<Arc<dyn HealthCheck>> {
self.health_checks()
}
}

#[cfg(feature = "db-sql")]
impl ProvideRef<DatabaseConnection> for AppContext {
fn provide(&self) -> &DatabaseConnection {
self.db()
}
}

#[cfg(feature = "db-sql")]
impl Provide<DatabaseConnection> for AppContext {
fn provide(&self) -> DatabaseConnection {
self.db().clone()
}
}

#[cfg(feature = "email-smtp")]
impl ProvideRef<lettre::SmtpTransport> for AppContext {
fn provide(&self) -> &lettre::SmtpTransport {
self.smtp()
}
}

#[cfg(feature = "email-smtp")]
impl Provide<lettre::SmtpTransport> for AppContext {
fn provide(&self) -> lettre::SmtpTransport {
self.smtp().clone()
}
}

#[cfg(feature = "email-sendgrid")]
impl ProvideRef<sendgrid::v3::Sender> for AppContext {
fn provide(&self) -> &sendgrid::v3::Sender {
self.sendgrid()
}
}

#[cfg(feature = "email-sendgrid")]
impl Provide<sendgrid::v3::Sender> for AppContext {
fn provide(&self) -> sendgrid::v3::Sender {
self.sendgrid().clone()
}
}

#[cfg(feature = "sidekiq")]
pub struct RedisEnqueue(sidekiq::RedisPool);
#[cfg(feature = "sidekiq")]
pub struct RedisFetch(sidekiq::RedisPool);

#[cfg(feature = "sidekiq")]
impl std::ops::Deref for RedisEnqueue {
type Target = sidekiq::RedisPool;

fn deref(&self) -> &Self::Target {
&self.0
}
}

#[cfg(feature = "sidekiq")]
impl std::ops::Deref for RedisFetch {
type Target = sidekiq::RedisPool;

fn deref(&self) -> &Self::Target {
&self.0
}
}

#[cfg(feature = "sidekiq")]
impl Provide<RedisEnqueue> for AppContext {
fn provide(&self) -> RedisEnqueue {
RedisEnqueue(self.redis_enqueue().clone())
}
}

#[cfg(feature = "sidekiq")]
impl ProvideRef<RedisEnqueue> for AppContext {
fn provide(&self) -> &RedisEnqueue {
self.inner.redis_enqueue()
}
}

#[cfg(feature = "sidekiq")]
impl Provide<Option<RedisFetch>> for AppContext {
fn provide(&self) -> Option<RedisFetch> {
self.redis_fetch()
.as_ref()
.map(|redis_fetch| RedisFetch(redis_fetch.clone()))
}
}

#[cfg(all(feature = "db-sql", feature = "test-containers"))]
#[cfg_attr(test, allow(dead_code))]
async fn db_test_container(
Expand Down Expand Up @@ -289,10 +421,11 @@ struct AppContextInner {
>,
>,
#[cfg(feature = "sidekiq")]
redis_enqueue: sidekiq::RedisPool,
redis_enqueue: RedisEnqueue,
/// The Redis connection pool used by [sidekiq::Processor] to fetch Sidekiq jobs from Redis.
/// May be `None` if the [fetch_pool.max_connections][crate::config::service::worker::sidekiq::ConnectionPool]
/// config is set to zero, in which case the [sidekiq::Processor] would also not be started.
// todo: In the next breaking version, wrap the pool in the `RedisFetch` new-type
#[cfg(feature = "sidekiq")]
redis_fetch: Option<sidekiq::RedisPool>,
#[cfg(all(feature = "sidekiq", feature = "test-containers"))]
Expand Down Expand Up @@ -340,7 +473,7 @@ impl AppContextInner {
}

#[cfg(feature = "sidekiq")]
fn redis_enqueue(&self) -> &sidekiq::RedisPool {
fn redis_enqueue(&self) -> &RedisEnqueue {
&self.redis_enqueue
}

Expand Down

0 comments on commit 2246d56

Please sign in to comment.