Associated Types

In the first part of this book, we explored how CGP leverages Rust's trait system to wire up components using blanket implementations. Because CGP operates within Rust's trait system, it allows us to incorporate advanced Rust features to create new design patterns. In this chapter, we will focus on using associated types with CGP to define context-generic providers that are generic over multiple abstract types.

Building Authentication Components

Suppose we want to build a simple authentication system using bearer tokens with an expiration time. To achieve this, we need to fetch the expiration time of a valid token and ensure that it is not in the past. A naive approach to implementing the authentication might look like the following:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

pub mod main {
pub mod traits {
    use anyhow::Error;
    use cgp::prelude::*;

    #[cgp_component {
        provider: AuthTokenValidator,
    }]
    pub trait CanValidateAuthToken {
        fn validate_auth_token(&self, auth_token: &str) -> Result<(), Error>;
    }

    #[cgp_component {
        provider: AuthTokenExpiryFetcher,
    }]
    pub trait CanFetchAuthTokenExpiry {
        fn fetch_auth_token_expiry(&self, auth_token: &str) -> Result<u64, Error>;
    }

    #[cgp_component {
        provider: CurrentTimeGetter,
    }]
    pub trait HasCurrentTime {
        fn current_time(&self) -> Result<u64, Error>;
    }
}

pub mod impls {
    use std::time::{SystemTime, UNIX_EPOCH};

    use anyhow::{anyhow, Error};

    use super::traits::*;

    pub struct ValidateTokenIsNotExpired;

    impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
    where
        Context: HasCurrentTime + CanFetchAuthTokenExpiry,
    {
        fn validate_auth_token(context: &Context, auth_token: &str) -> Result<(), Error> {
            let now = context.current_time()?;

            let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

            if token_expiry < now {
                Ok(())
            } else {
                Err(anyhow!("auth token has expired"))
            }
        }
    }

    pub struct GetSystemTimestamp;

    impl<Context> CurrentTimeGetter<Context> for GetSystemTimestamp {
        fn current_time(_context: &Context) -> Result<u64, Error> {
            let now = SystemTime::now()
                .duration_since(UNIX_EPOCH)?
                .as_millis()
                .try_into()?;

            Ok(now)
        }
    }
}

pub mod contexts {
    use std::collections::BTreeMap;

    use anyhow::anyhow;
    use cgp::prelude::*;

    use super::impls::*;
    use super::traits::*;

    pub struct MockApp {
        pub auth_tokens_store: BTreeMap<String, u64>,
    }

    pub struct MockAppComponents;

    impl HasComponents for MockApp {
        type Components = MockAppComponents;
    }

    delegate_components! {
        MockAppComponents {
            CurrentTimeGetterComponent: GetSystemTimestamp,
            AuthTokenValidatorComponent: ValidateTokenIsNotExpired,
        }
    }

    impl AuthTokenExpiryFetcher<MockApp> for MockAppComponents {
        fn fetch_auth_token_expiry(
            context: &MockApp,
            auth_token: &str,
        ) -> Result<u64, anyhow::Error> {
            context
                .auth_tokens_store
                .get(auth_token)
                .cloned()
                .ok_or_else(|| anyhow!("invalid auth token"))
        }
    }

    pub trait CanUseMockApp: CanValidateAuthToken {}

    impl CanUseMockApp for MockApp {}
}

}
}

In this example, we first define the CanValidateAuthToken trait, which serves as the primary API for validating authentication tokens. To facilitate the implementation of the validator, we also define the CanFetchAuthTokenExpiry trait, which is responsible for fetching the expiration time of an authentication token — assuming the token is valid. Finally, the HasCurrentTime trait is introduced to retrieve the current time.

Next, we define a context-generic provider, ValidateTokenIsNotExpired, which validates authentication tokens by comparing their expiration time with the current time. The provider fetches both the token’s expiration time and the current time, and ensure that the token is still valid. Additionally, we define another context-generic provider, GetSystemTimestamp, which retrieves the current time using std::time::SystemTime::now().

For this demonstration, we introduce a concrete context, MockApp, which includes an auth_tokens_store field. This store is a mocked collection of authentication tokens with their respective expiration times, stored in a BTreeMap. We also implement the AuthTokenExpiryFetcher trait specifically for the MockApp context, which retrieves expiration times from the mocked auth_tokens_store. Lastly, we define the CanUseMockApp trait, ensuring that MockApp properly implements the CanValidateAuthToken trait through the provided wiring.

