actor_sample/clients/
mod.rs

1//! # Domain-Specific Client Wrappers
2//!
3//! This module provides **type-safe, domain-specific wrappers** around the generic
4//! [`ResourceClient`](actor_framework::ResourceClient). These wrappers add domain
5//! knowledge and ergonomic APIs on top of the raw framework client.
6//!
7//! ## The Client Wrapper Pattern
8//!
9//! Instead of exposing the generic `ResourceClient<T>` directly, we wrap it in
10//! domain-specific clients that provide:
11//!
12//! 1. **Domain-specific methods** - `create_user()` instead of generic `create()`
13//! 2. **Type-safe errors** - `UserError` instead of generic `FrameworkError`
14//! 3. **Business logic** - Validation, transformation, orchestration
15//! 4. **Better API ergonomics** - Hide framework details from consumers
16//!
17//! ## Example: UserClient
18//!
19//! ```rust
20//! use actor_framework::ResourceClient;
21//! use actor_sample::model::{User, UserCreate, UserId};
22//! use actor_sample::user_actor::UserError;
23//!
24//! #[derive(Clone)]
25//! pub struct UserClient {
26//!     inner: ResourceClient<User>,
27//! }
28//!
29//! impl UserClient {
30//!     pub fn new(inner: ResourceClient<User>) -> Self {
31//!         Self { inner }
32//!     }
33//!
34//!     // Domain-specific method with type-safe errors
35//!     pub async fn create_user(&self, params: UserCreate) -> Result<UserId, UserError> {
36//!         self.inner.create(params).await
37//!             .map_err(|e| UserError::ActorCommunicationError(e.to_string()))
38//!     }
39//! }
40//! ```
41//!
42//! ## The ActorClient Trait
43//!
44//! The [`actor_client::ActorClient`] trait provides a common interface for all clients,
45//! automatically implementing `get()` and `delete()` methods:
46//!
47//! ```rust,ignore
48//! #[async_trait]
49//! impl ActorClient<User> for UserClient {
50//!     type Error = UserError;
51//!
52//!     fn inner(&self) -> &ResourceClient<User> {
53//!         &self.inner
54//!     }
55//!
56//!     fn map_error(e: FrameworkError) -> Self::Error {
57//!         UserError::ActorCommunicationError(e.to_string())
58//!     }
59//! }
60//! ```
61//!
62//! Now `UserClient` automatically gets:
63//! - `async fn get(&self, id: String) -> Result<Option<User>, UserError>`
64//! - `async fn delete(&self, id: String) -> Result<(), UserError>`
65//!
66//! ## Type-Safe Error Mapping
67//!
68//! Each client maps framework errors to domain-specific error types:
69//!
70//! ```rust,ignore
71//! // Framework error (generic)
72//! FrameworkError::Timeout
73//!
74//! // Mapped to domain error (specific)
75//! UserError::ActorCommunicationError("timeout".to_string())
76//! ```
77//!
78//! This allows consumers to pattern match on domain-specific errors:
79//!
80//! ```rust,ignore
81//! match user_client.get(id).await {
82//!     Ok(Some(user)) => println!("Found: {}", user.name),
83//!     Ok(None) => println!("User not found"),
84//!     Err(UserError::ActorCommunicationError(msg)) => {
85//!         println!("Communication failed: {}", msg)
86//!     }
87//!     Err(e) => println!("Other error: {}", e),
88//! }
89//! ```
90//!
91//! ## Orchestration Example: OrderClient
92//!
93//! Clients can orchestrate multiple actors to implement complex workflows:
94//!
95//! ```rust,ignore
96//! impl OrderClient {
97//!     pub async fn create_order(&self, params: OrderCreate) -> Result<String, OrderError> {
98//!         // 1. Validate user exists
99//!         let user = self.user_client.get(params.user_id.clone()).await?
100//!             .ok_or_else(|| OrderError::InvalidUser(params.user_id.clone()))?;
101//!
102//!         // 2. Reserve product stock
103//!         self.product_client.reserve_stock(
104//!             params.product_id.clone(),
105//!             params.quantity
106//!         ).await?;
107//!
108//!         // 3. Create the order
109//!         match self.inner.create(params.clone()).await {
110//!             Ok(id) => Ok(id),
111//!             Err(e) => {
112//!                 // COMPENSATING TRANSACTION: Rollback stock reservation
113//!                 // If we fail to create the order, we must release the stock
114//!                 // so it doesn't get "leaked" (permanently reserved).
115//!                 let _ = self.product_client.release_stock(
116//!                     params.product_id,
117//!                     params.quantity
118//!                 ).await;
119//!                 
120//!                 Err(OrderError::ActorCommunicationError(e.to_string()))
121//!             }
122//!         }
123//!     }
124//! }
125//! ```
126//!
127//! This keeps orchestration logic in the **client layer**, while actors remain
128//! focused on managing their own state.
129//!
130//! ## Benefits
131//!
132//! 1. **Type Safety** - Compile-time guarantees for domain operations
133//! 2. **Encapsulation** - Hide framework details from consumers
134//! 3. **Testability** - Easy to mock with [`MockClient`](actor_framework::mock::MockClient)
135//! 4. **Maintainability** - Domain logic lives in one place
136//! 5. **Discoverability** - IDE autocomplete shows domain methods
137
138pub mod order_client;
139pub mod product_client;
140pub mod user_client;
141
142pub use order_client::*;
143pub use product_client::*;
144pub use user_client::*;