actor_sample/clients/
product_client.rs1use crate::model::{Product, ProductId};
6use crate::product_actor::ProductError;
7use actor_framework::ActorClient;
8use actor_framework::{FrameworkError, ResourceClient};
9use async_trait::async_trait;
10use tracing::{debug, instrument};
11
12#[derive(Clone)]
14pub struct ProductClient {
15 inner: ResourceClient<Product>,
16}
17
18impl ProductClient {
19 pub fn new(inner: ResourceClient<Product>) -> Self {
20 Self { inner }
21 }
22}
23
24#[async_trait]
25impl ActorClient<Product> for ProductClient {
26 type Error = ProductError;
27
28 fn inner(&self) -> &ResourceClient<Product> {
29 &self.inner
30 }
31
32 fn map_error(e: FrameworkError) -> Self::Error {
33 ProductError::ActorCommunicationError(e.to_string())
34 }
35}
36
37impl ProductClient {
38 #[instrument(skip(self))]
41 pub async fn create_product(
42 &self,
43 params: crate::model::ProductCreate,
44 ) -> Result<ProductId, ProductError> {
45 debug!("Sending request");
46 self.inner
47 .create(params)
48 .await
49 .map_err(|e| ProductError::ActorCommunicationError(e.to_string()))
50 }
51
52 #[instrument(skip(self))]
56 #[allow(dead_code)]
57 pub async fn check_stock(&self, id: ProductId) -> Result<u32, ProductError> {
58 debug!("Checking stock for product {}", id);
59 use crate::product_actor::{ProductAction, ProductActionResult};
60 match self
61 .inner
62 .perform_action(id, ProductAction::CheckStock)
63 .await
64 {
65 Ok(ProductActionResult::CheckStock(level)) => Ok(level),
66 Ok(_) => unreachable!("CheckStock action must return CheckStock result"),
67 Err(e) => Err(ProductError::ActorCommunicationError(e.to_string())),
68 }
69 }
70
71 #[instrument(skip(self))]
75 pub async fn reserve_stock(&self, id: ProductId, quantity: u32) -> Result<(), ProductError> {
76 debug!("Reserving {} units for product {}", quantity, id);
77 use crate::product_actor::{ProductAction, ProductActionResult};
78 match self
79 .inner
80 .perform_action(id, ProductAction::ReserveStock(quantity))
81 .await
82 {
83 Ok(ProductActionResult::ReserveStock(())) => Ok(()),
84 Ok(_) => unreachable!("ReserveStock action must return ReserveStock result"),
85 Err(e) => Err(ProductError::ActorCommunicationError(e.to_string())),
86 }
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::product_actor::{ProductAction, ProductActionResult};
94 use actor_framework::mock::{create_mock_client, expect_action};
95
96 #[tokio::test]
97 async fn test_check_stock_returns_correct_level() {
98 let (client, mut receiver) = create_mock_client::<Product>(10);
99 let product_client = ProductClient::new(client);
100
101 let check_task =
103 tokio::spawn(async move { product_client.check_stock(ProductId(1)).await });
104
105 let (id, action, responder) = expect_action(&mut receiver)
107 .await
108 .expect("Expected Action request");
109
110 assert_eq!(id, ProductId(1));
111 assert!(matches!(action, ProductAction::CheckStock));
112
113 responder
115 .send(Ok(ProductActionResult::CheckStock(42)))
116 .unwrap();
117
118 let result = check_task.await.unwrap();
120 assert_eq!(result.unwrap(), 42);
121 }
122
123 #[tokio::test]
124 async fn test_reserve_stock_success() {
125 let (client, mut receiver) = create_mock_client::<Product>(10);
126 let product_client = ProductClient::new(client);
127
128 let reserve_task =
130 tokio::spawn(async move { product_client.reserve_stock(ProductId(1), 5).await });
131
132 let (id, action, responder) = expect_action(&mut receiver)
134 .await
135 .expect("Expected Action request");
136
137 assert_eq!(id, ProductId(1));
138 match action {
139 ProductAction::ReserveStock(amount) => assert_eq!(amount, 5),
140 _ => panic!("Expected ReserveStock action"),
141 }
142
143 responder
145 .send(Ok(ProductActionResult::ReserveStock(())))
146 .unwrap();
147
148 let result = reserve_task.await.unwrap();
150 assert!(result.is_ok());
151 }
152
153 #[tokio::test]
154 async fn test_reserve_stock_insufficient_stock() {
155 let (client, mut receiver) = create_mock_client::<Product>(10);
156 let product_client = ProductClient::new(client);
157
158 let reserve_task =
160 tokio::spawn(async move { product_client.reserve_stock(ProductId(1), 100).await });
161
162 let (id, action, responder) = expect_action(&mut receiver)
164 .await
165 .expect("Expected Action request");
166
167 assert_eq!(id, ProductId(1));
168 match action {
169 ProductAction::ReserveStock(amount) => assert_eq!(amount, 100),
170 _ => panic!("Expected ReserveStock action"),
171 }
172
173 use actor_framework::FrameworkError;
175 responder
176 .send(Err(FrameworkError::EntityError(Box::new(
177 std::io::Error::other("Stock check failed"),
178 ))))
179 .unwrap();
180
181 let result = reserve_task.await.unwrap();
183 assert!(result.is_err());
184 match result {
185 Err(ProductError::ActorCommunicationError(msg)) => {
186 assert!(msg.contains("Stock check failed") || msg.contains("Entity error"));
188 }
189 _ => panic!("Expected ActorCommunicationError"),
190 }
191 }
192
193 #[test]
194 fn test_type_safety_compile_time() {
195 }
205}