Abstract Types

The previous example demonstrates basic CGP techniques for implementing a reusable provider, ValidateTokenIsNotExpired, which can work with different concrete contexts. However, the method signatures are tied to specific types. For instance, we use String to represent the authentication token and u64 to represent the Unix timestamp in milliseconds.

Common practice suggests that we should use distinct types to differentiate values from different domains, reducing the chance of mixing them up. A common approach in Rust is to use the newtype pattern to define wrapper types, like so:

#![allow(unused)]
fn main() {
pub struct AuthToken {
    value: String,
}

pub struct Time {
    value: u64,
}
}

While the newtype pattern helps abstract over underlying values, it doesn't fully generalize the code to work with different types. For example, instead of defining our own Time type with Unix timestamp semantics, we may want to use a datetime library such as datetime or chrono. The choice of library could depend on the specific use case of a concrete application.

A more flexible approach is to define an abstract Time type that allows us to implement context-generic providers compatible with any Time type chosen by the concrete context. This can be achieved in CGP by defining type traits that contain associated types:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time: Eq + Ord;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}
}

Here, we define the HasTimeType trait with an associated type Time, which is constrained to types that implement Eq and Ord so that they can be compared. Similarly, the HasAuthTokenType trait defines an associated type AuthToken, without any additional constraints.

Similar to regular trait methods, CGP allows us to auto-derive blanket implementations that delegate the associated types to providers using HasComponents and DelegateComponent. Therefore, we can use #[cgp_component] on traits containing associated types as well.

With these type traits in place, we can now update our authentication components to leverage abstract types within the trait methods:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use std::time::Instant;

use anyhow::Error;
use cgp::prelude::*;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time: Eq + Ord;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType {
    fn fetch_auth_token_expiry(&self, auth_token: &Self::AuthToken) -> Result<Self::Time, Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType {
    fn current_time(&self) -> Result<Self::Time, Error>;
}
}

Here, we modify the CanValidateAuthToken trait to include HasAuthTokenType as a supertrait, allowing it to accept the abstract type Self::AuthToken as a method parameter. Likewise, CanFetchAuthTokenExpiry requires both HasAuthTokenType and HasTimeType, while HasCurrentTime only requires HasTimeType.

With the abstract types defined, we can now update ValidateTokenIsNotExpired to work generically with any Time and AuthToken types:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use anyhow::{anyhow, Error};
use cgp::prelude::*;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time: Eq + Ord;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType {
    fn fetch_auth_token_expiry(&self, auth_token: &Self::AuthToken) -> Result<Self::Time, Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType {
    fn current_time(&self) -> Result<Self::Time, Error>;
}

pub struct ValidateTokenIsNotExpired;

impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
where
    Context: HasCurrentTime + CanFetchAuthTokenExpiry,
{
    fn validate_auth_token(
        context: &Context,
        auth_token: &Context::AuthToken,
    ) -> Result<(), Error> {
        let now = context.current_time()?;

        let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

        if token_expiry < now {
            Ok(())
        } else {
            Err(anyhow!("auth token has expired"))
        }
    }
}
}

This example shows how CGP enables us to define context-generic providers that are not just generic over the context itself, but also over its associated types. Unlike traditional generic programming, where all generic parameters are specified positionally, CGP allows us to parameterize abstract types using names via associated types.

Defining Abstract Type Traits with cgp_type!

The type traits HasTimeType and HasAuthTokenType share a similar structure, and as you define more abstract types, this boilerplate can become tedious. To streamline the process, the cgp crate provides the cgp_type! macro, which simplifies type trait definitions.

Here's how you can define the same types with cgp_type!:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

cgp_type!( Time: Eq + Ord );
cgp_type!( AuthToken );
}

The cgp_type! macro accepts the name of an abstract type, $name, along with any applicable constraints for that type. It then automatically generates the same implementation as the cgp_component macro: a consumer trait named Has{$name}Type, a provider trait named Provide{$name}Type, and a component name type named ${name}TypeComponent. Each of the generated traits includes an associated type defined as type $name: $constraints;. In addition, cgp_type! also derives some other implementations, which we'll explore in later chapters.

Trait Minimalism

