Skip to main content

presentar_core/
binding.rs

1//! Interactive state binding for reactive UI.
2//!
3//! This module provides mechanisms to bind UI widget properties to application
4//! state, enabling declarative data flow and automatic UI updates.
5//!
6//! # Binding Types
7//!
8//! - `Binding<T>` - Two-way binding for read/write access
9//! - `Derived<T>` - Read-only computed value from state
10//! - `PropertyPath` - Path to a property in state (e.g., "user.name")
11//!
12//! # Example
13//!
14//! ```ignore
15//! use presentar_core::binding::{Binding, Derived, PropertyPath};
16//!
17//! // Create a binding to state.count
18//! let count_binding = Binding::new(|| state.count, |v| state.count = v);
19//!
20//! // Create a derived value
21//! let doubled = Derived::new(|| state.count * 2);
22//! ```
23
24use serde::{Deserialize, Serialize};
25use std::any::Any;
26use std::fmt;
27use std::sync::{Arc, RwLock};
28
29/// Type alias for subscriber callbacks.
30type SubscriberFn<T> = Box<dyn Fn(&T) + Send + Sync>;
31
32/// Type alias for subscribers list.
33type Subscribers<T> = Arc<RwLock<Vec<SubscriberFn<T>>>>;
34
35/// A property path for accessing nested state.
36///
37/// Property paths use dot notation: "user.profile.name"
38#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
39pub struct PropertyPath {
40    segments: Vec<String>,
41}
42
43impl PropertyPath {
44    /// Create a new property path from a string.
45    #[must_use]
46    pub fn new(path: &str) -> Self {
47        let segments = path
48            .split('.')
49            .filter(|s| !s.is_empty())
50            .map(String::from)
51            .collect();
52        Self { segments }
53    }
54
55    /// Create an empty root path.
56    #[must_use]
57    pub const fn root() -> Self {
58        Self {
59            segments: Vec::new(),
60        }
61    }
62
63    /// Get path segments.
64    #[must_use]
65    pub fn segments(&self) -> &[String] {
66        &self.segments
67    }
68
69    /// Check if path is empty (root).
70    #[must_use]
71    pub fn is_root(&self) -> bool {
72        self.segments.is_empty()
73    }
74
75    /// Get the number of segments.
76    #[must_use]
77    pub fn len(&self) -> usize {
78        self.segments.len()
79    }
80
81    /// Check if path is empty.
82    #[must_use]
83    pub fn is_empty(&self) -> bool {
84        self.segments.is_empty()
85    }
86
87    /// Append a segment to the path.
88    #[must_use]
89    pub fn join(&self, segment: &str) -> Self {
90        let mut segments = self.segments.clone();
91        segments.push(segment.to_string());
92        Self { segments }
93    }
94
95    /// Get the parent path.
96    #[must_use]
97    pub fn parent(&self) -> Option<Self> {
98        if self.segments.is_empty() {
99            None
100        } else {
101            let mut segments = self.segments.clone();
102            segments.pop();
103            Some(Self { segments })
104        }
105    }
106
107    /// Get the last segment (leaf name).
108    #[must_use]
109    pub fn leaf(&self) -> Option<&str> {
110        self.segments.last().map(String::as_str)
111    }
112
113    /// Convert to string representation.
114    #[must_use]
115    pub fn to_string_path(&self) -> String {
116        self.segments.join(".")
117    }
118}
119
120impl fmt::Display for PropertyPath {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        write!(f, "{}", self.to_string_path())
123    }
124}
125
126impl From<&str> for PropertyPath {
127    fn from(s: &str) -> Self {
128        Self::new(s)
129    }
130}
131
132/// Binding direction for property connections.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
134pub enum BindingDirection {
135    /// One-way binding: state → widget
136    #[default]
137    OneWay,
138    /// Two-way binding: state ↔ widget
139    TwoWay,
140    /// One-time binding: state → widget (initial only)
141    OneTime,
142}
143
144/// A binding configuration for connecting state to widget properties.
145#[derive(Debug, Clone)]
146pub struct BindingConfig {
147    /// Source path in state
148    pub source: PropertyPath,
149    /// Target property on widget
150    pub target: String,
151    /// Binding direction
152    pub direction: BindingDirection,
153    /// Optional transform function name
154    pub transform: Option<String>,
155    /// Optional fallback value (as string)
156    pub fallback: Option<String>,
157}
158
159impl BindingConfig {
160    /// Create a new one-way binding.
161    #[must_use]
162    pub fn one_way(source: impl Into<PropertyPath>, target: impl Into<String>) -> Self {
163        Self {
164            source: source.into(),
165            target: target.into(),
166            direction: BindingDirection::OneWay,
167            transform: None,
168            fallback: None,
169        }
170    }
171
172    /// Create a new two-way binding.
173    #[must_use]
174    pub fn two_way(source: impl Into<PropertyPath>, target: impl Into<String>) -> Self {
175        Self {
176            source: source.into(),
177            target: target.into(),
178            direction: BindingDirection::TwoWay,
179            transform: None,
180            fallback: None,
181        }
182    }
183
184    /// Set a transform function.
185    #[must_use]
186    pub fn transform(mut self, name: impl Into<String>) -> Self {
187        self.transform = Some(name.into());
188        self
189    }
190
191    /// Set a fallback value.
192    #[must_use]
193    pub fn fallback(mut self, value: impl Into<String>) -> Self {
194        self.fallback = Some(value.into());
195        self
196    }
197}
198
199/// Trait for types that can be bound to state.
200pub trait Bindable: Any + Send + Sync {
201    /// Get bindings for this widget.
202    fn bindings(&self) -> Vec<BindingConfig>;
203
204    /// Set bindings for this widget.
205    fn set_bindings(&mut self, bindings: Vec<BindingConfig>);
206
207    /// Apply a binding value update.
208    fn apply_binding(&mut self, target: &str, value: &dyn Any) -> bool;
209
210    /// Get the current value for a binding target.
211    fn get_binding_value(&self, target: &str) -> Option<Box<dyn Any + Send>>;
212}
213
214/// A reactive cell that holds a value and notifies on changes.
215pub struct ReactiveCell<T> {
216    value: Arc<RwLock<T>>,
217    subscribers: Subscribers<T>,
218}
219
220impl<T: Clone + Send + Sync + 'static> ReactiveCell<T> {
221    /// Create a new reactive cell with an initial value.
222    pub fn new(value: T) -> Self {
223        Self {
224            value: Arc::new(RwLock::new(value)),
225            subscribers: Arc::new(RwLock::new(Vec::new())),
226        }
227    }
228
229    /// Get the current value.
230    pub fn get(&self) -> T {
231        self.value
232            .read()
233            .expect("ReactiveCell lock poisoned")
234            .clone()
235    }
236
237    /// Set a new value, notifying subscribers.
238    pub fn set(&self, value: T) {
239        {
240            let mut guard = self.value.write().expect("ReactiveCell lock poisoned");
241            *guard = value;
242        }
243        self.notify();
244    }
245
246    /// Update the value using a function.
247    pub fn update<F>(&self, f: F)
248    where
249        F: FnOnce(&mut T),
250    {
251        {
252            let mut guard = self.value.write().expect("ReactiveCell lock poisoned");
253            f(&mut guard);
254        }
255        self.notify();
256    }
257
258    /// Subscribe to value changes.
259    pub fn subscribe<F>(&self, callback: F)
260    where
261        F: Fn(&T) + Send + Sync + 'static,
262    {
263        self.subscribers
264            .write()
265            .expect("ReactiveCell lock poisoned")
266            .push(Box::new(callback));
267    }
268
269    fn notify(&self) {
270        let value = self.value.read().expect("ReactiveCell lock poisoned");
271        let subscribers = self.subscribers.read().expect("ReactiveCell lock poisoned");
272        for sub in subscribers.iter() {
273            sub(&value);
274        }
275    }
276}
277
278impl<T: Clone + Send + Sync> Clone for ReactiveCell<T> {
279    fn clone(&self) -> Self {
280        Self {
281            value: self.value.clone(),
282            subscribers: Arc::new(RwLock::new(Vec::new())), // Don't clone subscribers
283        }
284    }
285}
286
287impl<T: Clone + Send + Sync + Default + 'static> Default for ReactiveCell<T> {
288    fn default() -> Self {
289        Self::new(T::default())
290    }
291}
292
293impl<T: Clone + Send + Sync + fmt::Debug + 'static> fmt::Debug for ReactiveCell<T> {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        f.debug_struct("ReactiveCell")
296            .field(
297                "value",
298                &*self.value.read().expect("ReactiveCell lock poisoned"),
299            )
300            .finish_non_exhaustive()
301    }
302}
303
304/// A computed value derived from other reactive sources.
305pub struct Computed<T> {
306    #[allow(dead_code)]
307    compute: Box<dyn Fn() -> T + Send + Sync>,
308    cached: Arc<RwLock<Option<T>>>,
309    dirty: Arc<RwLock<bool>>,
310}
311
312impl<T: Clone + Send + Sync + 'static> Computed<T> {
313    /// Create a new computed value.
314    pub fn new<F>(compute: F) -> Self
315    where
316        F: Fn() -> T + Send + Sync + 'static,
317    {
318        Self {
319            compute: Box::new(compute),
320            cached: Arc::new(RwLock::new(None)),
321            dirty: Arc::new(RwLock::new(true)),
322        }
323    }
324
325    /// Get the computed value (caches result).
326    pub fn get(&self) -> T {
327        let dirty = *self.dirty.read().expect("Computed lock poisoned");
328        if dirty {
329            let value = (self.compute)();
330            *self.cached.write().expect("Computed lock poisoned") = Some(value.clone());
331            *self.dirty.write().expect("Computed lock poisoned") = false;
332            value
333        } else {
334            self.cached
335                .read()
336                .expect("Computed lock poisoned")
337                .clone()
338                .expect("Computed cache should contain value when not dirty")
339        }
340    }
341
342    /// Mark the computed value as dirty (needs recomputation).
343    pub fn invalidate(&self) {
344        *self.dirty.write().expect("Computed lock poisoned") = true;
345    }
346}
347
348impl<T: Clone + Send + Sync + fmt::Debug + 'static> fmt::Debug for Computed<T> {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        f.debug_struct("Computed")
351            .field(
352                "cached",
353                &*self.cached.read().expect("Computed lock poisoned"),
354            )
355            .field(
356                "dirty",
357                &*self.dirty.read().expect("Computed lock poisoned"),
358            )
359            .finish_non_exhaustive()
360    }
361}
362
363/// A binding expression that can be evaluated against state.
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct BindingExpression {
366    /// Expression string (e.g., "{{ user.name }}" or "{{ count * 2 }}")
367    pub expression: String,
368    /// Parsed dependencies (property paths used)
369    pub dependencies: Vec<PropertyPath>,
370}
371
372impl BindingExpression {
373    /// Create a new binding expression.
374    #[must_use]
375    pub fn new(expression: impl Into<String>) -> Self {
376        let expression = expression.into();
377        let dependencies = Self::parse_dependencies(&expression);
378        Self {
379            expression,
380            dependencies,
381        }
382    }
383
384    /// Create a simple property binding.
385    #[must_use]
386    pub fn property(path: impl Into<PropertyPath>) -> Self {
387        let path: PropertyPath = path.into();
388        let expression = format!("{{{{ {} }}}}", path.to_string_path());
389        Self {
390            expression,
391            dependencies: vec![path],
392        }
393    }
394
395    /// Check if this is a simple property binding (no transforms).
396    #[must_use]
397    pub fn is_simple_property(&self) -> bool {
398        self.dependencies.len() == 1
399            && self.expression.trim().starts_with("{{")
400            && self.expression.trim().ends_with("}}")
401    }
402
403    /// Get the property path if this is a simple binding.
404    #[must_use]
405    pub fn as_property(&self) -> Option<&PropertyPath> {
406        if self.is_simple_property() {
407            self.dependencies.first()
408        } else {
409            None
410        }
411    }
412
413    fn parse_dependencies(expression: &str) -> Vec<PropertyPath> {
414        let mut deps = Vec::new();
415        let mut in_binding = false;
416        let mut current = String::new();
417
418        let chars: Vec<char> = expression.chars().collect();
419        let mut i = 0;
420
421        while i < chars.len() {
422            if i + 1 < chars.len() && chars[i] == '{' && chars[i + 1] == '{' {
423                in_binding = true;
424                i += 2;
425                continue;
426            }
427
428            if i + 1 < chars.len() && chars[i] == '}' && chars[i + 1] == '}' {
429                if !current.is_empty() {
430                    // Extract property path from current (may have transforms)
431                    let path_str = current.split('|').next().unwrap_or("").trim();
432                    if !path_str.is_empty() && !path_str.contains(|c: char| c.is_whitespace()) {
433                        deps.push(PropertyPath::new(path_str));
434                    }
435                    current.clear();
436                }
437                in_binding = false;
438                i += 2;
439                continue;
440            }
441
442            if in_binding {
443                current.push(chars[i]);
444            }
445
446            i += 1;
447        }
448
449        deps
450    }
451}
452
453/// Event binding that maps widget events to state messages.
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct EventBinding {
456    /// Widget event name (e.g., "click", "change", "submit")
457    pub event: String,
458    /// Action to dispatch
459    pub action: ActionBinding,
460}
461
462/// Action binding for state updates.
463#[derive(Debug, Clone, Serialize, Deserialize)]
464pub enum ActionBinding {
465    /// Set a property to a value
466    SetProperty {
467        /// Property path
468        path: PropertyPath,
469        /// Value expression
470        value: String,
471    },
472    /// Toggle a boolean property
473    ToggleProperty {
474        /// Property path
475        path: PropertyPath,
476    },
477    /// Increment a numeric property
478    IncrementProperty {
479        /// Property path
480        path: PropertyPath,
481        /// Amount to increment (default 1)
482        amount: Option<f64>,
483    },
484    /// Navigate to a route
485    Navigate {
486        /// Route path
487        route: String,
488    },
489    /// Dispatch a custom message
490    Dispatch {
491        /// Message type/name
492        message: String,
493        /// Optional payload
494        payload: Option<String>,
495    },
496    /// Execute multiple actions
497    Batch {
498        /// Actions to execute
499        actions: Vec<Self>,
500    },
501}
502
503impl EventBinding {
504    /// Create a new event binding.
505    #[must_use]
506    pub fn new(event: impl Into<String>, action: ActionBinding) -> Self {
507        Self {
508            event: event.into(),
509            action,
510        }
511    }
512
513    /// Create a click event binding.
514    #[must_use]
515    pub fn on_click(action: ActionBinding) -> Self {
516        Self::new("click", action)
517    }
518
519    /// Create a change event binding.
520    #[must_use]
521    pub fn on_change(action: ActionBinding) -> Self {
522        Self::new("change", action)
523    }
524}
525
526impl ActionBinding {
527    /// Create a set property action.
528    #[must_use]
529    pub fn set(path: impl Into<PropertyPath>, value: impl Into<String>) -> Self {
530        Self::SetProperty {
531            path: path.into(),
532            value: value.into(),
533        }
534    }
535
536    /// Create a toggle action.
537    #[must_use]
538    pub fn toggle(path: impl Into<PropertyPath>) -> Self {
539        Self::ToggleProperty { path: path.into() }
540    }
541
542    /// Create an increment action.
543    #[must_use]
544    pub fn increment(path: impl Into<PropertyPath>) -> Self {
545        Self::IncrementProperty {
546            path: path.into(),
547            amount: None,
548        }
549    }
550
551    /// Create an increment by amount action.
552    #[must_use]
553    pub fn increment_by(path: impl Into<PropertyPath>, amount: f64) -> Self {
554        Self::IncrementProperty {
555            path: path.into(),
556            amount: Some(amount),
557        }
558    }
559
560    /// Create a navigate action.
561    #[must_use]
562    pub fn navigate(route: impl Into<String>) -> Self {
563        Self::Navigate {
564            route: route.into(),
565        }
566    }
567
568    /// Create a dispatch action.
569    #[must_use]
570    pub fn dispatch(message: impl Into<String>) -> Self {
571        Self::Dispatch {
572            message: message.into(),
573            payload: None,
574        }
575    }
576
577    /// Create a dispatch with payload action.
578    #[must_use]
579    pub fn dispatch_with(message: impl Into<String>, payload: impl Into<String>) -> Self {
580        Self::Dispatch {
581            message: message.into(),
582            payload: Some(payload.into()),
583        }
584    }
585
586    /// Create a batch of actions.
587    #[must_use]
588    pub fn batch(actions: impl IntoIterator<Item = Self>) -> Self {
589        Self::Batch {
590            actions: actions.into_iter().collect(),
591        }
592    }
593}
594
595// =============================================================================
596// BindingManager - Orchestrates State-Widget Bindings
597// =============================================================================
598
599/// Manages bindings between application state and widget properties.
600///
601/// The `BindingManager` provides:
602/// - Registration of two-way bindings
603/// - Automatic propagation of state changes to widgets
604/// - Handling of widget changes back to state
605/// - Debouncing support for frequent updates
606#[derive(Debug, Default)]
607pub struct BindingManager {
608    /// Active bindings
609    bindings: Vec<ActiveBinding>,
610    /// Whether to debounce updates
611    debounce_ms: Option<u32>,
612    /// Pending updates queue
613    pending_updates: Vec<PendingUpdate>,
614}
615
616/// An active binding between state and widget.
617#[derive(Debug, Clone)]
618pub struct ActiveBinding {
619    /// Unique binding ID
620    pub id: BindingId,
621    /// Widget ID
622    pub widget_id: String,
623    /// Binding configuration
624    pub config: BindingConfig,
625    /// Current state value (as string for simplicity)
626    pub current_value: Option<String>,
627    /// Whether binding is active
628    pub active: bool,
629}
630
631/// Unique binding identifier.
632#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
633pub struct BindingId(pub u64);
634
635/// A pending update to be applied.
636#[derive(Debug, Clone)]
637pub struct PendingUpdate {
638    /// Source (widget or state)
639    pub source: UpdateSource,
640    /// Property path
641    pub path: PropertyPath,
642    /// New value as string
643    pub value: String,
644    /// Timestamp
645    pub timestamp: u64,
646}
647
648/// Source of a binding update.
649#[derive(Debug, Clone, Copy, PartialEq, Eq)]
650pub enum UpdateSource {
651    /// Update from state
652    State,
653    /// Update from widget
654    Widget,
655}
656
657impl BindingManager {
658    /// Create a new binding manager.
659    #[must_use]
660    pub fn new() -> Self {
661        Self::default()
662    }
663
664    /// Set debounce delay in milliseconds.
665    #[must_use]
666    pub fn with_debounce(mut self, ms: u32) -> Self {
667        self.debounce_ms = Some(ms);
668        self
669    }
670
671    /// Register a binding between state and widget.
672    pub fn register(&mut self, widget_id: impl Into<String>, config: BindingConfig) -> BindingId {
673        let id = BindingId(self.bindings.len() as u64);
674        self.bindings.push(ActiveBinding {
675            id,
676            widget_id: widget_id.into(),
677            config,
678            current_value: None,
679            active: true,
680        });
681        id
682    }
683
684    /// Unregister a binding.
685    pub fn unregister(&mut self, id: BindingId) {
686        if let Some(binding) = self.bindings.iter_mut().find(|b| b.id == id) {
687            binding.active = false;
688        }
689    }
690
691    /// Get bindings for a widget.
692    #[must_use]
693    pub fn bindings_for_widget(&self, widget_id: &str) -> Vec<&ActiveBinding> {
694        self.bindings
695            .iter()
696            .filter(|b| b.active && b.widget_id == widget_id)
697            .collect()
698    }
699
700    /// Get bindings for a state path.
701    #[must_use]
702    pub fn bindings_for_path(&self, path: &PropertyPath) -> Vec<&ActiveBinding> {
703        self.bindings
704            .iter()
705            .filter(|b| b.active && &b.config.source == path)
706            .collect()
707    }
708
709    /// Handle state change, propagate to widgets.
710    pub fn on_state_change(&mut self, path: &PropertyPath, value: &str) -> Vec<WidgetUpdate> {
711        let mut updates = Vec::new();
712
713        for binding in &mut self.bindings {
714            if !binding.active {
715                continue;
716            }
717
718            // Check if this path affects this binding
719            if &binding.config.source == path
720                || path
721                    .to_string_path()
722                    .starts_with(&binding.config.source.to_string_path())
723            {
724                binding.current_value = Some(value.to_string());
725
726                updates.push(WidgetUpdate {
727                    widget_id: binding.widget_id.clone(),
728                    property: binding.config.target.clone(),
729                    value: value.to_string(),
730                });
731            }
732        }
733
734        updates
735    }
736
737    /// Handle widget change, propagate to state.
738    pub fn on_widget_change(
739        &mut self,
740        widget_id: &str,
741        property: &str,
742        value: &str,
743    ) -> Vec<StateUpdate> {
744        let mut updates = Vec::new();
745
746        for binding in &self.bindings {
747            if !binding.active {
748                continue;
749            }
750
751            // Only two-way bindings propagate back to state
752            if binding.config.direction != BindingDirection::TwoWay {
753                continue;
754            }
755
756            if binding.widget_id == widget_id && binding.config.target == property {
757                updates.push(StateUpdate {
758                    path: binding.config.source.clone(),
759                    value: value.to_string(),
760                });
761            }
762        }
763
764        updates
765    }
766
767    /// Queue an update (for debouncing).
768    pub fn queue_update(&mut self, source: UpdateSource, path: PropertyPath, value: String) {
769        self.pending_updates.push(PendingUpdate {
770            source,
771            path,
772            value,
773            timestamp: 0, // Would be set to actual timestamp
774        });
775    }
776
777    /// Flush pending updates.
778    pub fn flush(&mut self) -> (Vec<WidgetUpdate>, Vec<StateUpdate>) {
779        let mut widget_updates = Vec::new();
780        let mut state_updates = Vec::new();
781
782        // Drain into separate Vec to avoid borrow issues
783        let updates: Vec<PendingUpdate> = self.pending_updates.drain(..).collect();
784
785        for update in updates {
786            match update.source {
787                UpdateSource::State => {
788                    widget_updates.extend(self.on_state_change(&update.path, &update.value));
789                }
790                UpdateSource::Widget => {
791                    // For widget updates, we'd need widget_id context
792                    state_updates.push(StateUpdate {
793                        path: update.path,
794                        value: update.value,
795                    });
796                }
797            }
798        }
799
800        (widget_updates, state_updates)
801    }
802
803    /// Get number of active bindings.
804    #[must_use]
805    pub fn active_count(&self) -> usize {
806        self.bindings.iter().filter(|b| b.active).count()
807    }
808
809    /// Clear all bindings.
810    pub fn clear(&mut self) {
811        self.bindings.clear();
812        self.pending_updates.clear();
813    }
814}
815
816/// Update to apply to a widget.
817#[derive(Debug, Clone)]
818pub struct WidgetUpdate {
819    /// Target widget ID
820    pub widget_id: String,
821    /// Property to update
822    pub property: String,
823    /// New value
824    pub value: String,
825}
826
827/// Update to apply to state.
828#[derive(Debug, Clone)]
829pub struct StateUpdate {
830    /// Property path
831    pub path: PropertyPath,
832    /// New value
833    pub value: String,
834}
835
836// =============================================================================
837// ValueConverter - Type Conversion for Bindings
838// =============================================================================
839
840/// Converts values between different types for binding.
841pub trait ValueConverter: Send + Sync {
842    /// Convert from source type to target type.
843    fn convert(&self, value: &str) -> Result<String, ConversionError>;
844
845    /// Convert back from target type to source type.
846    fn convert_back(&self, value: &str) -> Result<String, ConversionError>;
847}
848
849/// Error during value conversion.
850#[derive(Debug, Clone)]
851pub struct ConversionError {
852    /// Error message
853    pub message: String,
854}
855
856impl fmt::Display for ConversionError {
857    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
858        write!(f, "conversion error: {}", self.message)
859    }
860}
861
862impl std::error::Error for ConversionError {}
863
864/// Identity converter (no conversion).
865#[derive(Debug, Default)]
866pub struct IdentityConverter;
867
868impl ValueConverter for IdentityConverter {
869    fn convert(&self, value: &str) -> Result<String, ConversionError> {
870        Ok(value.to_string())
871    }
872
873    fn convert_back(&self, value: &str) -> Result<String, ConversionError> {
874        Ok(value.to_string())
875    }
876}
877
878/// Boolean to string converter.
879#[derive(Debug, Default)]
880pub struct BoolToStringConverter {
881    /// String for true value
882    pub true_string: String,
883    /// String for false value
884    pub false_string: String,
885}
886
887impl BoolToStringConverter {
888    /// Create with "true"/"false" strings.
889    #[must_use]
890    pub fn new() -> Self {
891        Self {
892            true_string: "true".to_string(),
893            false_string: "false".to_string(),
894        }
895    }
896
897    /// Create with custom strings.
898    #[must_use]
899    pub fn with_strings(true_str: impl Into<String>, false_str: impl Into<String>) -> Self {
900        Self {
901            true_string: true_str.into(),
902            false_string: false_str.into(),
903        }
904    }
905}
906
907impl ValueConverter for BoolToStringConverter {
908    fn convert(&self, value: &str) -> Result<String, ConversionError> {
909        match value {
910            "true" | "1" | "yes" => Ok(self.true_string.clone()),
911            "false" | "0" | "no" => Ok(self.false_string.clone()),
912            _ => Err(ConversionError {
913                message: format!("cannot convert '{value}' to bool"),
914            }),
915        }
916    }
917
918    fn convert_back(&self, value: &str) -> Result<String, ConversionError> {
919        if value == self.true_string {
920            Ok("true".to_string())
921        } else if value == self.false_string {
922            Ok("false".to_string())
923        } else {
924            Err(ConversionError {
925                message: format!("cannot convert '{value}' back to bool"),
926            })
927        }
928    }
929}
930
931/// Number formatter converter.
932#[derive(Debug, Default)]
933pub struct NumberFormatConverter {
934    /// Decimal places
935    pub decimals: usize,
936    /// Prefix (e.g., "$")
937    pub prefix: String,
938    /// Suffix (e.g., "%")
939    pub suffix: String,
940}
941
942impl NumberFormatConverter {
943    /// Create default formatter.
944    #[must_use]
945    pub fn new() -> Self {
946        Self::default()
947    }
948
949    /// Set decimal places.
950    #[must_use]
951    pub fn decimals(mut self, places: usize) -> Self {
952        self.decimals = places;
953        self
954    }
955
956    /// Set prefix.
957    #[must_use]
958    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
959        self.prefix = prefix.into();
960        self
961    }
962
963    /// Set suffix.
964    #[must_use]
965    pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
966        self.suffix = suffix.into();
967        self
968    }
969}
970
971impl ValueConverter for NumberFormatConverter {
972    fn convert(&self, value: &str) -> Result<String, ConversionError> {
973        let num: f64 = value.parse().map_err(|_| ConversionError {
974            message: format!("cannot parse '{value}' as number"),
975        })?;
976
977        let formatted = format!("{:.prec$}", num, prec = self.decimals);
978        Ok(format!("{}{}{}", self.prefix, formatted, self.suffix))
979    }
980
981    fn convert_back(&self, value: &str) -> Result<String, ConversionError> {
982        // Strip prefix and suffix
983        let stripped = value
984            .strip_prefix(&self.prefix)
985            .unwrap_or(value)
986            .strip_suffix(&self.suffix)
987            .unwrap_or(value)
988            .trim();
989
990        // Validate it's a number
991        let _: f64 = stripped.parse().map_err(|_| ConversionError {
992            message: format!("cannot parse '{stripped}' as number"),
993        })?;
994
995        Ok(stripped.to_string())
996    }
997}
998
999#[cfg(test)]
1000#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
1001#[path = "binding_tests.rs"]
1002mod tests;