Context-Generic Accessor Providers
While the previous accessor implementation for ApiClient
works, it requires explicit and concrete access to the ApiClient
context to implement the accessors. While this approach is manageable with only a couple of accessor methods, it can quickly become cumbersome as the application grows and requires numerous accessors across multiple contexts. A more efficient approach would be to implement context-generic providers for field accessors, allowing us to reuse them across any context that contains the relevant field.
To enable the implementation of context-generic accessors, the cgp
crate provides a derivable HasField
trait. This trait acts as a proxy, allowing access to fields in a concrete context. The trait is defined as follows:
#![allow(unused)] fn main() { use core::marker::PhantomData; pub trait HasField<Tag> { type Value; fn get_field(&self, tag: PhantomData<Tag>) -> &Self::Value; } }
For each field within a concrete context, we can implement a HasField
instance by associating a Tag
type with the field's name and an associated type Value
representing the field's type. Additionally, the HasField
trait includes a get_field
method, which retrieves a reference to the field value from the context. The get_field
method accepts an additional tag
parameter, which is a PhantomData
type parameter tied to the field's name Tag
. This phantom parameter helps with type inference in Rust, as without it, Rust would not be able to deduce which field associated with Tag
is being accessed.
We can automatically derive HasField
instances for a context like ApiClient
using the derive macro, as shown below:
#![allow(unused)] fn main() { extern crate cgp; use cgp::prelude::*; #[derive(HasField)] pub struct ApiClient { pub api_base_url: String, pub auth_token: String, } }
The derive macro would then generate the corresponding HasField
instances for
ApiClient
:
#![allow(unused)] fn main() { extern crate cgp; use core::marker::PhantomData; use cgp::prelude::*; pub struct ApiClient { pub api_base_url: String, pub auth_token: String, } impl HasField<symbol!("api_base_url")> for ApiClient { type Value = String; fn get_field(&self, _tag: PhantomData<symbol!("api_base_url")>) -> &String { &self.api_base_url } } impl HasField<symbol!("auth_token")> for ApiClient { type Value = String; fn get_field(&self, _tag: PhantomData<symbol!("auth_token")>) -> &String { &self.auth_token } } }
Symbols
In the derived HasField
instances, we observe the use of symbol!("api_base_url")
and symbol!("auth_token")
for the Tag
generic type. While a string like "api_base_url"
is a value of type &str
, we need to use it as a type within the Tag
parameter. To achieve this, we use the symbol!
macro to "lift" a string value into a unique type, which allows us to treat the string "api_base_url"
as a type. Essentially, this means that if the string content is the same across two uses of symbol!
, the types will be treated as equivalent.
Behind the scenes, the symbol!
macro first uses the Char
type to "lift" individual characters into types. The Char
type is defined as follows:
#![allow(unused)] fn main() { pub struct Char<const CHAR: char>; }
This makes use of Rust's const generics feature to parameterize Char
with a constant CHAR
of type char
. The Char
struct itself is empty, as we only use it for type-level manipulation.
Although we can use const generics to lift individual characters, we currently cannot use a type like String
or &str
within const generics. As a workaround, we construct a type-level list of characters. For example, symbol!("abc")
is desugared to a type-level list of characters like:
(Char<'a'>, (Char<'b'>, (Char<'c'>, ())))
In cgp
, instead of using Rust’s native tuple, we define the Cons
and Nil
types to represent type-level lists:
#![allow(unused)] fn main() { pub struct Nil; pub struct Cons<Head, Tail>(pub Head, pub Tail); }
The Nil
type represents an empty type-level list, while Cons
is used to prepend an element to the front of the list, similar to how linked lists work in Lisp.
Thus, the actual desugaring of symbol!("abc")
looks like this:
Cons<Char<'a'>, Cons<Char<'b'>, Cons<Char<'c'>, Nil>>>
While this type may seem complex, it has a compact representation from the perspective of the Rust compiler. Furthermore, since we don’t construct values from symbol types at runtime, there is no runtime overhead associated with them. The use of HasField
to implement context-generic accessors introduces negligible compile-time overhead, even in large codebases.
It’s important to note that the current representation of symbols is a temporary workaround. Once Rust supports using strings in const generics, we can simplify the desugaring process and adjust our implementation accordingly.
If the explanation here still feels unclear, think of symbols as strings being used as types rather than values. In later sections, we’ll explore how cgp
provides additional abstractions that abstract away the use of symbol!
and HasField
. These abstractions simplify the process, so you won’t need to worry about these details in simple cases.
Auto Accessor Traits
The process of defining and wiring many CGP components can be overwhelming for developers who are new to CGP. In the early stages of a project, there is typically not much need for customizing how fields are accessed. As a result, some developers may find the full use of field accessors introduced in this chapter unnecessarily complex.
To simplify the use of accessor traits, one approach is to define them not as CGP components, but as regular Rust traits with blanket implementations that leverage HasField
. For example, we can redefine the HasApiBaseUrl
trait as follows:
#![allow(unused)] fn main() { extern crate cgp; use core::marker::PhantomData; use cgp::prelude::*; pub trait HasApiBaseUrl { fn api_base_url(&self) -> &String; } impl<Context> HasApiBaseUrl for Context where Context: HasField<symbol!("api_base_url"), Value = String>, { fn api_base_url(&self) -> &String { self.get_field(PhantomData) } } }
With this approach, the HasApiBaseUrl
trait will be automatically implemented for any context that derives HasField
and contains the relevant field. There is no longer need for explicit wiring of the ApiBaseUrlGetterComponent
within the context components.
This approach allows providers, such as ReadMessageFromApi
, to still use accessor traits like HasApiBaseUrl
to simplify field access. Meanwhile, context implementers can simply use #[derive(HasField)]
without having to worry about manual wiring.
The main drawback of this approach is that the context cannot easily override the implementation of HasApiBaseUrl
, unless it opts not to implement HasField
. However, it would be straightforward to refactor the trait in the future to convert it into a full CGP component.
Overall, this approach may be an appealing option for developers who want a simpler experience with CGP without fully utilizing its advanced features.
The #[cgp_auto_getter]
Macro
To streamline the creation of auto accessor traits, the cgp
crate provides the #[cgp_auto_getter]
macro, which derives blanket implementations for accessor traits. For instance, the earlier example can be rewritten as follows:
#![allow(unused)] fn main() { extern crate cgp; use core::marker::PhantomData; use cgp::prelude::*; cgp_type!( AuthToken ); #[cgp_auto_getter] pub trait HasApiBaseUrl { fn api_base_url(&self) -> &String; } #[cgp_auto_getter] pub trait HasAuthToken: HasAuthTokenType { fn auth_token(&self) -> &Self::AuthToken; } }
Since #[cgp_auto_getter]
generates a blanket implementation leveraging HasField
directly, there is no corresponding provider trait being derived in this case.
The #[cgp_auto_getter]
attribute can also be applied to accessor traits that define multiple getter methods. For instance, we can combine two accessor traits into one, as shown below:
#![allow(unused)] fn main() { extern crate cgp; use core::marker::PhantomData; use cgp::prelude::*; cgp_type!( AuthToken ); #[cgp_auto_getter] pub trait HasApiClientFields: HasAuthTokenType { fn api_base_url(&self) -> &String; fn auth_token(&self) -> &Self::AuthToken; } }
By using #[cgp_auto_getter]
, accessor traits are automatically implemented for contexts that use #[derive(HasField)]
and include fields matching the names and return types of the accessor methods. This approach encapsulates the use of HasField
and symbol!
, providing well-typed and idiomatic interfaces for field access while abstracting the underlying mechanics.
Using HasField
in Accessor Providers
With HasField
, we can implement context-generic providers like ApiUrlGetter
. Here's an example:
#![allow(unused)] fn main() { extern crate cgp; use core::marker::PhantomData; use cgp::prelude::*; #[cgp_component { provider: ApiBaseUrlGetter, }] pub trait HasApiBaseUrl { fn api_base_url(&self) -> &String; } pub struct GetApiUrl; impl<Context> ApiBaseUrlGetter<Context> for GetApiUrl where Context: HasField<symbol!("api_url"), Value = String>, { fn api_base_url(context: &Context) -> &String { context.get_field(PhantomData) } } }
In this implementation, GetApiUrl
is defined for any Context
type that implements HasField<symbol!("api_url"), Value = String>
. This means that as long as the context uses #[derive(HasField)]
, and has a field named api_url
of type String
, the GetApiUrl
provider can be used with it.
Similarly, we can implement a context-generic provider for AuthTokenGetter
as follows:
#![allow(unused)] fn main() { extern crate cgp; use core::marker::PhantomData; use cgp::prelude::*; cgp_type!( AuthToken ); #[cgp_component { provider: AuthTokenGetter, }] pub trait HasAuthToken: HasAuthTokenType { fn auth_token(&self) -> &Self::AuthToken; } pub struct GetAuthToken; impl<Context> AuthTokenGetter<Context> for GetAuthToken where Context: HasAuthTokenType + HasField<symbol!("auth_token"), Value = Context::AuthToken>, { fn auth_token(context: &Context) -> &Context::AuthToken { context.get_field(PhantomData) } } }
The GetAuthToken
provider is slightly more complex since the auth_token
method returns an abstract Context::AuthToken
type. To handle this, we require the Context
to implement HasAuthTokenType
and for the Value
associated type to match Context::AuthToken
. This ensures that GetAuthToken
can be used with any context that has an auth_token
field of the same type as the AuthToken
defined in HasAuthTokenType
.
The UseFields
Pattern
The providers GetAuthToken
and GetApiUrl
share a common characteristic: they implement accessor traits for any context type by utilizing HasField
, with the field name corresponding to the accessor method name. To streamline this pattern, cgp
provides the UseFields
marker struct, which simplifies the implementation of such providers:
#![allow(unused)] fn main() { struct UseFields; }
With UseFields
, we can bypass the need to define custom provider structs and implement the logic directly on UseFields
, as shown below:
#![allow(unused)] fn main() { extern crate cgp; use core::marker::PhantomData; use cgp::prelude::*; #[cgp_component { provider: ApiBaseUrlGetter, }] pub trait HasApiBaseUrl { fn api_base_url(&self) -> &String; } cgp_type!( AuthToken ); #[cgp_component { provider: AuthTokenGetter, }] pub trait HasAuthToken: HasAuthTokenType { fn auth_token(&self) -> &Self::AuthToken; } impl<Context> ApiBaseUrlGetter<Context> for UseFields where Context: HasField<symbol!("api_url"), Value = String>, { fn api_base_url(context: &Context) -> &String { context.get_field(PhantomData) } } impl<Context> AuthTokenGetter<Context> for UseFields where Context: HasAuthTokenType + HasField<symbol!("auth_token"), Value = Context::AuthToken>, { fn auth_token(context: &Context) -> &Context::AuthToken { context.get_field(PhantomData) } } }
The #[cgp_getter]
Macro
The cgp
crate offers the #[cgp_getter]
macro, which automatically derives implementations like UseFields
. As an extension of #[cgp_component]
, it provides the same interface and generates the same CGP component traits and blanket implementations.
With #[cgp_getter]
, you can define accessor traits and seamlessly use UseFields
directly in the component wiring, eliminating the need for manual implementations:
#![allow(unused)] fn main() { extern crate cgp; extern crate cgp_error_anyhow; extern crate reqwest; extern crate serde; use core::fmt::Display; use cgp::core::component::UseDelegate; use cgp::core::error::{ErrorRaiserComponent, ErrorTypeComponent}; use cgp::core::field::UseField; use cgp::extra::error::RaiseFrom; use cgp::prelude::*; use cgp_error_anyhow::{DebugAnyhowError, UseAnyhowError}; use reqwest::blocking::Client; use reqwest::StatusCode; use serde::Deserialize; cgp_type!(Message); cgp_type!(MessageId); cgp_type!(AuthToken); #[cgp_component { provider: MessageQuerier, }] pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType { fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>; } pub struct ReadMessageFromApi; #[derive(Debug)] pub struct ErrStatusCode { pub status_code: StatusCode, } #[derive(Deserialize)] pub struct ApiMessageResponse { pub message: String, } impl<Context> MessageQuerier<Context> for ReadMessageFromApi where Context: HasMessageIdType<MessageId = u64> + HasMessageType<Message = String> + HasApiBaseUrl + HasAuthToken + CanRaiseError<reqwest::Error> + CanRaiseError<ErrStatusCode>, Context::AuthToken: Display, { fn query_message(context: &Context, message_id: &u64) -> Result<String, Context::Error> { let client = Client::new(); let url = format!("{}/api/messages/{}", context.api_base_url(), message_id); let response = client .get(url) .bearer_auth(context.auth_token()) .send() .map_err(Context::raise_error)?; let status_code = response.status(); if !status_code.is_success() { return Err(Context::raise_error(ErrStatusCode { status_code })); } let message_response: ApiMessageResponse = response.json().map_err(Context::raise_error)?; Ok(message_response.message) } } #[cgp_getter { provider: ApiBaseUrlGetter, }] pub trait HasApiBaseUrl { fn api_base_url(&self) -> &String; } #[cgp_getter { provider: AuthTokenGetter, }] pub trait HasAuthToken: HasAuthTokenType { fn auth_token(&self) -> &Self::AuthToken; } #[derive(HasField)] pub struct ApiClient { pub api_base_url: String, pub auth_token: String, } pub struct ApiClientComponents; impl HasComponents for ApiClient { type Components = ApiClientComponents; } delegate_components! { ApiClientComponents { ErrorTypeComponent: UseAnyhowError, ErrorRaiserComponent: UseDelegate<RaiseApiErrors>, MessageTypeComponent: UseType<String>, MessageIdTypeComponent: UseType<u64>, AuthTokenTypeComponent: UseType<String>, [ ApiBaseUrlGetterComponent, AuthTokenGetterComponent, ]: UseFields, MessageQuerierComponent: ReadMessageFromApi, } } pub struct RaiseApiErrors; delegate_components! { RaiseApiErrors { reqwest::Error: RaiseFrom, ErrStatusCode: DebugAnyhowError, } } pub trait CanUseApiClient: CanQueryMessage {} impl CanUseApiClient for ApiClient {} }
Compared to #[cgp_auto_getter]
, #[cgp_getter]
follows the same wiring process as other CGP components. To achieve the same outcome as #[cgp_auto_getter]
, the only additional step required is delegating the getter component to UseFields within delegate_components!
.
The primary advantage of using #[cgp_getter]
is the ability to define custom accessor providers that can retrieve fields from the context in various ways, as we will explore in the next section.
Like #[cgp_auto_getter]
, #[cgp_getter]
can also be used with accessor traits containing multiple methods. This makes it easy to upgrade a trait, such as HasApiClientFields
, to use #[cgp_getter]
if custom accessor providers are needed in the future:
#![allow(unused)] fn main() { extern crate cgp; use core::marker::PhantomData; use cgp::prelude::*; cgp_type!( AuthToken ); #[cgp_getter { provider: ApiClientFieldsGetter, }] pub trait HasApiClientFields: HasAuthTokenType { fn api_base_url(&self) -> &String; fn auth_token(&self) -> &Self::AuthToken; } }
Static Accessors
One advantage of defining minimal accessor traits is that it allows the implementation of custom accessor providers that do not necessarily read field values from the context. For instance, we can create static accessor providers that always return a global constant value.
Static accessors are useful when we want to hard-code values for a specific context. For example, we might define a production ApiClient
context that always uses a fixed API URL:
#![allow(unused)] fn main() { extern crate cgp; use core::marker::PhantomData; use std::sync::OnceLock; use cgp::prelude::*; #[cgp_component { provider: ApiBaseUrlGetter, }] pub trait HasApiBaseUrl { fn api_base_url(&self) -> &String; } pub struct UseProductionApiUrl; impl<Context> ApiBaseUrlGetter<Context> for UseProductionApiUrl { fn api_base_url(_context: &Context) -> &String { static BASE_URL: OnceLock<String> = OnceLock::new(); BASE_URL.get_or_init(|| "https://api.example.com".into()) } } }
In this example, the UseProductionApiUrl
provider implements ApiBaseUrlGetter
for any context type. Inside the api_base_url
method, we define a static
variable BASE_URL
using OnceLock<String>
. This allows us to initialize the global variable exactly once, and it remains constant throughout the application.
OnceLock
is especially useful since constructors like String::from
are not const
fn in Rust. By using OnceLock::get_or_init
, we can run non-const constructors at runtime while still benefiting from compile-time guarantees. The static variable is scoped within the method, so it is only accessible and initialized by the provider.
With UseProductionApiUrl
, we can now define a production ApiClient
context, as shown below:
#![allow(unused)] fn main() { extern crate cgp; extern crate cgp_error_anyhow; extern crate reqwest; extern crate serde; use core::fmt::Display; use core::marker::PhantomData; use std::sync::OnceLock; use cgp::core::component::UseDelegate; use cgp::extra::error::RaiseFrom; use cgp::core::error::{ErrorRaiserComponent, ErrorTypeComponent}; use cgp::core::field::UseField; use cgp::prelude::*; use cgp_error_anyhow::{DebugAnyhowError, UseAnyhowError}; use reqwest::blocking::Client; use reqwest::StatusCode; use serde::Deserialize; cgp_type!( Message ); cgp_type!( MessageId ); cgp_type!( AuthToken ); #[cgp_component { provider: MessageQuerier, }] pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType { fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>; } #[cgp_component { provider: ApiBaseUrlGetter, }] pub trait HasApiBaseUrl { fn api_base_url(&self) -> &String; } #[cgp_component { provider: AuthTokenGetter, }] pub trait HasAuthToken: HasAuthTokenType { fn auth_token(&self) -> &Self::AuthToken; } pub struct ReadMessageFromApi; #[derive(Debug)] pub struct ErrStatusCode { pub status_code: StatusCode, } #[derive(Deserialize)] pub struct ApiMessageResponse { pub message: String, } impl<Context> MessageQuerier<Context> for ReadMessageFromApi where Context: HasMessageIdType<MessageId = u64> + HasMessageType<Message = String> + HasApiBaseUrl + HasAuthToken + CanRaiseError<reqwest::Error> + CanRaiseError<ErrStatusCode>, Context::AuthToken: Display, { fn query_message(context: &Context, message_id: &u64) -> Result<String, Context::Error> { let client = Client::new(); let url = format!("{}/api/messages/{}", context.api_base_url(), message_id); let response = client .get(url) .bearer_auth(context.auth_token()) .send() .map_err(Context::raise_error)?; let status_code = response.status(); if !status_code.is_success() { return Err(Context::raise_error(ErrStatusCode { status_code })); } let message_response: ApiMessageResponse = response.json().map_err(Context::raise_error)?; Ok(message_response.message) } } pub struct UseProductionApiUrl; impl<Context> ApiBaseUrlGetter<Context> for UseProductionApiUrl { fn api_base_url(_context: &Context) -> &String { static BASE_URL: OnceLock<String> = OnceLock::new(); BASE_URL.get_or_init(|| "https://api.example.com".into()) } } pub struct GetAuthToken; impl<Context> AuthTokenGetter<Context> for GetAuthToken where Context: HasAuthTokenType + HasField<symbol!("auth_token"), Value = Context::AuthToken>, { fn auth_token(context: &Context) -> &Context::AuthToken { context.get_field(PhantomData) } } #[derive(HasField)] pub struct ApiClient { pub auth_token: String, } pub struct ApiClientComponents; pub struct RaiseApiErrors; impl HasComponents for ApiClient { type Components = ApiClientComponents; } delegate_components! { ApiClientComponents { ErrorTypeComponent: UseAnyhowError, ErrorRaiserComponent: UseDelegate<RaiseApiErrors>, MessageIdTypeComponent: UseType<u64>, MessageTypeComponent: UseType<String>, AuthTokenTypeComponent: UseType<String>, ApiBaseUrlGetterComponent: UseProductionApiUrl, AuthTokenGetterComponent: GetAuthToken, MessageQuerierComponent: ReadMessageFromApi, } } delegate_components! { RaiseApiErrors { reqwest::Error: RaiseFrom, ErrStatusCode: DebugAnyhowError, } } pub trait CanUseApiClient: CanQueryMessage {} impl CanUseApiClient for ApiClient {} }
In the component wiring, we specify UseProductionApiUrl
as the provider for ApiBaseUrlGetterComponent
. Notably, the ApiClient
context no longer contains the api_base_url
field.
Static accessors are particularly useful for implementing specialized contexts where certain fields must remain constant. With this approach, constant values don't need to be passed around as part of the context during runtime, and there's no concern about incorrect values being assigned at runtime. Additionally, because of the compile-time wiring, this method may offer performance benefits compared to passing dynamic values during execution.
Using HasField
Directly Inside Providers
Since the HasField
trait can be automatically derived by contexts, some developers may be tempted to forgo defining accessor traits and instead use HasField
directly within the providers. For example, one could remove HasApiBaseUrl
and HasAuthToken
and implement ReadMessageFromApi
as follows:
#![allow(unused)] fn main() { extern crate cgp; extern crate reqwest; extern crate serde; use core::fmt::Display; use core::marker::PhantomData; use cgp::prelude::*; use reqwest::blocking::Client; use reqwest::StatusCode; use serde::Deserialize; cgp_type!( Message ); cgp_type!( MessageId ); cgp_type!( AuthToken ); #[cgp_component { provider: MessageQuerier, }] pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType { fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>; } pub struct ReadMessageFromApi; #[derive(Debug)] pub struct ErrStatusCode { pub status_code: StatusCode, } #[derive(Deserialize)] pub struct ApiMessageResponse { pub message: String, } impl<Context> MessageQuerier<Context> for ReadMessageFromApi where Context: HasMessageIdType<MessageId = u64> + HasMessageType<Message = String> + HasAuthTokenType + HasField<symbol!("api_base_url"), Value = String> + HasField<symbol!("auth_token"), Value = Context::AuthToken> + CanRaiseError<reqwest::Error> + CanRaiseError<ErrStatusCode>, Context::AuthToken: Display, { fn query_message(context: &Context, message_id: &u64) -> Result<String, Context::Error> { let client = Client::new(); let url = format!( "{}/api/messages/{}", context.get_field(PhantomData::<symbol!("api_base_url")>), message_id ); let response = client .get(url) .bearer_auth(context.get_field(PhantomData::<symbol!("auth_token")>)) .send() .map_err(Context::raise_error)?; let status_code = response.status(); if !status_code.is_success() { return Err(Context::raise_error(ErrStatusCode { status_code })); } let message_response: ApiMessageResponse = response.json().map_err(Context::raise_error)?; Ok(message_response.message) } } }
In the example above, the provider ReadMessageFromApi
requires the context to implement HasField<symbol!("api_base_url")>
and HasField<symbol!("auth_token")>
. To preserve the original behavior, we add constraints ensuring that the api_base_url
field is of type String
and that the auth_token
field matches the type of Context::AuthToken
.
When using get_field
, since there are multiple HasField
instances in scope, we need to fully qualify the field access to specify which field we want to retrieve. For example, we call context.get_field(PhantomData::<symbol!("api_base_url")>)
to access the api_base_url
field.
However, while the direct use of HasField
is possible, it does not necessarily simplify the code. In fact, it often requires more verbose specifications for each field. Additionally, using HasField
directly necessitates explicitly defining the field types. In contrast, with custom accessor traits like HasAuthToken
, we can specify that a method returns an abstract type like `Self::AuthToken, which prevents accidental access to fields with the same underlying concrete type.
Using HasField
directly also makes the provider less flexible if the context requires custom access methods. For instance, if we wanted to put the api_base_url
field inside a separate ApiConfig
struct, we would run into difficulties with HasField
:
#![allow(unused)] fn main() { pub struct Config { pub api_base_url: String, // other fields } pub struct ApiClient { pub config: Config, pub auth_token: String, // other fields } }
In this case, an accessor trait like HasApiUrl
would allow the context to easily use a custom accessor provider. With direct use of HasField
, however, indirect access would be more cumbersome to implement.
That said, using HasField
directly can be convenient during the initial development stages, as it reduces the number of traits a developer needs to manage. Therefore, we encourage readers to use HasField
where appropriate and gradually migrate to more specific accessor traits when necessary.