At first glance, it might seem overly verbose to define multiple type traits and require each to be explicitly included as a supertrait of a method interface. For instance, you might be tempted to consolidate the methods and types into a single trait, like this:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use cgp::prelude::*;
use anyhow::Error;

#[cgp_component {
    provider: AppImpl,
}]
pub trait AppTrait {
    type Time: Eq + Ord;

    type AuthToken;

    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Error>;

    fn fetch_auth_token_expiry(&self, auth_token: &Self::AuthToken) -> Result<Self::Time, Error>;

    fn current_time(&self) -> Result<Self::Time, Error>;
}
}

While this approach might seem simpler, it introduces unnecessary coupling between potentially unrelated types and methods. For example, an application implementing token validation might delegate this functionality to an external microservice. In such a case, it is redundant to require the application to specify a Time type that it doesn’t actually use.

In practice, we find the practical benefits of defining many minimal traits often outweight any theoretical advantages of combining multiple items into one trait. As we will demonstrate in later chapters, having traits that contain only one type or method would also enable more advanced CGP patterns to be applied, including the use of cgp_type! that we have just covered.

We encourage readers to embrace minimal traits without concern for theoretical overhead. However, during the early phases of a project, you might prefer to consolidate items to reduce cognitive overload while learning or prototyping. As the project matures, you can always refactor and decompose larger traits into smaller, more focused ones, following the techniques outlined in this book.

Impl-Side Associated Type Constraints

The minimalism philosophy of CGP extends to the constraints placed on associated types within type traits. Consider the earlier definition of HasTimeType:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

cgp_type!( Time: Eq + Ord );
}

Here, the associated Time type is constrained by Eq + Ord. This means that all concrete implementations of Time must satisfy these constraints, regardless of whether they are actually required by the providers. In fact, if we revisit our previous examples, we notice that the Eq constraint isn’t used anywhere.

Such overly restrictive constraints can become a bottleneck as the application evolves. As complexity increases, it’s common to require additional traits on Time, such as Debug + Display + Clone + Hash + Serialize + Deserialize and so on. Imposing these constraints globally limits flexibility and makes it harder to adapt to changing requirements.

Fortunately, CGP allows us to apply the same principle of impl-side dependencies to associated type constraints. Consider the following example:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use anyhow::{anyhow, Error};
use cgp::prelude::*;

cgp_type!( Time );

cgp_type!( AuthToken );

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType {
    fn fetch_auth_token_expiry(&self, auth_token: &Self::AuthToken) -> Result<Self::Time, Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType {
    fn current_time(&self) -> Result<Self::Time, Error>;
}

pub struct ValidateTokenIsNotExpired;

impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
where
    Context: HasCurrentTime + CanFetchAuthTokenExpiry,
    Context::Time: Ord,
{
    fn validate_auth_token(
        context: &Context,
        auth_token: &Context::AuthToken,
    ) -> Result<(), Error> {
        let now = context.current_time()?;

        let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

        if token_expiry < now {
            Ok(())
        } else {
            Err(anyhow!("auth token has expired"))
        }
    }
}
}

In this example, we redefine HasTimeType::Time without any constraints. Instead, we specify the constraint Context::Time: Ord in the provider implementation for ValidateTokenIsNotExpired. This ensures that the ValidateTokenIsNotExpired provider can compare the token expiry time using Ord, while avoiding unnecessary global constraints on Time.

By applying constraints on the implementation side, we can conditionally require HasTimeType::Time to implement Ord, but only when the ValidateTokenIsNotExpired provider is in use. This approach allows abstract types to scale flexibly alongside generic context types, enabling the same CGP patterns to be applied to abstract types.

In some cases, it can still be convenient to include constraints (e.g., Debug) directly on an associated type, especially if those constraints are nearly universal across providers. Additionally, current Rust error reporting often produces clearer error messages when constraints are defined at the associated type level, as opposed to being deferred to the implementation.

As a guideline, we recommend that readers begin by defining type traits without placing constraints on associated types, relying instead on implementation-side constraints wherever possible. However, readers may choose to apply global constraints to associated types when appropriate, particularly for simple and widely applicable traits like Debug and Eq.

Type Providers

With type abstraction in place, we can define context-generic providers for the Time and AuthToken abstract types. For example, we can create a provider that uses std::time::Instant as the Time type:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use std::time::Instant;

