Expand description
§Domain-Specific Client Wrappers
This module provides type-safe, domain-specific wrappers around the generic
ResourceClient. These wrappers add domain
knowledge and ergonomic APIs on top of the raw framework client.
§The Client Wrapper Pattern
Instead of exposing the generic ResourceClient<T> directly, we wrap it in
domain-specific clients that provide:
- Domain-specific methods -
create_user()instead of genericcreate() - Type-safe errors -
UserErrorinstead of genericFrameworkError - Business logic - Validation, transformation, orchestration
- Better API ergonomics - Hide framework details from consumers
§Example: UserClient
use actor_framework::ResourceClient;
use actor_sample::model::{User, UserCreate, UserId};
use actor_sample::user_actor::UserError;
#[derive(Clone)]
pub struct UserClient {
inner: ResourceClient<User>,
}
impl UserClient {
pub fn new(inner: ResourceClient<User>) -> Self {
Self { inner }
}
// Domain-specific method with type-safe errors
pub async fn create_user(&self, params: UserCreate) -> Result<UserId, UserError> {
self.inner.create(params).await
.map_err(|e| UserError::ActorCommunicationError(e.to_string()))
}
}§The ActorClient Trait
The [actor_client::ActorClient] trait provides a common interface for all clients,
automatically implementing get() and delete() methods:
#[async_trait]
impl ActorClient<User> for UserClient {
type Error = UserError;
fn inner(&self) -> &ResourceClient<User> {
&self.inner
}
fn map_error(e: FrameworkError) -> Self::Error {
UserError::ActorCommunicationError(e.to_string())
}
}Now UserClient automatically gets:
async fn get(&self, id: String) -> Result<Option<User>, UserError>async fn delete(&self, id: String) -> Result<(), UserError>
§Type-Safe Error Mapping
Each client maps framework errors to domain-specific error types:
// Framework error (generic)
FrameworkError::Timeout
// Mapped to domain error (specific)
UserError::ActorCommunicationError("timeout".to_string())This allows consumers to pattern match on domain-specific errors:
match user_client.get(id).await {
Ok(Some(user)) => println!("Found: {}", user.name),
Ok(None) => println!("User not found"),
Err(UserError::ActorCommunicationError(msg)) => {
println!("Communication failed: {}", msg)
}
Err(e) => println!("Other error: {}", e),
}§Orchestration Example: OrderClient
Clients can orchestrate multiple actors to implement complex workflows:
impl OrderClient {
pub async fn create_order(&self, params: OrderCreate) -> Result<String, OrderError> {
// 1. Validate user exists
let user = self.user_client.get(params.user_id.clone()).await?
.ok_or_else(|| OrderError::InvalidUser(params.user_id.clone()))?;
// 2. Reserve product stock
self.product_client.reserve_stock(
params.product_id.clone(),
params.quantity
).await?;
// 3. Create the order
match self.inner.create(params.clone()).await {
Ok(id) => Ok(id),
Err(e) => {
// COMPENSATING TRANSACTION: Rollback stock reservation
// If we fail to create the order, we must release the stock
// so it doesn't get "leaked" (permanently reserved).
let _ = self.product_client.release_stock(
params.product_id,
params.quantity
).await;
Err(OrderError::ActorCommunicationError(e.to_string()))
}
}
}
}This keeps orchestration logic in the client layer, while actors remain focused on managing their own state.
§Benefits
- Type Safety - Compile-time guarantees for domain operations
- Encapsulation - Hide framework details from consumers
- Testability - Easy to mock with
MockClient - Maintainability - Domain logic lives in one place
- Discoverability - IDE autocomplete shows domain methods
Re-exports§
pub use order_client::*;pub use product_client::*;pub use user_client::*;
Modules§
- order_
client - Order Client
- product_
client - Product Client
- user_
client - User Client