Field Accessors
With impl-side dependencies, CGP offers a way to inject dependencies into providers without cluttering the public interfaces with extra constraints. One common use of this dependency injection is for a provider to retrieve values from the context. This pattern is often referred to as a field accessor or getter, since it involves accessing field values from the context. In this chapter, we'll explore how to define and use field accessors effectively with CGP.
Example: API Call
Suppose our application needs to make API calls to an external service to read messages by their message ID. To abstract away the details of the API call, we can define CGP traits as follows:
#![allow(unused)] fn main() { extern crate cgp; use cgp::prelude::*; #[cgp_type] pub trait HasMessageType { type Message; } #[cgp_type] pub trait HasMessageIdType { type MessageId; } #[cgp_component(MessageQuerier)] pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType { fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>; } }
Following the patterns for associated types, we define the type traits HasMessageIdType
and HasMessageType
to abstract away the details of the message ID and message structures. Additionally, the CanQueryMessage
trait accepts an abstract MessageId
and returns either an abstract Message
or an abstract Error
, following the patterns for error handling.
With the interfaces defined, we now implement a simple API client provider that queries the message via an HTTP request.
#![allow(unused)] fn main() { extern crate cgp; extern crate reqwest; extern crate serde; use cgp::prelude::*; use reqwest::blocking::Client; use reqwest::StatusCode; use serde::Deserialize; #[cgp_type] pub trait HasMessageType { type Message; } #[cgp_type] pub trait HasMessageIdType { type MessageId; } #[cgp_component(MessageQuerier)] pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType { fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>; } #[derive(Debug)] pub struct ErrStatusCode { pub status_code: StatusCode, } #[derive(Deserialize)] pub struct ApiMessageResponse { pub message: String, } #[cgp_new_provider] impl<Context> MessageQuerier<Context> for ReadMessageFromApi where Context: HasMessageIdType<MessageId = u64> + HasMessageType<Message = String> + CanRaiseError<reqwest::Error> + CanRaiseError<ErrStatusCode>, { fn query_message(_context: &Context, message_id: &u64) -> Result<String, Context::Error> { let client = Client::new(); let response = client .get(format!("http://localhost:8000/api/messages/{message_id}")) .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) } } }
For the purposes of the examples in this chapter, we will use the reqwest
library to make HTTP calls. We will also use the blocking version of the API in this chapter, as asynchronous programming in CGP will be covered in later chapters.
In the example above, we implement MessageQuerier
for the ReadMessageFromApi
provider. For simplicity, we add the constraint that MessageId
must be of type u64
and the Message
type is a basic String
.
We also use the context to handle errors. Specifically, we raise the reqwest::Error
returned by the reqwest
methods, as well as a custom ErrStatusCode
error if the server responds with an error HTTP status.
Within the method, we first create a reqwest::Client
, and then use it to send an HTTP GET request to the URL "http://localhost:8000/api/messages/{message_id}"
. If the returned HTTP status is unsuccessful, we raise the ErrStatusCode
. Otherwise, we parse the response body as JSON into the ApiMessageResponse
struct, which expects the response to contain a message
field.
It's clear that the naive provider has some hard-coded values. For instance, the API base URL http://localhost:8000
is fixed, but it should be configurable. In the next section, we will explore how to define accessor traits to retrieve these configurable values from the context.
Getting the Base API URL
In CGP, defining an accessor trait to retrieve values from the context is straightforward. To make the base API URL configurable, we define a HasApiBaseUrl
trait as follows:
#![allow(unused)] fn main() { extern crate cgp; use cgp::prelude::*; #[cgp_component(ApiBaseUrlGetter)] pub trait HasApiBaseUrl { fn api_base_url(&self) -> &String; } }
The HasApiBaseUrl
trait defines a method, api_base_url
, which returns a reference to a String
from the context. In production applications, you might prefer to return a url::Url
or even an abstract Url
type instead of a String
. However, for simplicity, we use a String
in this example.
Next, we can include the HasApiBaseUrl
trait within ReadMessageFromApi
, allowing us to construct the HTTP request using the base API URL provided by the context:
#![allow(unused)] fn main() { extern crate cgp; extern crate reqwest; extern crate serde; use cgp::prelude::*; use reqwest::blocking::Client; use reqwest::StatusCode; use serde::Deserialize; #[cgp_type] pub trait HasMessageType { type Message; } #[cgp_type] pub trait HasMessageIdType { type MessageId; } #[cgp_component(MessageQuerier)] pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType { fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>; } #[cgp_component(ApiBaseUrlGetter)] pub trait HasApiBaseUrl { fn api_base_url(&self) -> &String; } #[derive(Debug)] pub struct ErrStatusCode { pub status_code: StatusCode, } #[derive(Deserialize)] pub struct ApiMessageResponse { pub message: String, } #[cgp_new_provider] impl<Context> MessageQuerier<Context> for ReadMessageFromApi where Context: HasMessageIdType<MessageId = u64> + HasMessageType<Message = String> + HasApiBaseUrl + CanRaiseError<reqwest::Error> + CanRaiseError<ErrStatusCode>, { 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).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) } } }
Getting the Auth Token
In addition to the base API URL, many API services require authentication to protect their resources from unauthorized access. For this example, we’ll use simple bearer tokens for API access.
Just as we did with HasApiBaseUrl
, we can define a HasAuthToken
trait to retrieve the authentication token as follows:
#![allow(unused)] fn main() { extern crate cgp; use cgp::prelude::*; #[cgp_type] pub trait HasAuthTokenType { type AuthToken; } #[cgp_component(AuthTokenGetter)] pub trait HasAuthToken: HasAuthTokenType { fn auth_token(&self) -> &Self::AuthToken; } }
Similar to the pattern used in the earlier chapter, we first define HasAuthTokenType
to keep the AuthToken
type abstract. In fact, this HasAuthTokenType
trait and its associated providers can be reused across different chapters or applications. This demonstrates how minimal CGP traits facilitate the reuse of interfaces in multiple contexts.
Next, we define a getter trait, HasAuthToken
, which provides access to an abstract AuthToken
value from the context. With this in place, we can now update ReadMessageFromApi
to include the authentication token in the Authorization
HTTP header:
#![allow(unused)] fn main() { extern crate cgp; extern crate reqwest; extern crate serde; use core::fmt::Display; use cgp::prelude::*; use reqwest::blocking::Client; use reqwest::StatusCode; use serde::Deserialize; #[cgp_type] pub trait HasMessageType { type Message; } #[cgp_type] pub trait HasMessageIdType { type MessageId; } #[cgp_type] pub trait HasAuthTokenType { type AuthToken; } #[cgp_component(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; } #[derive(Debug)] pub struct ErrStatusCode { pub status_code: StatusCode, } #[derive(Deserialize)] pub struct ApiMessageResponse { pub message: String, } #[cgp_new_provider] 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) } } }
In this updated code, we use the bearer_auth
method from the reqwest
library to include the authentication token in the HTTP header. In this case, the provider only requires that Context::AuthToken
implement the Display
trait, allowing it to work with custom AuthToken
types, not limited to String
.
Traits with Multiple Getter Methods
As you build providers that need access to various pieces of data – like a ReadMessageFromApi
provider that requires both an api_base_url
and an auth_token
– you might initially consider grouping these accessors into a single trait.
Here's an example of how you could define a trait with multiple getter methods for API client fields:
#![allow(unused)] fn main() { extern crate cgp; use cgp::prelude::*; #[cgp_type] pub trait HasAuthTokenType { type AuthToken; } #[cgp_component(ApiClientFieldsGetter)] pub trait HasApiClientFields: HasAuthTokenType { fn api_base_url(&self) -> &String; fn auth_token(&self) -> &Self::AuthToken; } }
While this approach is syntactically valid, it introduces unnecessary coupling. If a provider only needs the api_base_url
, it still inherits a dependency on the auth_token
. This could potentially impact modularity and reusability.
Furthermore, this design prevents you from defining separate, independent providers for api_base_url
and auth_token
, each potentially with its own distinct logic for obtaining that data.
Traits containing only one method also unlock powerful patterns like the UseField
pattern (which we'll introduce later). This pattern significantly simplifies the boilerplate required to implement accessor traits and enables the creation of reusable accessor providers.
Ultimately, CGP provides the flexibility to design your traits in a way that suits your needs. You are not strictly prevented from defining multiple accessor methods within a single trait, or even mixing getters with associated types or other methods.
However, you may find that defining accessor traits that each contain only one getter method is a frequently used pattern, often chosen for its potential benefits in modularity, reusability, and compatibility with helper patterns like UseField. You'll see this pattern frequently demonstrated throughout this book.
Implementing Accessor Providers
Now that we have implemented the provider, we would look at how to implement
a concrete context that uses ReadMessageFromApi
and implement the accessors.
We can implement an ApiClient
context that makes use of all providers
as follows:
#![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::extra::error::RaiseFrom; use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent}; use cgp::prelude::*; use cgp_error_anyhow::{DebugAnyhowError, UseAnyhowError}; use reqwest::blocking::Client; use reqwest::StatusCode; use serde::Deserialize; #[cgp_type] pub trait HasMessageType { type Message; } #[cgp_type] pub trait HasMessageIdType { type MessageId; } #[cgp_type] pub trait HasAuthTokenType { type AuthToken; } #[cgp_component(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; } #[derive(Debug)] pub struct ErrStatusCode { pub status_code: StatusCode, } #[derive(Deserialize)] pub struct ApiMessageResponse { pub message: String, } #[cgp_new_provider] 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_context] pub struct ApiClient { pub api_base_url: String, pub auth_token: String, } delegate_components! { ApiClientComponents { ErrorTypeProviderComponent: UseAnyhowError, ErrorRaiserComponent: UseDelegate<RaiseApiErrors>, MessageIdTypeProviderComponent: UseType<u64>, MessageTypeProviderComponent: UseType<String>, AuthTokenTypeProviderComponent: UseType<String>, MessageQuerierComponent: ReadMessageFromApi, } } delegate_components! { new RaiseApiErrors { reqwest::Error: RaiseFrom, ErrStatusCode: DebugAnyhowError, } } #[cgp_provider] impl ApiBaseUrlGetter<ApiClient> for ApiClientComponents { fn api_base_url(api_client: &ApiClient) -> &String { &api_client.api_base_url } } #[cgp_provider] impl AuthTokenGetter<ApiClient> for ApiClientComponents { fn auth_token(api_client: &ApiClient) -> &String { &api_client.auth_token } } pub trait CanUseApiClient: CanQueryMessage {} impl CanUseApiClient for ApiClient {} }
The ApiClient
context is defined with the fields that we need to implement the accessor traits.
We then have context-specific implementation of ApiBaseUrlGetter
and AuthTokenGetter
to work
directly with ApiClient
. With that, our wiring is completed, and we can check that
ApiClient
implements CanQueryMessage
.