use cgp::prelude::*;
use anyhow::Error;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time: Eq + Ord;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType {
    fn current_time(&self) -> Result<Self::Time, Error>;
}

pub struct UseInstant;

impl<Context> ProvideTimeType<Context> for UseInstant {
    type Time = Instant;
}

impl<Context> CurrentTimeGetter<Context> for UseInstant
where
    Context: HasTimeType<Time = Instant>,
{
    fn current_time(_context: &Context) -> Result<Instant, Error> {
        Ok(Instant::now())
    }
}
}

Here, the UseInstant provider implements ProvideTimeType for any Context type by setting the associated type Time to Instant. Additionally, it implements CurrentTimeGetter for any Context, provided that Context::Time is Instant. This type equality constraint works similarly to regular implementation-side dependencies and is frequently used for scope-limited access to a concrete type associated with an abstract type.

The type equality constraint is necessary because a given context might not always use UseInstant as the provider for ProvideTimeType. Instead, the context could choose a different provider that uses another type to represent Time. Consequently, UseInstant can only implement CurrentTimeGetter if the Context uses it or another provider that also uses Instant as its Time type.

Aside from Instant, we can also define alternative providers for Time, using other types like datetime::LocalDateTime:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;
extern crate datetime;

use cgp::prelude::*;
use anyhow::Error;
use datetime::LocalDateTime;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time: Eq + Ord;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType {
    fn current_time(&self) -> Result<Self::Time, Error>;
}

pub struct UseLocalDateTime;

impl<Context> ProvideTimeType<Context> for UseLocalDateTime {
    type Time = LocalDateTime;
}

impl<Context> CurrentTimeGetter<Context> for UseLocalDateTime
where
    Context: HasTimeType<Time = LocalDateTime>,
{
    fn current_time(_context: &Context) -> Result<LocalDateTime, Error> {
        Ok(LocalDateTime::now())
    }
}
}

Since our application only requires the Time type to implement Ord and the ability to retrieve the current time, we can easily swap between different time providers, as long as they meet these dependencies. As the application evolves, additional constraints might be introduced on the Time type, potentially limiting the available concrete time types. However, with CGP, we can incrementally introduce new dependencies based on the application’s needs, avoiding premature restrictions caused by unused requirements.

Similarly, for the abstract AuthToken type, we can define a context-generic provider ProvideAuthTokenType that uses String as its implementation:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

pub struct UseStringAuthToken;

impl<Context> ProvideAuthTokenType<Context> for UseStringAuthToken {
    type AuthToken = String;
}
}

Compared to the newtype pattern, we can use plain String values directly, without wrapping them in a newtype struct. Contrary to common wisdom, in CGP, we place less emphasis on wrapping every domain type in a newtype. This is particularly true when most of the application is written in a context-generic style. The rationale is that abstract types and their accompanying interfaces already fulfill the role of newtypes by encapsulating and "protecting" raw values, reducing the need for additional wrapping.

That said, readers are free to define newtypes and use them alongside abstract types. For beginners, this can be especially useful, as later chapters will explore methods to properly restrict access to underlying concrete types in context-generic code. Additionally, newtypes remain valuable when the raw values are also used in non-context-generic code, where access to the concrete types is unrestricted.

Throughout this book, we will primarily use plain types to implement abstract types, without additional newtype wrapping. However, we will revisit the comparison between newtypes and abstract types in later chapters, providing further guidance on when each approach is most appropriate.

The UseType Pattern

Implementing type providers can quickly become repetitive as the number of abstract types grows. For example, to use String as the AuthToken type, we first need to define a new struct, UseStringAuthToken, and then implement ProvideAuthTokenType for it. To streamline this process, the cgp_type! macro simplifies the implementation by automatically generating a provider using the UseType pattern. The generated implementation looks like this:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

pub struct UseType<Type>(pub PhantomData<Type>);

impl<Context, AuthToken> ProvideAuthTokenType<Context> for UseType<AuthToken> {
    type AuthToken = AuthToken;
}
}

Here, UseType is a marker type with a generic parameter Type, representing the type to be used for a given type trait. Since PhantomData is its only field, UseType is never intended to be used as a runtime value. The generic implementation of ProvideAuthTokenType for UseType ensures that the AuthToken type is directly set to the Type parameter of UseType.

With this generic implementation, we can redefine UseStringAuthToken as a simple type alias for UseType<String>:

