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}