Skip to main content

forge_dioxus/
types.rs

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/// An access token + refresh token pair returned by auth endpoints.
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct TokenPair {
235    pub access_token: String,
236    pub refresh_token: String,
237}
238
239/// Mutation handle returned by `use_forge_mutation`. Clone into event handlers,
240/// call `.call(args)` to execute.
241#[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}