#![allow(unused)]
fn main() {
use core::marker::PhantomData;

pub struct UseType<Type>(pub PhantomData<Type>);

type UseStringAuthToken = UseType<String>;
}

In fact, we can even skip defining type aliases altogether and use UseType directly in the delegate_components macro when wiring type providers.

The UseType struct is included in the cgp crate, and when you define an abstract type using the cgp_type! macro, the corresponding generic UseType implementation is automatically derived. This makes UseType a powerful tool for simplifying component wiring and reducing boilerplate in your code.

Putting It Altogether

With all the pieces in place, we can now apply what we've learned and refactor our naive authentication components to utilize abstract types, as shown below:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;
extern crate datetime;

pub mod main {
pub mod traits {
    use anyhow::Error;
    use cgp::prelude::*;

    cgp_type!( Time );
    cgp_type!( AuthToken );

    #[cgp_component {
        provider: AuthTokenValidator,
    }]
    pub trait CanValidateAuthToken: HasAuthTokenType {
        fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Error>;
    }

    #[cgp_component {
        provider: AuthTokenExpiryFetcher,
    }]
    pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType {
        fn fetch_auth_token_expiry(
            &self,
            auth_token: &Self::AuthToken,
        ) -> Result<Self::Time, Error>;
    }

    #[cgp_component {
        provider: CurrentTimeGetter,
    }]
    pub trait HasCurrentTime: HasTimeType {
        fn current_time(&self) -> Result<Self::Time, Error>;
    }
}

pub mod impls {
    use anyhow::{anyhow, Error};
    use cgp::prelude::*;
    use datetime::LocalDateTime;

    use super::traits::*;

    pub struct ValidateTokenIsNotExpired;

    impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
    where
        Context: HasCurrentTime + CanFetchAuthTokenExpiry,
        Context::Time: Ord,
    {
        fn validate_auth_token(
            context: &Context,
            auth_token: &Context::AuthToken,
        ) -> Result<(), Error> {
            let now = context.current_time()?;

            let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

            if token_expiry < now {
                Ok(())
            } else {
                Err(anyhow!("auth token has expired"))
            }
        }
    }

    pub struct UseLocalDateTime;

    impl<Context> ProvideTimeType<Context> for UseLocalDateTime {
        type Time = LocalDateTime;
    }

    impl<Context> CurrentTimeGetter<Context> for UseLocalDateTime
    where
        Context: HasTimeType<Time = LocalDateTime>,
    {
        fn current_time(_context: &Context) -> Result<LocalDateTime, Error> {
            Ok(LocalDateTime::now())
        }
    }
}

pub mod contexts {
    use std::collections::BTreeMap;

    use anyhow::anyhow;
    use cgp::prelude::*;
    use datetime::LocalDateTime;

    use super::impls::*;
    use super::traits::*;

    pub struct MockApp {
        pub auth_tokens_store: BTreeMap<String, LocalDateTime>,
    }

    pub struct MockAppComponents;

    impl HasComponents for MockApp {
        type Components = MockAppComponents;
    }

    delegate_components! {
        MockAppComponents {
            [
                TimeTypeComponent,
                CurrentTimeGetterComponent,
            ]: UseLocalDateTime,
            AuthTokenTypeComponent: UseType<String>,
            AuthTokenValidatorComponent: ValidateTokenIsNotExpired,
        }
    }

    impl AuthTokenExpiryFetcher<MockApp> for MockAppComponents {
        fn fetch_auth_token_expiry(
            context: &MockApp,
            auth_token: &String,
        ) -> Result<LocalDateTime, anyhow::Error> {
            context
                .auth_tokens_store
                .get(auth_token)
                .cloned()
                .ok_or_else(|| anyhow!("invalid auth token"))
        }
    }

    pub trait CanUseMockApp: CanValidateAuthToken {}

    impl CanUseMockApp for MockApp {}
}

}
}

Compared to our earlier approach, it is now much easier to update the MockApp context to use different time and auth token providers. If different use cases require distinct concrete types, we can easily define additional context types with different configurations, all without duplicating the core logic.

So far, we have applied abstract types to the Time and AuthToken types, but we are still relying on the concrete anyhow::Error type. In the next chapter, we will explore error handling in depth and learn how to use abstract error types to improve the way application errors are managed.