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///
91/// This enum represents all possible types that can be produced by
92/// evaluating a binding expression.
93///
94/// # Variants
95///
96/// * `String` - Text values
97/// * `Integer` - Whole numbers
98/// * `Float` - Decimal numbers
99/// * `Bool` - Boolean values
100/// * `List` - Collections of values
101/// * `Object` - Key-value mappings (for structs/records)
102/// * `None` - Absence of value
103#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
104pub enum BindingValue {
105    /// String value
106    String(String),
107    /// Integer value
108    Integer(i64),
109    /// Floating-point value
110    Float(f64),
111    /// Boolean value
112    Bool(bool),
113    /// List of values
114    List(Vec<BindingValue>),
115    /// Object/record with named fields
116    Object(std::collections::HashMap<String, BindingValue>),
117    /// No value (null/none)
118    None,
119}
120
121impl BindingValue {
122    /// Convert to display string for rendering
123    ///
124    /// Used when a binding value needs to be displayed as text.
125    ///
126    /// # Examples
127    ///
128    /// ```rust
129    /// use dampen_core::BindingValue;
130    ///
131    /// let val = BindingValue::Integer(42);
132    /// assert_eq!(val.to_display_string(), "42");
133    /// ```
134    pub fn to_display_string(&self) -> String {
135        match self {
136            BindingValue::String(s) => s.clone(),
137            BindingValue::Integer(i) => i.to_string(),
138            BindingValue::Float(f) => f.to_string(),
139            BindingValue::Bool(b) => b.to_string(),
140            BindingValue::List(l) => format!("[{} items]", l.len()),
141            BindingValue::Object(map) => format!("{{Object with {} fields}}", map.len()),
142            BindingValue::None => String::new(),
143        }
144    }
145
146    /// Convert to boolean for conditionals
147    ///
148    /// Used when a binding is used in a boolean context like `enabled="{condition}"`.
149    ///
150    /// # Truthiness Rules
151    ///
152    /// * `Bool(true)` → `true`
153    /// * Non-empty strings → `true`
154    /// * Non-zero numbers → `true`
155    /// * Non-empty lists → `true`
156    /// * `None` → `false`
157    pub fn to_bool(&self) -> bool {
158        match self {
159            BindingValue::Bool(b) => *b,
160            BindingValue::String(s) => !s.is_empty(),
161            BindingValue::Integer(i) => *i != 0,
162            BindingValue::Float(f) => *f != 0.0,
163            BindingValue::List(l) => !l.is_empty(),
164            BindingValue::Object(map) => !map.is_empty(),
165            BindingValue::None => false,
166        }
167    }
168
169    /// Create BindingValue from a value
170    ///
171    /// Convenience method for converting types that implement `ToBindingValue`.
172    pub fn from_value<T: ToBindingValue>(value: &T) -> Self {
173        value.to_binding_value()
174    }
175
176    /// Get a field from an Object binding value
177    ///
178    /// Returns `None` if this is not an Object or the field doesn't exist.
179    pub fn get_field(&self, field_name: &str) -> Option<BindingValue> {
180        match self {
181            BindingValue::Object(map) => map.get(field_name).cloned(),
182            _ => None,
183        }
184    }
185}
186
187/// Trait for converting types to BindingValue
188///
189/// This trait is implemented for common Rust types to allow them to be
190/// used in binding expressions.
191///
192/// # Example
193///
194/// ```rust
195/// use dampen_core::{ToBindingValue, BindingValue};
196///
197/// let val = 42i32.to_binding_value();
198/// assert_eq!(val, BindingValue::Integer(42));
199/// ```
200pub trait ToBindingValue {
201    /// Convert self to a BindingValue
202    fn to_binding_value(&self) -> BindingValue;
203}
204
205/// Convert `String` to `BindingValue::String`
206impl ToBindingValue for String {
207    fn to_binding_value(&self) -> BindingValue {
208        BindingValue::String(self.clone())
209    }
210}
211
212/// Convert `&str` to `BindingValue::String`
213impl ToBindingValue for &str {
214    fn to_binding_value(&self) -> BindingValue {
215        BindingValue::String(self.to_string())
216    }
217}
218
219/// Convert `i32` to `BindingValue::Integer`
220impl ToBindingValue for i32 {
221    fn to_binding_value(&self) -> BindingValue {
222        BindingValue::Integer(*self as i64)
223    }
224}
225
226/// Convert `i64` to `BindingValue::Integer`
227impl ToBindingValue for i64 {
228    fn to_binding_value(&self) -> BindingValue {
229        BindingValue::Integer(*self)
230    }
231}
232
233/// Convert `f32` to `BindingValue::Float`
234impl ToBindingValue for f32 {
235    fn to_binding_value(&self) -> BindingValue {
236        BindingValue::Float(*self as f64)
237    }
238}
239
240/// Convert `f64` to `BindingValue::Float`
241impl ToBindingValue for f64 {
242    fn to_binding_value(&self) -> BindingValue {
243        BindingValue::Float(*self)
244    }
245}
246
247/// Convert `bool` to `BindingValue::Bool`
248impl ToBindingValue for bool {
249    fn to_binding_value(&self) -> BindingValue {
250        BindingValue::Bool(*self)
251    }
252}
253
254/// Convert `Vec<T>` to `BindingValue::List`
255impl<T: ToBindingValue> ToBindingValue for Vec<T> {
256    fn to_binding_value(&self) -> BindingValue {
257        BindingValue::List(self.iter().map(|v| v.to_binding_value()).collect())
258    }
259}
260
261/// Convert `Option<T>` to `BindingValue` or `BindingValue::None`
262impl<T: ToBindingValue> ToBindingValue for Option<T> {
263    fn to_binding_value(&self) -> BindingValue {
264        match self {
265            Some(v) => v.to_binding_value(),
266            None => BindingValue::None,
267        }
268    }
269}
270
271/// Convert `HashMap<String, T>` to `BindingValue::Object`
272impl<T: ToBindingValue> ToBindingValue for std::collections::HashMap<String, T> {
273    fn to_binding_value(&self) -> BindingValue {
274        BindingValue::Object(
275            self.iter()
276                .map(|(k, v)| (k.clone(), v.to_binding_value()))
277                .collect(),
278        )
279    }
280}
281
282/// Implement UiBindable for the unit type.
283///
284/// This allows `AppState<()>` to be used for static UIs without a model.
285impl UiBindable for () {
286    fn get_field(&self, _path: &[&str]) -> Option<BindingValue> {
287        None
288    }
289
290    fn available_fields() -> Vec<String> {
291        vec![]
292    }
293}