leptos_state/utils/
types.rs

1use std::fmt;
2use thiserror::Error;
3
4/// Result type for leptos-state operations
5pub type StateResult<T> = Result<T, StateError>;
6
7/// Error types for leptos-state operations
8#[derive(Error, Debug, Clone, PartialEq)]
9#[cfg_attr(
10    feature = "serialization",
11    derive(serde::Serialize, serde::Deserialize)
12)]
13pub enum StateError {
14    #[error("Store not found: {name}")]
15    StoreNotFound { name: String },
16
17    #[error("Invalid state transition from {from} to {to}")]
18    InvalidTransition { from: String, to: String },
19
20    #[error("Guard condition failed: {reason}")]
21    GuardFailed { reason: String },
22
23    #[error("Serialization error: {message}")]
24    SerializationError { message: String },
25
26    #[error("Validation error: {field} - {message}")]
27    ValidationError { field: String, message: String },
28
29    #[error("Machine not initialized")]
30    MachineNotInitialized,
31
32    #[error("Context error: {message}")]
33    ContextError { message: String },
34
35    #[error("Unknown error: {message}")]
36    Unknown { message: String },
37}
38
39impl StateError {
40    pub fn store_not_found(name: impl Into<String>) -> Self {
41        Self::StoreNotFound { name: name.into() }
42    }
43
44    pub fn invalid_transition(from: impl Into<String>, to: impl Into<String>) -> Self {
45        Self::InvalidTransition {
46            from: from.into(),
47            to: to.into(),
48        }
49    }
50
51    pub fn guard_failed(reason: impl Into<String>) -> Self {
52        Self::GuardFailed {
53            reason: reason.into(),
54        }
55    }
56
57    pub fn serialization_error(message: impl Into<String>) -> Self {
58        Self::SerializationError {
59            message: message.into(),
60        }
61    }
62
63    pub fn validation_error(field: impl Into<String>, message: impl Into<String>) -> Self {
64        Self::ValidationError {
65            field: field.into(),
66            message: message.into(),
67        }
68    }
69
70    pub fn context_error(message: impl Into<String>) -> Self {
71        Self::ContextError {
72            message: message.into(),
73        }
74    }
75
76    pub fn unknown(message: impl Into<String>) -> Self {
77        Self::Unknown {
78            message: message.into(),
79        }
80    }
81
82    pub fn new(message: impl Into<String>) -> Self {
83        Self::Unknown {
84            message: message.into(),
85        }
86    }
87
88    pub fn custom(message: impl Into<String>) -> Self {
89        Self::Unknown {
90            message: message.into(),
91        }
92    }
93}
94
95// From implementations for common error types
96#[cfg(feature = "serde_json")]
97impl From<serde_json::Error> for StateError {
98    fn from(err: serde_json::Error) -> Self {
99        Self::SerializationError {
100            message: err.to_string(),
101        }
102    }
103}
104
105#[cfg(feature = "serde_yaml")]
106impl From<serde_yaml::Error> for StateError {
107    fn from(err: serde_yaml::Error) -> Self {
108        Self::SerializationError {
109            message: err.to_string(),
110        }
111    }
112}
113
114impl From<std::io::Error> for StateError {
115    fn from(err: std::io::Error) -> Self {
116        Self::Unknown {
117            message: err.to_string(),
118        }
119    }
120}
121
122impl From<Box<dyn std::error::Error + Send + Sync>> for StateError {
123    fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
124        Self::Unknown {
125            message: err.to_string(),
126        }
127    }
128}
129
130/// Type alias for store identifiers
131pub type StoreId = String;
132
133/// Type alias for machine identifiers
134pub type MachineId = String;
135
136/// Type alias for state identifiers
137pub type StateId = String;
138
139/// Type alias for event identifiers
140pub type EventId = String;
141
142/// Subscription handle for cleanup
143pub struct SubscriptionHandle {
144    id: String,
145    cleanup: Option<Box<dyn FnOnce() + std::marker::Send>>,
146}
147
148impl SubscriptionHandle {
149    pub fn new(cleanup: impl FnOnce() + 'static + std::marker::Send) -> Self {
150        Self {
151            id: format!("sub_{}", js_sys::Math::random()),
152            cleanup: Some(Box::new(cleanup)),
153        }
154    }
155
156    pub fn id(&self) -> &str {
157        &self.id
158    }
159
160    pub fn cancel(mut self) {
161        if let Some(cleanup) = self.cleanup.take() {
162            cleanup();
163        }
164    }
165}
166
167impl Drop for SubscriptionHandle {
168    fn drop(&mut self) {
169        if let Some(cleanup) = self.cleanup.take() {
170            cleanup();
171        }
172    }
173}
174
175/// Configuration for stores and machines
176#[derive(Debug, Clone)]
177pub struct Config {
178    pub enable_devtools: bool,
179    pub enable_persistence: bool,
180    pub enable_time_travel: bool,
181    pub enable_logging: bool,
182    pub log_level: LogLevel,
183    pub persistence_key: Option<String>,
184}
185
186impl Default for Config {
187    fn default() -> Self {
188        Self {
189            enable_devtools: cfg!(debug_assertions),
190            enable_persistence: false,
191            enable_time_travel: false,
192            enable_logging: cfg!(debug_assertions),
193            log_level: LogLevel::Info,
194            persistence_key: None,
195        }
196    }
197}
198
199/// Log levels for debugging
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum LogLevel {
202    Trace,
203    Debug,
204    Info,
205    Warn,
206    Error,
207}
208
209impl fmt::Display for LogLevel {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            LogLevel::Trace => write!(f, "TRACE"),
213            LogLevel::Debug => write!(f, "DEBUG"),
214            LogLevel::Info => write!(f, "INFO"),
215            LogLevel::Warn => write!(f, "WARN"),
216            LogLevel::Error => write!(f, "ERROR"),
217        }
218    }
219}
220
221/// Helper trait for creating unique identifiers
222pub trait WithId {
223    fn id(&self) -> String;
224}
225
226/// Helper trait for validation
227pub trait Validate {
228    type Error;
229    fn validate(&self) -> Result<(), Self::Error>;
230}
231
232/// Helper trait for serialization
233pub trait Serialize {
234    fn serialize(&self) -> StateResult<String>;
235}
236
237/// Helper trait for deserialization
238pub trait Deserialize<T> {
239    fn deserialize(data: &str) -> StateResult<T>;
240}
241
242/// Time utilities for delayed transitions and timeouts
243pub mod time {
244    use std::time::{Duration, Instant};
245
246    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
247    pub struct Timeout {
248        duration: Duration,
249        start: Instant,
250    }
251
252    impl Timeout {
253        pub fn new(duration: Duration) -> Self {
254            Self {
255                duration,
256                start: Instant::now(),
257            }
258        }
259
260        pub fn is_expired(&self) -> bool {
261            self.start.elapsed() >= self.duration
262        }
263
264        pub fn remaining(&self) -> Duration {
265            self.duration.saturating_sub(self.start.elapsed())
266        }
267
268        pub fn reset(&mut self) {
269            self.start = Instant::now();
270        }
271    }
272}
273
274/// Collection utilities for managing multiple stores/machines
275pub mod collections {
276    use super::StoreId;
277    use std::collections::HashMap;
278
279    /// Registry for multiple stores
280    #[derive(Debug, Clone)]
281    pub struct StoreRegistry<T> {
282        stores: HashMap<StoreId, T>,
283    }
284
285    impl<T> StoreRegistry<T> {
286        pub fn new() -> Self {
287            Self {
288                stores: HashMap::new(),
289            }
290        }
291
292        pub fn register(&mut self, id: StoreId, store: T) {
293            self.stores.insert(id, store);
294        }
295
296        pub fn get(&self, id: &StoreId) -> Option<&T> {
297            self.stores.get(id)
298        }
299
300        pub fn get_mut(&mut self, id: &StoreId) -> Option<&mut T> {
301            self.stores.get_mut(id)
302        }
303
304        pub fn remove(&mut self, id: &StoreId) -> Option<T> {
305            self.stores.remove(id)
306        }
307
308        pub fn list(&self) -> impl Iterator<Item = &StoreId> {
309            self.stores.keys()
310        }
311
312        pub fn len(&self) -> usize {
313            self.stores.len()
314        }
315
316        pub fn is_empty(&self) -> bool {
317            self.stores.is_empty()
318        }
319    }
320
321    impl<T> Default for StoreRegistry<T> {
322        fn default() -> Self {
323            Self::new()
324        }
325    }
326
327    /// Registry for multiple machines
328    pub type MachineRegistry<T> = StoreRegistry<T>;
329}
330
331#[cfg(feature = "serialization")]
332mod serde_support {
333    use super::StateError;
334    use serde::{Deserialize, Serialize};
335
336    impl<T> super::Serialize for T
337    where
338        T: Serialize,
339    {
340        fn serialize(&self) -> super::StateResult<String> {
341            serde_json::to_string(self).map_err(|e| StateError::serialization_error(e.to_string()))
342        }
343    }
344
345    impl<T> super::Deserialize<T> for T
346    where
347        T: for<'de> Deserialize<'de>,
348    {
349        fn deserialize(data: &str) -> super::StateResult<T> {
350            serde_json::from_str(data).map_err(|e| StateError::serialization_error(e.to_string()))
351        }
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use std::time::Duration;
359
360    #[test]
361    fn state_error_creation() {
362        let error = StateError::store_not_found("test_store");
363        assert!(matches!(error, StateError::StoreNotFound { .. }));
364
365        let error = StateError::invalid_transition("idle", "running");
366        assert!(matches!(error, StateError::InvalidTransition { .. }));
367    }
368
369    #[test]
370    fn config_default() {
371        let config = Config::default();
372        assert_eq!(config.enable_devtools, cfg!(debug_assertions));
373        assert!(!config.enable_persistence);
374    }
375
376    #[test]
377    fn subscription_handle_cleanup() {
378        // Skip this test since it requires WASM-specific functionality
379        // The SubscriptionHandle is designed for WASM environments
380        println!("Skipping subscription handle cleanup test - requires WASM environment");
381    }
382
383    #[test]
384    fn timeout_functionality() {
385        let mut timeout = time::Timeout::new(Duration::from_millis(100));
386        assert!(!timeout.is_expired());
387
388        std::thread::sleep(Duration::from_millis(150));
389        assert!(timeout.is_expired());
390
391        timeout.reset();
392        assert!(!timeout.is_expired());
393    }
394
395    #[test]
396    fn store_registry() {
397        let mut registry = collections::StoreRegistry::new();
398        assert!(registry.is_empty());
399
400        registry.register("store1".to_string(), "value1");
401        assert_eq!(registry.len(), 1);
402
403        assert_eq!(registry.get(&"store1".to_string()), Some(&"value1"));
404        assert_eq!(registry.get(&"store2".to_string()), None);
405
406        let removed = registry.remove(&"store1".to_string());
407        assert_eq!(removed, Some("value1"));
408        assert!(registry.is_empty());
409    }
410}