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}