orbit/component/props/
props.rs

1//! Enhanced props system for Orbit UI components
2//!
3//! This module provides improved props functionality including:
4//! - Type-safe props definitions
5//! - Props validation
6//! - Default values
7//! - Required vs optional props
8
9use std::fmt::{self, Display};
10use std::sync::Arc;
11
12/// Error indicating validation problems with props
13#[derive(Debug, Clone)]
14pub enum PropValidationError {
15    /// A required property was missing
16    MissingRequired(String),
17    /// A property had an invalid value
18    InvalidValue {
19        /// Name of the property
20        name: String,
21        /// Description of the validation error
22        reason: String,
23    },
24    /// A property had a type mismatch
25    TypeMismatch {
26        /// Name of the property
27        name: String,
28        /// Expected type
29        expected: String,
30        /// Actual type
31        actual: String,
32    },
33    /// Multiple validation errors
34    Multiple(Vec<PropValidationError>),
35}
36
37impl Display for PropValidationError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            PropValidationError::MissingRequired(name) => {
41                write!(f, "Missing required property: {name}")
42            }
43            PropValidationError::InvalidValue { name, reason } => {
44                write!(f, "Invalid value for property {name}: {reason}")
45            }
46            PropValidationError::TypeMismatch {
47                name,
48                expected,
49                actual,
50            } => write!(
51                f,
52                "Type mismatch for property {name}: expected {expected}, got {actual}"
53            ),
54            PropValidationError::Multiple(errors) => {
55                writeln!(f, "Multiple validation errors:")?;
56                for (i, error) in errors.iter().enumerate() {
57                    writeln!(f, "  {}. {}", i + 1, error)?;
58                }
59                Ok(())
60            }
61        }
62    }
63}
64
65impl std::error::Error for PropValidationError {}
66
67/// Trait for props validation
68pub trait PropValidator<P> {
69    /// Validate the props
70    fn validate(&self, props: &P) -> Result<(), PropValidationError>;
71}
72
73/// Represents a property that can be required or optional with default value
74pub enum PropValue<T> {
75    /// Value is present
76    Value(T),
77    /// Value is not present, but there is a default
78    Default(fn() -> T),
79    /// Value is required but not present
80    Required,
81}
82
83impl<T: Clone> PropValue<T> {
84    /// Get the value, returning default if needed
85    pub fn get(&self) -> Result<T, PropValidationError> {
86        match self {
87            PropValue::Value(val) => Ok(val.clone()),
88            PropValue::Default(default_fn) => Ok(default_fn()),
89            PropValue::Required => Err(PropValidationError::MissingRequired(
90                "Property is required".to_string(),
91            )),
92        }
93    }
94
95    /// Create a new value
96    pub fn new(value: T) -> Self {
97        PropValue::Value(value)
98    }
99
100    /// Create a new optional value with default
101    pub fn new_default(default_fn: fn() -> T) -> Self {
102        PropValue::Default(default_fn)
103    }
104
105    /// Create a new required value
106    pub fn new_required() -> Self {
107        PropValue::Required
108    }
109
110    /// Set the value
111    pub fn set(&mut self, value: T) {
112        *self = PropValue::Value(value);
113    }
114
115    /// Check if value is present
116    pub fn is_set(&self) -> bool {
117        matches!(self, PropValue::Value(_))
118    }
119}
120
121/// Builder for constructing component props with validation
122pub struct PropsBuilder<P> {
123    /// The props being built
124    props: P,
125    /// Optional validator to run before completion
126    validator: Option<Arc<dyn PropValidator<P> + Send + Sync>>,
127}
128
129impl<P> PropsBuilder<P> {
130    /// Create a new props builder
131    pub fn new(props: P) -> Self {
132        Self {
133            props,
134            validator: None,
135        }
136    }
137
138    /// Set a validator for the props
139    pub fn with_validator<V>(mut self, validator: V) -> Self
140    where
141        V: PropValidator<P> + Send + Sync + 'static,
142    {
143        self.validator = Some(Arc::new(validator));
144        self
145    }
146
147    /// Build the props, running validation
148    pub fn build(self) -> Result<P, PropValidationError> {
149        if let Some(validator) = self.validator {
150            validator.validate(&self.props)?;
151        }
152        Ok(self.props)
153    }
154}
155
156/// A simple property validator that can be composed of multiple validators
157pub struct CompositeValidator<P> {
158    /// The validators to run
159    validators: Vec<Arc<dyn PropValidator<P> + Send + Sync>>,
160}
161
162impl<P> CompositeValidator<P> {
163    /// Create a new composite validator
164    pub fn new() -> Self {
165        Self {
166            validators: Vec::new(),
167        }
168    }
169
170    /// Add a validator to the composite
171    pub fn add<V>(&mut self, validator: V)
172    where
173        V: PropValidator<P> + Send + Sync + 'static,
174    {
175        self.validators.push(Arc::new(validator));
176    }
177}
178
179impl<P> PropValidator<P> for CompositeValidator<P> {
180    fn validate(&self, props: &P) -> Result<(), PropValidationError> {
181        let mut errors = Vec::new();
182
183        for validator in &self.validators {
184            if let Err(err) = validator.validate(props) {
185                errors.push(err);
186            }
187        }
188
189        if errors.is_empty() {
190            Ok(())
191        } else if errors.len() == 1 {
192            Err(errors.remove(0))
193        } else {
194            Err(PropValidationError::Multiple(errors))
195        }
196    }
197}
198
199impl<P> Default for CompositeValidator<P> {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205/// Type-safe builder for props
206#[macro_export]
207macro_rules! define_props {
208    (
209        $(#[$struct_meta:meta])*
210        $vis:vis struct $name:ident {
211            $(
212                $(#[$field_meta:meta])*
213                $field_vis:vis $field_name:ident: $field_type:ty
214            ),*
215            $(,)?
216        }
217    ) => {
218        $(#[$struct_meta])*
219        $vis struct $name {
220            $(
221                $(#[$field_meta])*
222                $field_vis $field_name: $field_type,
223            )*
224        }
225
226        impl $name {
227            /// Create a new props builder
228            pub fn builder() -> $crate::component::props::PropsBuilder<Self> {
229                let props = Self {
230                    $(
231                        $field_name: Default::default(),
232                    )*
233                };
234                $crate::component::props::PropsBuilder::new(props)
235            }
236
237            // Generate setter methods for each field
238            $(
239                #[allow(dead_code)]
240                pub fn $field_name(mut self, value: $field_type) -> Self {
241                    self.$field_name = value;
242                    self
243                }
244            )*
245        }
246
247        impl Default for $name {
248            fn default() -> Self {
249                Self {
250                    $(
251                        $field_name: Default::default(),
252                    )*
253                }
254            }
255        }
256    };
257}
258
259// Export the paste crate for the macro to use
260pub use paste;
261
262/// Creates a validator that ensures a field meets a condition
263#[macro_export]
264macro_rules! validate_field {
265    ($props_type:ty, $field:ident, $condition:expr, $message:expr) => {
266        struct FieldValidator<T>(std::marker::PhantomData<T>);
267
268        impl $crate::component::props::PropValidator<$props_type> for FieldValidator<$props_type> {
269            fn validate(
270                &self,
271                props: &$props_type,
272            ) -> Result<(), $crate::component::props::PropValidationError> {
273                if !$condition(&props.$field) {
274                    return Err(
275                        $crate::component::props::PropValidationError::InvalidValue {
276                            name: stringify!($field).to_string(),
277                            reason: $message.to_string(),
278                        },
279                    );
280                }
281                Ok(())
282            }
283        }
284
285        FieldValidator::<$props_type>(std::marker::PhantomData)
286    };
287}
288
289/// Advanced props builder with validation and required field support
290#[macro_export]
291macro_rules! define_props_advanced {
292    (
293        $(#[$struct_meta:meta])*
294        $vis:vis struct $name:ident {
295            $(
296                $(#[required])?
297                $(#[$field_meta:meta])*
298                $field_vis:vis $field_name:ident: $field_type:ty
299                $(= $default:expr)?
300            ),*
301            $(,)?
302        }
303    ) => {
304        // First define the basic struct
305        define_props! {
306            $(#[$struct_meta])*
307            $vis struct $name {
308                $(
309                    $(#[$field_meta])*
310                    $field_vis $field_name: $field_type
311                ),*
312            }
313        }
314
315        // Generate builder struct with validation
316        paste::paste! {
317            pub struct [<$name Builder>] {
318                $(
319                    $field_name: $crate::component::props::PropValue<$field_type>,
320                )*
321            }
322
323            impl [<$name Builder>] {
324                pub fn new() -> Self {
325                    Self {
326                        $(
327                            $field_name: define_props_advanced!(@field_init $field_name $(#[required])? $(= $default)?),
328                        )*
329                    }
330                }
331
332                $(
333                    pub fn $field_name(mut self, value: $field_type) -> Self {
334                        self.$field_name.set(value);
335                        self
336                    }
337                )*
338
339                pub fn build(self) -> Result<$name, $crate::component::props::PropValidationError> {
340                    let mut errors = Vec::new();
341
342                    $(
343                        let $field_name = match self.$field_name.get() {
344                            Ok(val) => val,
345                            Err(_) => {
346                                // Check if this field was marked as required
347                                if define_props_advanced!(@is_required $field_name $(#[required])?) {
348                                    errors.push($crate::component::props::PropValidationError::MissingRequired(
349                                        stringify!($field_name).to_string()
350                                    ));
351                                }
352                                Default::default()
353                            }
354                        };
355                    )*
356
357                    if errors.is_empty() {
358                        Ok($name {
359                            $(
360                                $field_name,
361                            )*
362                        })
363                    } else if errors.len() == 1 {
364                        Err(errors.remove(0))
365                    } else {
366                        Err($crate::component::props::PropValidationError::Multiple(errors))
367                    }
368                }
369            }
370
371            impl Default for [<$name Builder>] {
372                fn default() -> Self {
373                    Self::new()
374                }
375            }
376        }
377    };
378
379    // Helper to initialize fields based on whether they're required or have defaults
380    (@field_init $field_name:ident #[required] = $default:expr) => {
381        $crate::component::props::PropValue::new_required()
382    };
383    (@field_init $field_name:ident #[required]) => {
384        $crate::component::props::PropValue::new_required()
385    };
386    (@field_init $field_name:ident = $default:expr) => {
387        $crate::component::props::PropValue::new_default(|| $default)
388    };
389    (@field_init $field_name:ident) => {
390        $crate::component::props::PropValue::new_default(Default::default)
391    };
392
393    // Helper to check if a field is required
394    (@is_required $field_name:ident #[required]) => { true };
395    (@is_required $field_name:ident) => { false };
396}