1
2use std::marker::PhantomData;
3
4use serde::de::DeserializeOwned;
5use serde::{Deserialize, Serialize};
6
7use crate::ForgeClient;
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
10pub struct ForgeError {
11 pub code: String,
12 pub message: String,
13 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub details: Option<serde_json::Value>,
15}
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct ForgeClientError {
19 pub code: String,
20 pub message: String,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub details: Option<serde_json::Value>,
23}
24
25impl ForgeClientError {
26 pub fn new(
27 code: impl Into<String>,
28 message: impl Into<String>,
29 details: Option<serde_json::Value>,
30 ) -> Self {
31 Self {
32 code: code.into(),
33 message: message.into(),
34 details,
35 }
36 }
37
38 pub fn as_forge_error(&self) -> ForgeError {
39 ForgeError {
40 code: self.code.clone(),
41 message: self.message.clone(),
42 details: self.details.clone(),
43 }
44 }
45}
46
47impl std::fmt::Display for ForgeClientError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 write!(f, "{}: {}", self.code, self.message)
50 }
51}
52
53impl std::error::Error for ForgeClientError {}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
56#[serde(rename_all = "snake_case")]
57pub enum ConnectionState {
58 #[default]
59 Disconnected,
60 Connecting,
61 Connected,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct QueryState<T> {
66 pub loading: bool,
67 pub data: Option<T>,
68 pub error: Option<ForgeError>,
69}
70
71impl<T> Default for QueryState<T> {
72 fn default() -> Self {
73 Self {
74 loading: true,
75 data: None,
76 error: None,
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct SubscriptionState<T> {
83 pub loading: bool,
84 pub data: Option<T>,
85 pub error: Option<ForgeError>,
86 pub stale: bool,
87 pub connection_state: ConnectionState,
88}
89
90impl<T> Default for SubscriptionState<T> {
91 fn default() -> Self {
92 Self {
93 loading: true,
94 data: None,
95 error: None,
96 stale: false,
97 connection_state: ConnectionState::Disconnected,
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(rename_all = "snake_case")]
104pub enum JobStatus {
105 Pending,
106 Claimed,
107 Running,
108 Completed,
109 Retry,
110 Failed,
111 DeadLetter,
112 CancelRequested,
113 Cancelled,
114 NotFound,
115}
116
117impl Default for JobStatus {
118 fn default() -> Self {
119 Self::Pending
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124pub struct JobState<TOutput> {
125 pub job_id: String,
126 pub status: JobStatus,
127 pub progress: Option<f64>,
128 pub message: Option<String>,
129 pub output: Option<TOutput>,
130 pub error: Option<String>,
131}
132
133impl<TOutput> Default for JobState<TOutput> {
134 fn default() -> Self {
135 Self {
136 job_id: String::new(),
137 status: JobStatus::Pending,
138 progress: None,
139 message: None,
140 output: None,
141 error: None,
142 }
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub struct JobExecutionState<TOutput> {
148 pub loading: bool,
149 pub connection_state: ConnectionState,
150 pub state: JobState<TOutput>,
151}
152
153impl<TOutput> Default for JobExecutionState<TOutput> {
154 fn default() -> Self {
155 Self {
156 loading: true,
157 connection_state: ConnectionState::Disconnected,
158 state: JobState::default(),
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(rename_all = "snake_case")]
165pub enum WorkflowStatus {
166 Created,
167 Running,
168 Waiting,
169 Completed,
170 Compensating,
171 Compensated,
172 Failed,
173 NotFound,
174}
175
176impl Default for WorkflowStatus {
177 fn default() -> Self {
178 Self::Created
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub struct WorkflowStepState {
185 pub name: String,
186 pub status: String,
187 pub error: Option<String>,
188}
189
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191pub struct WorkflowState<TOutput> {
192 pub workflow_id: String,
193 pub status: WorkflowStatus,
194 pub step: Option<String>,
195 pub waiting_for: Option<String>,
196 pub steps: Vec<WorkflowStepState>,
197 pub output: Option<TOutput>,
198 pub error: Option<String>,
199}
200
201impl<TOutput> Default for WorkflowState<TOutput> {
202 fn default() -> Self {
203 Self {
204 workflow_id: String::new(),
205 status: WorkflowStatus::Created,
206 step: None,
207 waiting_for: None,
208 steps: Vec::new(),
209 output: None,
210 error: None,
211 }
212 }
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216pub struct WorkflowExecutionState<TOutput> {
217 pub loading: bool,
218 pub connection_state: ConnectionState,
219 pub state: WorkflowState<TOutput>,
220}
221
222impl<TOutput> Default for WorkflowExecutionState<TOutput> {
223 fn default() -> Self {
224 Self {
225 loading: true,
226 connection_state: ConnectionState::Disconnected,
227 state: WorkflowState::default(),
228 }
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct TokenPair {
235 pub access_token: String,
236 pub refresh_token: String,
237}
238
239#[derive(Clone)]
242pub struct Mutation<A, R> {
243 client: ForgeClient,
244 function_name: &'static str,
245 _phantom: PhantomData<fn(A) -> R>,
246}
247
248impl<A, R> Mutation<A, R>
249where
250 A: Serialize + 'static,
251 R: DeserializeOwned + 'static,
252{
253 pub(crate) fn new(client: ForgeClient, function_name: &'static str) -> Self {
254 Self {
255 client,
256 function_name,
257 _phantom: PhantomData,
258 }
259 }
260
261 pub async fn call(&self, args: A) -> Result<R, ForgeClientError> {
262 self.client.call(self.function_name, args).await
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use serde_json::json;
270
271 #[test]
272 fn client_error_as_forge_error_preserves_code_message_and_details() {
273 let err = ForgeClientError::new(
274 "VALIDATION",
275 "Name is required",
276 Some(json!({"field": "name"})),
277 );
278
279 assert_eq!(
280 err.as_forge_error(),
281 ForgeError {
282 code: "VALIDATION".into(),
283 message: "Name is required".into(),
284 details: Some(json!({"field": "name"})),
285 }
286 );
287 }
288
289 #[test]
290 fn subscription_state_default_is_loading_and_disconnected() {
291 let state = SubscriptionState::<Vec<String>>::default();
292
293 assert!(state.loading);
294 assert_eq!(state.data, None);
295 assert_eq!(state.error, None);
296 assert!(!state.stale);
297 assert_eq!(state.connection_state, ConnectionState::Disconnected);
298 }
299
300 #[test]
301 fn job_and_workflow_status_serialize_in_snake_case() {
302 assert_eq!(serde_json::to_string(&JobStatus::CancelRequested).unwrap(), "\"cancel_requested\"");
303 assert_eq!(serde_json::to_string(&WorkflowStatus::NotFound).unwrap(), "\"not_found\"");
304 }
305
306 #[test]
307 fn query_and_subscription_state_defaults_are_safe_for_initial_render() {
308 let query = QueryState::<Vec<String>>::default();
309 let subscription = SubscriptionState::<Vec<String>>::default();
310
311 assert!(query.loading);
312 assert!(query.data.is_none());
313 assert!(query.error.is_none());
314
315 assert!(subscription.loading);
316 assert!(subscription.data.is_none());
317 assert!(subscription.error.is_none());
318 assert!(!subscription.stale);
319 assert_eq!(subscription.connection_state, ConnectionState::Disconnected);
320 }
321
322 #[test]
323 fn job_and_workflow_execution_state_defaults_start_disconnected() {
324 let job = JobExecutionState::<serde_json::Value>::default();
325 let workflow = WorkflowExecutionState::<serde_json::Value>::default();
326
327 assert!(job.loading);
328 assert_eq!(job.connection_state, ConnectionState::Disconnected);
329 assert_eq!(job.state.status, JobStatus::Pending);
330
331 assert!(workflow.loading);
332 assert_eq!(workflow.connection_state, ConnectionState::Disconnected);
333 assert_eq!(workflow.state.status, WorkflowStatus::Created);
334 }
335}
336
337#[derive(Debug, Clone)]
338pub enum StreamEvent<T> {
339 Connection(ConnectionState),
340 Data(T),
341 Error(ForgeClientError),
342}
343
344#[derive(Debug, Clone, Deserialize)]
345pub(crate) struct RpcEnvelopeRaw {
346 pub success: bool,
347 #[serde(default)]
348 pub data: Option<serde_json::Value>,
349 #[serde(default)]
350 pub error: Option<ForgeError>,
351}
352
353#[derive(Debug, Clone, Deserialize)]
354pub(crate) struct ConnectedEvent {
355 pub session_id: Option<String>,
356 pub session_secret: Option<String>,
357}
358
359#[derive(Debug, Clone, Deserialize)]
360pub(crate) struct SseEnvelopeRaw {
361 #[serde(default)]
362 pub target: Option<String>,
363 #[serde(default)]
364 pub payload: Option<serde_json::Value>,
365 #[serde(default)]
366 pub code: Option<String>,
367 #[serde(default)]
368 pub message: Option<String>,
369}