Skip to main content

dampen_core/binding/
mod.rs

1//! Binding system types
2//!
3//! This module provides the core abstraction for data binding in Dampen.
4//!
5//! # Overview
6//!
7//! The binding system allows XML expressions like `{counter}` or `{user.name}`
8//! to access fields from Rust structs at runtime.
9//!
10//! # Key Types
11//!
12//! - [`UiBindable`] - Trait implemented by models to expose fields
13//! - [`BindingValue`] - Runtime value representation
14//! - [`ToBindingValue`] - Trait for converting Rust types to BindingValue
15//!
16//! # Example
17//!
18//! ```rust
19//! use dampen_core::{UiBindable, BindingValue};
20//!
21//! #[derive(Default)]
22//! struct Model {
23//!     count: i32,
24//!     name: String,
25//! }
26//!
27//! impl UiBindable for Model {
28//!     fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
29//!         match path {
30//!             ["count"] => Some(BindingValue::Integer(self.count as i64)),
31//!             ["name"] => Some(BindingValue::String(self.name.clone())),
32//!             _ => None,
33//!         }
34//!     }
35//!
36//!     fn available_fields() -> Vec<String> {
37//!         vec!["count".to_string(), "name".to_string()]
38//!     }
39//! }
40//! ```
41
42/// Trait for types that expose bindable fields
43///
44/// This trait is typically derived using `#[derive(UiModel)]` from the
45/// `dampen-macros` crate, but can be implemented manually for custom logic.
46///
47/// # Example
48///
49/// ```rust
50/// use dampen_core::{UiBindable, BindingValue};
51///
52/// struct MyModel { value: i32 }
53///
54/// impl UiBindable for MyModel {
55///     fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
56///         if path == ["value"] {
57///             Some(BindingValue::Integer(self.value as i64))
58///         } else {
59///             None
60///         }
61///     }
62///
63///     fn available_fields() -> Vec<String> {
64///         vec!["value".to_string()]
65///     }
66/// }
67/// ```
68pub trait UiBindable {
69    /// Get a field value by path
70    ///
71    /// # Arguments
72    ///
73    /// * `path` - Array of path segments, e.g., `["user", "name"]`
74    ///
75    /// # Returns
76    ///
77    /// `Some(BindingValue)` if the field exists, `None` otherwise
78    fn get_field(&self, path: &[&str]) -> Option<BindingValue>;
79
80    /// List available field paths for error suggestions
81    ///
82    /// Used to provide helpful error messages when a binding references
83    /// a non-existent field.
84    fn available_fields() -> Vec<String>
85    where
86        Self: Sized;
87}
88
89/// Value returned from a binding evaluation
90#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
91pub enum BindingValue {
92    /// String value
93    String(String),
94    /// Integer value
95    Integer(i64),
96    /// Floating-point value
97    Float(f64),
98    /// Boolean value
99    Bool(bool),
100    /// List of values
101    List(Vec<BindingValue>),
102    /// Object/record with named fields
103    Object(std::collections::HashMap<String, BindingValue>),
104    /// Custom opaque value (not serializable)
105    #[serde(skip)]
106    Custom(std::sync::Arc<dyn std::any::Any + Send + Sync>),
107    /// No value (null/none)
108    None,
109}
110
111impl PartialEq for BindingValue {
112    fn eq(&self, other: &Self) -> bool {
113        match (self, other) {
114            (Self::String(l0), Self::String(r0)) => l0 == r0,
115            (Self::Integer(l0), Self::Integer(r0)) => l0 == r0,
116            (Self::Float(l0), Self::Float(r0)) => l0 == r0,
117            (Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
118            (Self::List(l0), Self::List(r0)) => l0 == r0,
119            (Self::Object(l0), Self::Object(r0)) => l0 == r0,
120            (Self::Custom(l0), Self::Custom(r0)) => std::sync::Arc::ptr_eq(l0, r0),
121            (Self::None, Self::None) => true,
122            _ => false,
123        }
124    }
125}
126
127impl BindingValue {
128    /// Convert to display string for rendering
129    ///
130    /// Used when a binding value needs to be displayed as text.
131    ///
132    /// # Examples
133    ///
134    /// ```rust
135    /// use dampen_core::BindingValue;
136    ///
137    /// let val = BindingValue::Integer(42);
138    /// assert_eq!(val.to_display_string(), "42");
139    /// ```
140    pub fn to_display_string(&self) -> String {
141        match self {
142            BindingValue::String(s) => s.clone(),
143            BindingValue::Integer(i) => i.to_string(),
144            BindingValue::Float(f) => f.to_string(),
145            BindingValue::Bool(b) => b.to_string(),
146            BindingValue::List(l) => format!("[{} items]", l.len()),
147            BindingValue::Object(map) => format!("{{Object with {} fields}}", map.len()),
148            BindingValue::Custom(_) => "[Custom Value]".to_string(),
149            BindingValue::None => String::new(),
150        }
151    }
152
153    /// Convert to boolean for conditionals
154    ///
155    /// Used when a binding is used in a boolean context like `enabled="{condition}"`.
156    ///
157    /// # Truthiness Rules
158    ///
159    /// * `Bool(true)` → `true`
160    /// * Non-empty strings → `true`
161    /// * Non-zero numbers → `true`
162    /// * Non-empty lists → `true`
163    /// * `None` → `false`
164    /// * `Custom` → `true`
165    pub fn to_bool(&self) -> bool {
166        match self {
167            BindingValue::Bool(b) => *b,
168            BindingValue::String(s) => !s.is_empty(),
169            BindingValue::Integer(i) => *i != 0,
170            BindingValue::Float(f) => *f != 0.0,
171            BindingValue::List(l) => !l.is_empty(),
172            BindingValue::Object(map) => !map.is_empty(),
173            BindingValue::Custom(_) => true,
174            BindingValue::None => false,
175        }
176    }
177
178    /// Create BindingValue from a value
179    ///
180    /// Convenience method for converting types that implement `ToBindingValue`.
181    pub fn from_value<T: ToBindingValue>(value: &T) -> Self {
182        value.to_binding_value()
183    }
184
185    /// Get a field from an Object binding value
186    ///
187    /// Returns `None` if this is not an Object or the field doesn't exist.
188    pub fn get_field(&self, field_name: &str) -> Option<BindingValue> {
189        match self {
190            BindingValue::Object(map) => map.get(field_name).cloned(),
191            _ => None,
192        }
193    }
194}
195
196/// Trait for converting types to BindingValue
197///
198/// This trait is implemented for common Rust types to allow them to be
199/// used in binding expressions.
200///
201/// # Example
202///
203/// ```rust
204/// use dampen_core::{ToBindingValue, BindingValue};
205///
206/// let val = 42i32.to_binding_value();
207/// assert_eq!(val, BindingValue::Integer(42));
208/// ```
209pub trait ToBindingValue {
210    /// Convert self to a BindingValue
211    fn to_binding_value(&self) -> BindingValue;
212}
213
214/// Convert `String` to `BindingValue::String`
215impl ToBindingValue for String {
216    fn to_binding_value(&self) -> BindingValue {
217        BindingValue::String(self.clone())
218    }
219}
220
221/// Convert `&str` to `BindingValue::String`
222impl ToBindingValue for &str {
223    fn to_binding_value(&self) -> BindingValue {
224        BindingValue::String(self.to_string())
225    }
226}
227
228/// Convert `i32` to `BindingValue::Integer`
229impl ToBindingValue for i32 {
230    fn to_binding_value(&self) -> BindingValue {
231        BindingValue::Integer(*self as i64)
232    }
233}
234
235/// Convert `i64` to `BindingValue::Integer`
236impl ToBindingValue for i64 {
237    fn to_binding_value(&self) -> BindingValue {
238        BindingValue::Integer(*self)
239    }
240}
241
242/// Convert `f32` to `BindingValue::Float`
243impl ToBindingValue for f32 {
244    fn to_binding_value(&self) -> BindingValue {
245        BindingValue::Float(*self as f64)
246    }
247}
248
249/// Convert `f64` to `BindingValue::Float`
250impl ToBindingValue for f64 {
251    fn to_binding_value(&self) -> BindingValue {
252        BindingValue::Float(*self)
253    }
254}
255
256/// Convert `bool` to `BindingValue::Bool`
257impl ToBindingValue for bool {
258    fn to_binding_value(&self) -> BindingValue {
259        BindingValue::Bool(*self)
260    }
261}
262
263/// Convert `Vec<T>` to `BindingValue::List`
264impl<T: ToBindingValue> ToBindingValue for Vec<T> {
265    fn to_binding_value(&self) -> BindingValue {
266        BindingValue::List(self.iter().map(|v| v.to_binding_value()).collect())
267    }
268}
269
270/// Convert `Option<T>` to `BindingValue` or `BindingValue::None`
271impl<T: ToBindingValue> ToBindingValue for Option<T> {
272    fn to_binding_value(&self) -> BindingValue {
273        match self {
274            Some(v) => v.to_binding_value(),
275            None => BindingValue::None,
276        }
277    }
278}
279
280/// Convert `HashMap<String, T>` to `BindingValue::Object`
281impl<T: ToBindingValue> ToBindingValue for std::collections::HashMap<String, T> {
282    fn to_binding_value(&self) -> BindingValue {
283        BindingValue::Object(
284            self.iter()
285                .map(|(k, v)| (k.clone(), v.to_binding_value()))
286                .collect(),
287        )
288    }
289}
290
291/// Convert `Arc<dyn Any + Send + Sync>` to `BindingValue::Custom`
292impl ToBindingValue for std::sync::Arc<dyn std::any::Any + Send + Sync> {
293    fn to_binding_value(&self) -> BindingValue {
294        BindingValue::Custom(self.clone())
295    }
296}
297
298/// Implement UiBindable for the unit type.
299///
300/// This allows `AppState<()>` to be used for static UIs without a model.
301impl UiBindable for () {
302    fn get_field(&self, _path: &[&str]) -> Option<BindingValue> {
303        None
304    }
305
306    fn available_fields() -> Vec<String> {
307        vec![]
308    }
309}