diff --git a/Cargo.lock b/Cargo.lock index e7564ce..c9e11c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "anyhow" @@ -155,6 +155,7 @@ version = "0.1.0" dependencies = [ "anyhow", "cgp", + "datetime", "itertools", "serde", "serde_json", @@ -189,12 +190,35 @@ dependencies = [ "cgp-component", ] +[[package]] +name = "datetime" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c3f7a77f3e57fedf80e09136f2d8777ebf621207306f6d96d610af048354bc" +dependencies = [ + "iso8601", + "libc", + "locale", + "pad", + "redox_syscall", + "winapi", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "iso8601" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e86914a73535f3f541a765adea0a9fafcf53fa6adb73662c4988fd9233766f" +dependencies = [ + "nom", +] + [[package]] name = "itertools" version = "0.11.0" @@ -210,12 +234,46 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "locale" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "pad" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" +dependencies = [ + "unicode-width", +] + [[package]] name = "prettyplease" version = "0.2.25" @@ -244,6 +302,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "ryu" version = "1.0.18" @@ -298,3 +362,37 @@ name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index f90bf52..236cf16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -itertools = "0.11.0" +cgp = { version = "0.2.0" } serde = {version = "1", features = ["derive"] } +itertools = "0.11.0" serde_json = "1" anyhow = "1" -cgp = { version = "0.2.0" } \ No newline at end of file +datetime = "0.5.2" \ No newline at end of file diff --git a/content/SUMMARY.md b/content/SUMMARY.md index a93be92..8131c18 100644 --- a/content/SUMMARY.md +++ b/content/SUMMARY.md @@ -20,22 +20,18 @@ # Design Patterns +- [Associated Types](associated-types.md) +- [Error Handling]() + - [`HasErrorType`]() + - [From Errors]() + - [`CanRaiseError`]() + - [Error Wrapping]() - [Component Presets]() -- [Associated Types]() - - [Parameterized Associated Types]() - - [Impl-side Generic Types]() - - [Associated Type Specialization]() - - [`HasType`]() - [Trait-Generic Providers]() - [`WithProvider`]() - [`UseContext`]() - [`UseType`]() - [`UseDelegate`]() -- [Error Handling]() - - [`HasErrorType`]() - - [From Errors]() - - [`CanRaiseError`]() - - [Error Wrapping]() - [Provider Composition]() - [Provider Middleware]() - [Detached Provider]() diff --git a/content/associated-types.md b/content/associated-types.md index 05b9015..4727001 100644 --- a/content/associated-types.md +++ b/content/associated-types.md @@ -1 +1,854 @@ -# Associated Types \ No newline at end of file +# Associated Types + +In the first part of this book, we have learned about how CGP makes use of +Rust's trait system to wire up components using blanket implementations. +Since CGP works within Rust's trait system, we can make use of advanced +Rust features together with CGP to form new design patterns. +In this chapter, we will learn about how to make use of _associated types_ +with CGP to define context-generic providers that are generic over multiple +_abstract_ types. + +## Building Authentication Components + +Supposed that we want to build a simple authentication system using _bearer tokens_ +with expiry time. To build such system, we would need to fetch the expiry time of +a valid token, and ensure that the time is not in the past. A naive attempt of +implementing the authentication would be as follows: + +```rust +# 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; + } + + #[cgp_component { + provider: CurrentTimeGetter, + }] + pub trait HasCurrentTime { + fn current_time(&self) -> Result; + } +} + +pub mod impls { + use std::time::{SystemTime, UNIX_EPOCH}; + + use anyhow::{anyhow, Error}; + + use super::traits::*; + + pub struct ValidateTokenIsNotExpired; + + impl AuthTokenValidator 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 CurrentTimeGetter for GetSystemTimestamp { + fn current_time(_context: &Context) -> Result { + 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, + } + + pub struct MockAppComponents; + + impl HasComponents for MockApp { + type Components = MockAppComponents; + } + + delegate_components! { + MockAppComponents { + CurrentTimeGetterComponent: GetSystemTimestamp, + AuthTokenValidatorComponent: ValidateTokenIsNotExpired, + } + } + + impl AuthTokenExpiryFetcher for MockAppComponents { + fn fetch_auth_token_expiry( + context: &MockApp, + auth_token: &str, + ) -> Result { + context + .auth_tokens_store + .get(auth_token) + .cloned() + .ok_or_else(|| anyhow!("invalid auth token")) + } + } + + pub trait CanUseMockApp: CanValidateAuthToken {} + + impl CanUseMockApp for MockApp {} +} +# +# } +``` + +We first define `CanValidateAuthToken`, which would be used as the main API for validating an +auth token. In order to help implementing the validator, we also define +`CanFetchAuthTokenExpiry` used for fetching the expiry time of an auth token, if the token is valid. +Finally, we also define `HasCurrentTime` which is used for fetching the current time. + +We then define a context-generic provider `ValidateTokenIsNotExpired`, which validates auth tokens +by fetching the token's expiry time and the current time, and ensure that the token's expiry time +does not exceed the current time. We also define a context-generic provider `GetSystemTimestamp`, +which gets the current time using `std::time::System::now()`. + +For the purpose of this demo, we also define a concrete context `MockApp`, which contains a +`auth_tokens_store` field with mocked collection of auth tokens with respective expiry time +stored in a `BTreeMap`. +We then implement a context-specific provider of `AuthTokenExpiryFetcher` for `MockApp`, +which reads from the mocked `auth_tokens_store`. +We also define a check trait `CanUseMockApp`, to check that `MockApp` correctly implements +`CanValidateAuthToken` with the wiring provided. + +## Abstract Types + +The naive example above makes use of basic CGP techniques to implement a reusable provider +`ValidateTokenIsNotExpired`, which can be used with different concrete contexts. +However, we can see that the method signatures are tied to specific types. +In particular, we used `String` to represent the auth token, and `u64` to +represent the unix timestamp in milliseconds. + +Common wisdom tells us that we should use distinct types to distinguish values +from specific domains, so that we do not accidentally mix up values from different +domains. A common approach in Rust is to make use of the _newtype pattern_ to +define wrapper types such as follows: + +```rust +pub struct AuthToken { + value: String, +} + +pub struct Time { + value: u64, +} +``` + +Although the newtype pattern abstracts over the underlying value, it does not allow +our code to be generalized over distinct types. For example, instead of defining +our own `Time` type with Unix timestamp semantics, we may want to use a datetime +library such as `chrono` or `datetime`. However, the exact choice of a datetime +library may depend on the specific use case of a concrete application. + +A better approach would be to define an _abstract_ time type, so that we can +implement context-generic providers that can work with _any_ time type that +the concrete context chooses. We can do this in CGP by defining _type traits_ +that contain _associated types_: + +```rust +# 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; +} +``` + +We first introduce a `HasTimeType` trait, which contains only an associated type +`Time`. We also have additional constraints that the abstract `Time` type must +implement `Eq` and `Ord`, so that we compare between two time values. +Similarly, we also introduce a `HasAuthTokenType` trait, which contains an `AuthToken` +associated type, but without any extra constraint. + +Similar to trait methods, we can use CGP to auto derive blanket implementations +that delegate the implementation associated types to providers using `HasComponents` +and `DelegateComponent`. As such, we can use `#[cgp_component]` also on traits that +contain associated types. + +With the type traits defined, we can update our authentication components to make +use of the abstract types inside the trait methods: + +```rust +# 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; +} + +#[cgp_component { + provider: CurrentTimeGetter, +}] +pub trait HasCurrentTime: HasTimeType { + fn current_time(&self) -> Result; +} +``` + +The trait `CanValidateAuthToken` is updated to include `HasAuthTokenType` +as a supertrait, so that it can accept the abstract type `Self::AuthToken` +inside the method parameter `validate_auth_token`. +Similarly, `CanFetchAuthTokenExpiry` requires both `HasAuthTokenType` +and `HasTimeType`, while `HasCurrentTime` only requires `HasTimeType`. + +With the abstract types in place, we can now redefine `ValidateTokenIsNotExpired` +to be implemented generically over _any_ abstract `Time` and `AuthToken` types. + +```rust +# 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; +# } +# +# #[cgp_component { +# provider: CurrentTimeGetter, +# }] +# pub trait HasCurrentTime: HasTimeType { +# fn current_time(&self) -> Result; +# } +# +pub struct ValidateTokenIsNotExpired; + +impl AuthTokenValidator 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")) + } + } +} +``` + +Through this example, we can see that CGP allows us to define context-generic providers +that are not only generic over the main context, but also all its associated types. +Compared to regular generic programming, instead of specifying all generic parameters +by position, we are able to parameterize the abstract types using _names_, in the form +of associated types. + +## Trait Minimalism + +It may look overly verbose to define multiple type traits and require +the exact type trait to to be included as the supertrait of a method +interface. For example, one may be tempted to define just one trait +that contains methods and types, such as: + +```rust +# 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; + + fn current_time(&self) -> Result; +} +``` + +However, doing so introduces unnecessary _coupling_ between unrelated types and methods. +For example, an application may want to implement the token validation by forwarding the +validation to an external _microservice_. In such case, it would be redundant to require +the application to choose a time type that it won'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 to such traits. + +Because of this, we encourage readers to follow our advice and be _encouraged_ +to use as many minimal traits without worrying about any theoretical overhead. +That said, this advice is _non-binding_, so readers are free to add as many items +as they prefer into a trait, and perhaps go through the hard way of learning why the +alternative is better. + +## Impl-Side Associated Type Constraints + +The minimalism philosophy for CGP also extends to the constraints specified +on the associated type inside a type trait. +Looking back at the definition of `HasTimeType`: + +```rust +# extern crate cgp; +# +# use cgp::prelude::*; +# +#[cgp_component { + name: TimeTypeComponent, + provider: ProvideTimeType, +}] +pub trait HasTimeType { + type Time: Eq + Ord; +} +``` + +The associated `Time` type has the constraint `Eq + Ord` specified. With this, the constraints +are imposed on _all_ concrete time types, regardless of whether they are actually used by +the providers. In fact, if we revisit our previous code, we could notice that the `Eq` +constraint is not reallying being used anywhere. + +For this reason, the constraints specified on the associated type often become a bottleneck +that significantly restricts how the application can evolve. For example, as the application +grows more complex, it is not uncommon to now require `Time` to implement many additional traits, +such as `Debug + Display + Clone + Hash + Serialize + Deserialize` and so on. + +Fortunately with CGP, we can reuse the same techniques as impl-side dependencies, and apply +them on the associated type constraints: + +```rust +# extern crate cgp; +# extern crate anyhow; +# +# use anyhow::{anyhow, Error}; +# use cgp::prelude::*; +# +#[cgp_component { + name: TimeTypeComponent, + provider: ProvideTimeType, +}] +pub trait HasTimeType { + type Time; +} + +# #[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; +# } +# +# #[cgp_component { +# provider: CurrentTimeGetter, +# }] +# pub trait HasCurrentTime: HasTimeType { +# fn current_time(&self) -> Result; +# } +# +pub struct ValidateTokenIsNotExpired; + +impl AuthTokenValidator 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 the above example, we redefine `HasTimeType::Time` to _not_ have any constraint. +Then in the provider implementation of `ValidateTokenIsNotExpired`, we add an +additional constraint that requires `Context::Time: Ord`. This way, +`ValidateTokenIsNotExpired` is able to compare the token expiry time, even +when `Ord` is not specified on `HasTimeType::Time`. + +With this approach, we can _conditionally_ require `HasTimeType::Time` to implement +`Ord`, only when `ValidateTokenIsNotExpired` is used as the provider. +This essentially allows the abstract types to scale in the same way as the generic +context types, and allows us to make use of the same CGP patterns also on abstract types. + +That said, in some cases it is still convenient to directly include constraints +such as `Debug` on an associated type, especially if the constraint is used in +almost all providers. With the current state of error reporting, including +all constraints on the associated type also tend to provide better error messages, +when there is any unsatisfied constraint. + +As a guideline, we encourage readers to first try to define type traits without +including any constraint on the associated type, and try to include the constraints +on the impl-side as often as possible. However readers are free to include default +constraints to associated types as they see fit, at least for relatively trivial +types such as `Debug` and `Eq`. + +## Type Providers + +With the type abstraction in place, we can define different context-generic +providers for the `Time` and `AuthToken` abstract types. +For instance, we can define a provider that provides `std::time::Instant` +as the `Time` type: + +```rust +# 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; +# } +# +pub struct UseInstant; + +impl ProvideTimeType for UseInstant { + type Time = Instant; +} + +impl CurrentTimeGetter for UseInstant +where + Context: HasTimeType