use serde::{Deserialize, Serialize};
use std::any::Any;
use std::fmt;
use std::sync::{Arc, RwLock};
type SubscriberFn<T> = Box<dyn Fn(&T) + Send + Sync>;
type Subscribers<T> = Arc<RwLock<Vec<SubscriberFn<T>>>>;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PropertyPath {
segments: Vec<String>,
}
impl PropertyPath {
#[must_use]
pub fn new(path: &str) -> Self {
let segments = path
.split('.')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
Self { segments }
}
#[must_use]
pub const fn root() -> Self {
Self {
segments: Vec::new(),
}
}
#[must_use]
pub fn segments(&self) -> &[String] {
&self.segments
}
#[must_use]
pub fn is_root(&self) -> bool {
self.segments.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.segments.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
#[must_use]
pub fn join(&self, segment: &str) -> Self {
let mut segments = self.segments.clone();
segments.push(segment.to_string());
Self { segments }
}
#[must_use]
pub fn parent(&self) -> Option<Self> {
if self.segments.is_empty() {
None
} else {
let mut segments = self.segments.clone();
segments.pop();
Some(Self { segments })
}
}
#[must_use]
pub fn leaf(&self) -> Option<&str> {
self.segments.last().map(String::as_str)
}
#[must_use]
pub fn to_string_path(&self) -> String {
self.segments.join(".")
}
}
impl fmt::Display for PropertyPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string_path())
}
}
impl From<&str> for PropertyPath {
fn from(s: &str) -> Self {
Self::new(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BindingDirection {
#[default]
OneWay,
TwoWay,
OneTime,
}
#[derive(Debug, Clone)]
pub struct BindingConfig {
pub source: PropertyPath,
pub target: String,
pub direction: BindingDirection,
pub transform: Option<String>,
pub fallback: Option<String>,
}
impl BindingConfig {
#[must_use]
pub fn one_way(source: impl Into<PropertyPath>, target: impl Into<String>) -> Self {
Self {
source: source.into(),
target: target.into(),
direction: BindingDirection::OneWay,
transform: None,
fallback: None,
}
}
#[must_use]
pub fn two_way(source: impl Into<PropertyPath>, target: impl Into<String>) -> Self {
Self {
source: source.into(),
target: target.into(),
direction: BindingDirection::TwoWay,
transform: None,
fallback: None,
}
}
#[must_use]
pub fn transform(mut self, name: impl Into<String>) -> Self {
self.transform = Some(name.into());
self
}
#[must_use]
pub fn fallback(mut self, value: impl Into<String>) -> Self {
self.fallback = Some(value.into());
self
}
}
pub trait Bindable: Any + Send + Sync {
fn bindings(&self) -> Vec<BindingConfig>;
fn set_bindings(&mut self, bindings: Vec<BindingConfig>);
fn apply_binding(&mut self, target: &str, value: &dyn Any) -> bool;
fn get_binding_value(&self, target: &str) -> Option<Box<dyn Any + Send>>;
}
pub struct ReactiveCell<T> {
value: Arc<RwLock<T>>,
subscribers: Subscribers<T>,
}
impl<T: Clone + Send + Sync + 'static> ReactiveCell<T> {
pub fn new(value: T) -> Self {
Self {
value: Arc::new(RwLock::new(value)),
subscribers: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn get(&self) -> T {
self.value
.read()
.expect("ReactiveCell lock poisoned")
.clone()
}
pub fn set(&self, value: T) {
{
let mut guard = self.value.write().expect("ReactiveCell lock poisoned");
*guard = value;
}
self.notify();
}
pub fn update<F>(&self, f: F)
where
F: FnOnce(&mut T),
{
{
let mut guard = self.value.write().expect("ReactiveCell lock poisoned");
f(&mut guard);
}
self.notify();
}
pub fn subscribe<F>(&self, callback: F)
where
F: Fn(&T) + Send + Sync + 'static,
{
self.subscribers
.write()
.expect("ReactiveCell lock poisoned")
.push(Box::new(callback));
}
fn notify(&self) {
let value = self.value.read().expect("ReactiveCell lock poisoned");
let subscribers = self.subscribers.read().expect("ReactiveCell lock poisoned");
for sub in subscribers.iter() {
sub(&value);
}
}
}
impl<T: Clone + Send + Sync> Clone for ReactiveCell<T> {
fn clone(&self) -> Self {
Self {
value: self.value.clone(),
subscribers: Arc::new(RwLock::new(Vec::new())), }
}
}
impl<T: Clone + Send + Sync + Default + 'static> Default for ReactiveCell<T> {
fn default() -> Self {
Self::new(T::default())
}
}
impl<T: Clone + Send + Sync + fmt::Debug + 'static> fmt::Debug for ReactiveCell<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ReactiveCell")
.field(
"value",
&*self.value.read().expect("ReactiveCell lock poisoned"),
)
.finish_non_exhaustive()
}
}
pub struct Computed<T> {
#[allow(dead_code)]
compute: Box<dyn Fn() -> T + Send + Sync>,
cached: Arc<RwLock<Option<T>>>,
dirty: Arc<RwLock<bool>>,
}
impl<T: Clone + Send + Sync + 'static> Computed<T> {
pub fn new<F>(compute: F) -> Self
where
F: Fn() -> T + Send + Sync + 'static,
{
Self {
compute: Box::new(compute),
cached: Arc::new(RwLock::new(None)),
dirty: Arc::new(RwLock::new(true)),
}
}
pub fn get(&self) -> T {
let dirty = *self.dirty.read().expect("Computed lock poisoned");
if dirty {
let value = (self.compute)();
*self.cached.write().expect("Computed lock poisoned") = Some(value.clone());
*self.dirty.write().expect("Computed lock poisoned") = false;
value
} else {
self.cached
.read()
.expect("Computed lock poisoned")
.clone()
.expect("Computed cache should contain value when not dirty")
}
}
pub fn invalidate(&self) {
*self.dirty.write().expect("Computed lock poisoned") = true;
}
}
impl<T: Clone + Send + Sync + fmt::Debug + 'static> fmt::Debug for Computed<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Computed")
.field(
"cached",
&*self.cached.read().expect("Computed lock poisoned"),
)
.field(
"dirty",
&*self.dirty.read().expect("Computed lock poisoned"),
)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BindingExpression {
pub expression: String,
pub dependencies: Vec<PropertyPath>,
}
impl BindingExpression {
#[must_use]
pub fn new(expression: impl Into<String>) -> Self {
let expression = expression.into();
let dependencies = Self::parse_dependencies(&expression);
Self {
expression,
dependencies,
}
}
#[must_use]
pub fn property(path: impl Into<PropertyPath>) -> Self {
let path: PropertyPath = path.into();
let expression = format!("{{{{ {} }}}}", path.to_string_path());
Self {
expression,
dependencies: vec![path],
}
}
#[must_use]
pub fn is_simple_property(&self) -> bool {
self.dependencies.len() == 1
&& self.expression.trim().starts_with("{{")
&& self.expression.trim().ends_with("}}")
}
#[must_use]
pub fn as_property(&self) -> Option<&PropertyPath> {
if self.is_simple_property() {
self.dependencies.first()
} else {
None
}
}
fn parse_dependencies(expression: &str) -> Vec<PropertyPath> {
let mut deps = Vec::new();
let mut in_binding = false;
let mut current = String::new();
let chars: Vec<char> = expression.chars().collect();
let mut i = 0;
while i < chars.len() {
if i + 1 < chars.len() && chars[i] == '{' && chars[i + 1] == '{' {
in_binding = true;
i += 2;
continue;
}
if i + 1 < chars.len() && chars[i] == '}' && chars[i + 1] == '}' {
if !current.is_empty() {
let path_str = current.split('|').next().unwrap_or("").trim();
if !path_str.is_empty() && !path_str.contains(|c: char| c.is_whitespace()) {
deps.push(PropertyPath::new(path_str));
}
current.clear();
}
in_binding = false;
i += 2;
continue;
}
if in_binding {
current.push(chars[i]);
}
i += 1;
}
deps
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventBinding {
pub event: String,
pub action: ActionBinding,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ActionBinding {
SetProperty {
path: PropertyPath,
value: String,
},
ToggleProperty {
path: PropertyPath,
},
IncrementProperty {
path: PropertyPath,
amount: Option<f64>,
},
Navigate {
route: String,
},
Dispatch {
message: String,
payload: Option<String>,
},
Batch {
actions: Vec<Self>,
},
}
impl EventBinding {
#[must_use]
pub fn new(event: impl Into<String>, action: ActionBinding) -> Self {
Self {
event: event.into(),
action,
}
}
#[must_use]
pub fn on_click(action: ActionBinding) -> Self {
Self::new("click", action)
}
#[must_use]
pub fn on_change(action: ActionBinding) -> Self {
Self::new("change", action)
}
}
impl ActionBinding {
#[must_use]
pub fn set(path: impl Into<PropertyPath>, value: impl Into<String>) -> Self {
Self::SetProperty {
path: path.into(),
value: value.into(),
}
}
#[must_use]
pub fn toggle(path: impl Into<PropertyPath>) -> Self {
Self::ToggleProperty { path: path.into() }
}
#[must_use]
pub fn increment(path: impl Into<PropertyPath>) -> Self {
Self::IncrementProperty {
path: path.into(),
amount: None,
}
}
#[must_use]
pub fn increment_by(path: impl Into<PropertyPath>, amount: f64) -> Self {
Self::IncrementProperty {
path: path.into(),
amount: Some(amount),
}
}
#[must_use]
pub fn navigate(route: impl Into<String>) -> Self {
Self::Navigate {
route: route.into(),
}
}
#[must_use]
pub fn dispatch(message: impl Into<String>) -> Self {
Self::Dispatch {
message: message.into(),
payload: None,
}
}
#[must_use]
pub fn dispatch_with(message: impl Into<String>, payload: impl Into<String>) -> Self {
Self::Dispatch {
message: message.into(),
payload: Some(payload.into()),
}
}
#[must_use]
pub fn batch(actions: impl IntoIterator<Item = Self>) -> Self {
Self::Batch {
actions: actions.into_iter().collect(),
}
}
}
#[derive(Debug, Default)]
pub struct BindingManager {
bindings: Vec<ActiveBinding>,
debounce_ms: Option<u32>,
pending_updates: Vec<PendingUpdate>,
}
#[derive(Debug, Clone)]
pub struct ActiveBinding {
pub id: BindingId,
pub widget_id: String,
pub config: BindingConfig,
pub current_value: Option<String>,
pub active: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct BindingId(pub u64);
#[derive(Debug, Clone)]
pub struct PendingUpdate {
pub source: UpdateSource,
pub path: PropertyPath,
pub value: String,
pub timestamp: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpdateSource {
State,
Widget,
}
impl BindingManager {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_debounce(mut self, ms: u32) -> Self {
self.debounce_ms = Some(ms);
self
}
pub fn register(&mut self, widget_id: impl Into<String>, config: BindingConfig) -> BindingId {
let id = BindingId(self.bindings.len() as u64);
self.bindings.push(ActiveBinding {
id,
widget_id: widget_id.into(),
config,
current_value: None,
active: true,
});
id
}
pub fn unregister(&mut self, id: BindingId) {
if let Some(binding) = self.bindings.iter_mut().find(|b| b.id == id) {
binding.active = false;
}
}
#[must_use]
pub fn bindings_for_widget(&self, widget_id: &str) -> Vec<&ActiveBinding> {
self.bindings
.iter()
.filter(|b| b.active && b.widget_id == widget_id)
.collect()
}
#[must_use]
pub fn bindings_for_path(&self, path: &PropertyPath) -> Vec<&ActiveBinding> {
self.bindings
.iter()
.filter(|b| b.active && &b.config.source == path)
.collect()
}
pub fn on_state_change(&mut self, path: &PropertyPath, value: &str) -> Vec<WidgetUpdate> {
let mut updates = Vec::new();
for binding in &mut self.bindings {
if !binding.active {
continue;
}
if &binding.config.source == path
|| path
.to_string_path()
.starts_with(&binding.config.source.to_string_path())
{
binding.current_value = Some(value.to_string());
updates.push(WidgetUpdate {
widget_id: binding.widget_id.clone(),
property: binding.config.target.clone(),
value: value.to_string(),
});
}
}
updates
}
pub fn on_widget_change(
&mut self,
widget_id: &str,
property: &str,
value: &str,
) -> Vec<StateUpdate> {
let mut updates = Vec::new();
for binding in &self.bindings {
if !binding.active {
continue;
}
if binding.config.direction != BindingDirection::TwoWay {
continue;
}
if binding.widget_id == widget_id && binding.config.target == property {
updates.push(StateUpdate {
path: binding.config.source.clone(),
value: value.to_string(),
});
}
}
updates
}
pub fn queue_update(&mut self, source: UpdateSource, path: PropertyPath, value: String) {
self.pending_updates.push(PendingUpdate {
source,
path,
value,
timestamp: 0, });
}
pub fn flush(&mut self) -> (Vec<WidgetUpdate>, Vec<StateUpdate>) {
let mut widget_updates = Vec::new();
let mut state_updates = Vec::new();
let updates: Vec<PendingUpdate> = self.pending_updates.drain(..).collect();
for update in updates {
match update.source {
UpdateSource::State => {
widget_updates.extend(self.on_state_change(&update.path, &update.value));
}
UpdateSource::Widget => {
state_updates.push(StateUpdate {
path: update.path,
value: update.value,
});
}
}
}
(widget_updates, state_updates)
}
#[must_use]
pub fn active_count(&self) -> usize {
self.bindings.iter().filter(|b| b.active).count()
}
pub fn clear(&mut self) {
self.bindings.clear();
self.pending_updates.clear();
}
}
#[derive(Debug, Clone)]
pub struct WidgetUpdate {
pub widget_id: String,
pub property: String,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct StateUpdate {
pub path: PropertyPath,
pub value: String,
}
pub trait ValueConverter: Send + Sync {
fn convert(&self, value: &str) -> Result<String, ConversionError>;
fn convert_back(&self, value: &str) -> Result<String, ConversionError>;
}
#[derive(Debug, Clone)]
pub struct ConversionError {
pub message: String,
}
impl fmt::Display for ConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "conversion error: {}", self.message)
}
}
impl std::error::Error for ConversionError {}
#[derive(Debug, Default)]
pub struct IdentityConverter;
impl ValueConverter for IdentityConverter {
fn convert(&self, value: &str) -> Result<String, ConversionError> {
Ok(value.to_string())
}
fn convert_back(&self, value: &str) -> Result<String, ConversionError> {
Ok(value.to_string())
}
}
#[derive(Debug, Default)]
pub struct BoolToStringConverter {
pub true_string: String,
pub false_string: String,
}
impl BoolToStringConverter {
#[must_use]
pub fn new() -> Self {
Self {
true_string: "true".to_string(),
false_string: "false".to_string(),
}
}
#[must_use]
pub fn with_strings(true_str: impl Into<String>, false_str: impl Into<String>) -> Self {
Self {
true_string: true_str.into(),
false_string: false_str.into(),
}
}
}
impl ValueConverter for BoolToStringConverter {
fn convert(&self, value: &str) -> Result<String, ConversionError> {
match value {
"true" | "1" | "yes" => Ok(self.true_string.clone()),
"false" | "0" | "no" => Ok(self.false_string.clone()),
_ => Err(ConversionError {
message: format!("cannot convert '{value}' to bool"),
}),
}
}
fn convert_back(&self, value: &str) -> Result<String, ConversionError> {
if value == self.true_string {
Ok("true".to_string())
} else if value == self.false_string {
Ok("false".to_string())
} else {
Err(ConversionError {
message: format!("cannot convert '{value}' back to bool"),
})
}
}
}
#[derive(Debug, Default)]
pub struct NumberFormatConverter {
pub decimals: usize,
pub prefix: String,
pub suffix: String,
}
impl NumberFormatConverter {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn decimals(mut self, places: usize) -> Self {
self.decimals = places;
self
}
#[must_use]
pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
#[must_use]
pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
self.suffix = suffix.into();
self
}
}
impl ValueConverter for NumberFormatConverter {
fn convert(&self, value: &str) -> Result<String, ConversionError> {
let num: f64 = value.parse().map_err(|_| ConversionError {
message: format!("cannot parse '{value}' as number"),
})?;
let formatted = format!("{:.prec$}", num, prec = self.decimals);
Ok(format!("{}{}{}", self.prefix, formatted, self.suffix))
}
fn convert_back(&self, value: &str) -> Result<String, ConversionError> {
let stripped = value
.strip_prefix(&self.prefix)
.unwrap_or(value)
.strip_suffix(&self.suffix)
.unwrap_or(value)
.trim();
let _: f64 = stripped.parse().map_err(|_| ConversionError {
message: format!("cannot parse '{stripped}' as number"),
})?;
Ok(stripped.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_property_path_new() {
let path = PropertyPath::new("user.profile.name");
assert_eq!(path.segments(), &["user", "profile", "name"]);
}
#[test]
fn test_property_path_root() {
let path = PropertyPath::root();
assert!(path.is_root());
assert!(path.is_empty());
}
#[test]
fn test_property_path_len() {
let path = PropertyPath::new("a.b.c");
assert_eq!(path.len(), 3);
}
#[test]
fn test_property_path_join() {
let path = PropertyPath::new("user");
let joined = path.join("name");
assert_eq!(joined.to_string_path(), "user.name");
}
#[test]
fn test_property_path_parent() {
let path = PropertyPath::new("user.profile.name");
let parent = path.parent().unwrap();
assert_eq!(parent.to_string_path(), "user.profile");
}
#[test]
fn test_property_path_leaf() {
let path = PropertyPath::new("user.profile.name");
assert_eq!(path.leaf(), Some("name"));
}
#[test]
fn test_property_path_display() {
let path = PropertyPath::new("a.b.c");
assert_eq!(format!("{path}"), "a.b.c");
}
#[test]
fn test_property_path_from_str() {
let path: PropertyPath = "user.name".into();
assert_eq!(path.segments(), &["user", "name"]);
}
#[test]
fn test_binding_config_one_way() {
let config = BindingConfig::one_way("count", "value");
assert_eq!(config.source.to_string_path(), "count");
assert_eq!(config.target, "value");
assert_eq!(config.direction, BindingDirection::OneWay);
}
#[test]
fn test_binding_config_two_way() {
let config = BindingConfig::two_way("user.name", "text");
assert_eq!(config.direction, BindingDirection::TwoWay);
}
#[test]
fn test_binding_config_transform() {
let config = BindingConfig::one_way("count", "label").transform("toString");
assert_eq!(config.transform, Some("toString".to_string()));
}
#[test]
fn test_binding_config_fallback() {
let config = BindingConfig::one_way("user.name", "text").fallback("Anonymous");
assert_eq!(config.fallback, Some("Anonymous".to_string()));
}
#[test]
fn test_reactive_cell_new() {
let cell = ReactiveCell::new(42);
assert_eq!(cell.get(), 42);
}
#[test]
fn test_reactive_cell_set() {
let cell = ReactiveCell::new(0);
cell.set(100);
assert_eq!(cell.get(), 100);
}
#[test]
fn test_reactive_cell_update() {
let cell = ReactiveCell::new(10);
cell.update(|v| *v *= 2);
assert_eq!(cell.get(), 20);
}
#[test]
fn test_reactive_cell_subscribe() {
use std::sync::atomic::{AtomicI32, Ordering};
let cell = ReactiveCell::new(0);
let count = Arc::new(AtomicI32::new(0));
let count_clone = count.clone();
cell.subscribe(move |v| {
count_clone.store(*v, Ordering::SeqCst);
});
cell.set(42);
assert_eq!(count.load(Ordering::SeqCst), 42);
}
#[test]
fn test_reactive_cell_default() {
let cell: ReactiveCell<i32> = ReactiveCell::default();
assert_eq!(cell.get(), 0);
}
#[test]
fn test_reactive_cell_clone() {
let cell1 = ReactiveCell::new(10);
let cell2 = cell1.clone();
cell1.set(20);
assert_eq!(cell2.get(), 20); }
#[test]
fn test_computed_new() {
let computed = Computed::new(|| 42);
assert_eq!(computed.get(), 42);
}
#[test]
fn test_computed_caches() {
use std::sync::atomic::{AtomicUsize, Ordering};
let call_count = Arc::new(AtomicUsize::new(0));
let call_count_clone = call_count.clone();
let computed = Computed::new(move || {
call_count_clone.fetch_add(1, Ordering::SeqCst);
42
});
computed.get();
computed.get();
computed.get();
assert_eq!(call_count.load(Ordering::SeqCst), 1); }
#[test]
fn test_computed_invalidate() {
use std::sync::atomic::{AtomicUsize, Ordering};
let call_count = Arc::new(AtomicUsize::new(0));
let call_count_clone = call_count.clone();
let computed = Computed::new(move || {
call_count_clone.fetch_add(1, Ordering::SeqCst);
42
});
computed.get();
computed.invalidate();
computed.get();
assert_eq!(call_count.load(Ordering::SeqCst), 2); }
#[test]
fn test_binding_expression_new() {
let expr = BindingExpression::new("{{ user.name }}");
assert_eq!(expr.dependencies.len(), 1);
assert_eq!(expr.dependencies[0].to_string_path(), "user.name");
}
#[test]
fn test_binding_expression_property() {
let expr = BindingExpression::property("count");
assert!(expr.is_simple_property());
assert_eq!(expr.as_property().unwrap().to_string_path(), "count");
}
#[test]
fn test_binding_expression_with_transform() {
let expr = BindingExpression::new("{{ count | format }}");
assert_eq!(expr.dependencies[0].to_string_path(), "count");
}
#[test]
fn test_binding_expression_multiple_deps() {
let expr = BindingExpression::new("{{ first }} and {{ second }}");
assert_eq!(expr.dependencies.len(), 2);
}
#[test]
fn test_event_binding_new() {
let binding = EventBinding::new("click", ActionBinding::toggle("visible"));
assert_eq!(binding.event, "click");
}
#[test]
fn test_event_binding_on_click() {
let binding = EventBinding::on_click(ActionBinding::dispatch("submit"));
assert_eq!(binding.event, "click");
}
#[test]
fn test_event_binding_on_change() {
let binding = EventBinding::on_change(ActionBinding::set("value", "new"));
assert_eq!(binding.event, "change");
}
#[test]
fn test_action_binding_set() {
let action = ActionBinding::set("user.name", "Alice");
if let ActionBinding::SetProperty { path, value } = action {
assert_eq!(path.to_string_path(), "user.name");
assert_eq!(value, "Alice");
} else {
panic!("Expected SetProperty");
}
}
#[test]
fn test_action_binding_toggle() {
let action = ActionBinding::toggle("visible");
if let ActionBinding::ToggleProperty { path } = action {
assert_eq!(path.to_string_path(), "visible");
} else {
panic!("Expected ToggleProperty");
}
}
#[test]
fn test_action_binding_increment() {
let action = ActionBinding::increment("count");
if let ActionBinding::IncrementProperty { path, amount } = action {
assert_eq!(path.to_string_path(), "count");
assert!(amount.is_none());
} else {
panic!("Expected IncrementProperty");
}
}
#[test]
fn test_action_binding_increment_by() {
let action = ActionBinding::increment_by("score", 10.0);
if let ActionBinding::IncrementProperty { amount, .. } = action {
assert_eq!(amount, Some(10.0));
} else {
panic!("Expected IncrementProperty");
}
}
#[test]
fn test_action_binding_navigate() {
let action = ActionBinding::navigate("/home");
if let ActionBinding::Navigate { route } = action {
assert_eq!(route, "/home");
} else {
panic!("Expected Navigate");
}
}
#[test]
fn test_action_binding_dispatch() {
let action = ActionBinding::dispatch("submit");
if let ActionBinding::Dispatch { message, payload } = action {
assert_eq!(message, "submit");
assert!(payload.is_none());
} else {
panic!("Expected Dispatch");
}
}
#[test]
fn test_action_binding_dispatch_with() {
let action = ActionBinding::dispatch_with("submit", "form_data");
if let ActionBinding::Dispatch { message, payload } = action {
assert_eq!(message, "submit");
assert_eq!(payload, Some("form_data".to_string()));
} else {
panic!("Expected Dispatch");
}
}
#[test]
fn test_action_binding_batch() {
let action = ActionBinding::batch([
ActionBinding::increment("count"),
ActionBinding::navigate("/next"),
]);
if let ActionBinding::Batch { actions } = action {
assert_eq!(actions.len(), 2);
} else {
panic!("Expected Batch");
}
}
#[test]
fn test_binding_manager_new() {
let manager = BindingManager::new();
assert_eq!(manager.active_count(), 0);
}
#[test]
fn test_binding_manager_register() {
let mut manager = BindingManager::new();
let id = manager.register("widget1", BindingConfig::one_way("count", "text"));
assert_eq!(id.0, 0);
assert_eq!(manager.active_count(), 1);
}
#[test]
fn test_binding_manager_unregister() {
let mut manager = BindingManager::new();
let id = manager.register("widget1", BindingConfig::one_way("count", "text"));
manager.unregister(id);
assert_eq!(manager.active_count(), 0);
}
#[test]
fn test_binding_manager_bindings_for_widget() {
let mut manager = BindingManager::new();
manager.register("widget1", BindingConfig::one_way("count", "text"));
manager.register("widget1", BindingConfig::one_way("name", "label"));
manager.register("widget2", BindingConfig::one_way("other", "value"));
let bindings = manager.bindings_for_widget("widget1");
assert_eq!(bindings.len(), 2);
}
#[test]
fn test_binding_manager_bindings_for_path() {
let mut manager = BindingManager::new();
manager.register("widget1", BindingConfig::one_way("user.name", "text"));
manager.register("widget2", BindingConfig::one_way("user.name", "label"));
let path = PropertyPath::new("user.name");
let bindings = manager.bindings_for_path(&path);
assert_eq!(bindings.len(), 2);
}
#[test]
fn test_binding_manager_on_state_change() {
let mut manager = BindingManager::new();
manager.register("widget1", BindingConfig::one_way("count", "text"));
manager.register("widget2", BindingConfig::one_way("count", "label"));
let path = PropertyPath::new("count");
let updates = manager.on_state_change(&path, "42");
assert_eq!(updates.len(), 2);
assert!(updates.iter().any(|u| u.widget_id == "widget1"));
assert!(updates.iter().any(|u| u.widget_id == "widget2"));
}
#[test]
fn test_binding_manager_on_widget_change_two_way() {
let mut manager = BindingManager::new();
manager.register("input1", BindingConfig::two_way("user.name", "value"));
let updates = manager.on_widget_change("input1", "value", "Alice");
assert_eq!(updates.len(), 1);
assert_eq!(updates[0].path.to_string_path(), "user.name");
assert_eq!(updates[0].value, "Alice");
}
#[test]
fn test_binding_manager_on_widget_change_one_way_no_propagate() {
let mut manager = BindingManager::new();
manager.register("label1", BindingConfig::one_way("count", "text"));
let updates = manager.on_widget_change("label1", "text", "new value");
assert!(updates.is_empty()); }
#[test]
fn test_binding_manager_with_debounce() {
let manager = BindingManager::new().with_debounce(100);
assert_eq!(manager.debounce_ms, Some(100));
}
#[test]
fn test_binding_manager_queue_and_flush() {
let mut manager = BindingManager::new();
manager.register("widget1", BindingConfig::one_way("count", "text"));
manager.queue_update(
UpdateSource::State,
PropertyPath::new("count"),
"42".to_string(),
);
let (widget_updates, _) = manager.flush();
assert_eq!(widget_updates.len(), 1);
}
#[test]
fn test_binding_manager_clear() {
let mut manager = BindingManager::new();
manager.register("w1", BindingConfig::one_way("a", "b"));
manager.register("w2", BindingConfig::one_way("c", "d"));
manager.clear();
assert_eq!(manager.active_count(), 0);
}
#[test]
fn test_identity_converter() {
let converter = IdentityConverter;
assert_eq!(converter.convert("hello").unwrap(), "hello");
assert_eq!(converter.convert_back("world").unwrap(), "world");
}
#[test]
fn test_bool_to_string_converter() {
let converter = BoolToStringConverter::new();
assert_eq!(converter.convert("true").unwrap(), "true");
assert_eq!(converter.convert("false").unwrap(), "false");
assert_eq!(converter.convert("1").unwrap(), "true");
assert_eq!(converter.convert("0").unwrap(), "false");
}
#[test]
fn test_bool_to_string_converter_custom() {
let converter = BoolToStringConverter::with_strings("Yes", "No");
assert_eq!(converter.convert("true").unwrap(), "Yes");
assert_eq!(converter.convert("false").unwrap(), "No");
assert_eq!(converter.convert_back("Yes").unwrap(), "true");
assert_eq!(converter.convert_back("No").unwrap(), "false");
}
#[test]
fn test_bool_to_string_converter_error() {
let converter = BoolToStringConverter::new();
assert!(converter.convert("invalid").is_err());
}
#[test]
fn test_number_format_converter() {
let converter = NumberFormatConverter::new().decimals(2);
assert_eq!(converter.convert("42").unwrap(), "42.00");
assert_eq!(converter.convert("3.14159").unwrap(), "3.14");
}
#[test]
fn test_number_format_converter_with_prefix_suffix() {
let converter = NumberFormatConverter::new()
.decimals(2)
.prefix("$")
.suffix(" USD");
assert_eq!(converter.convert("100").unwrap(), "$100.00 USD");
assert_eq!(converter.convert_back("$100.00 USD").unwrap(), "100.00");
}
#[test]
fn test_number_format_converter_error() {
let converter = NumberFormatConverter::new();
assert!(converter.convert("not a number").is_err());
}
#[test]
fn test_conversion_error_display() {
let err = ConversionError {
message: "test error".to_string(),
};
assert!(err.to_string().contains("test error"));
}
#[test]
fn test_widget_update_struct() {
let update = WidgetUpdate {
widget_id: "input1".to_string(),
property: "value".to_string(),
value: "Hello".to_string(),
};
assert_eq!(update.widget_id, "input1");
}
#[test]
fn test_state_update_struct() {
let update = StateUpdate {
path: PropertyPath::new("user.name"),
value: "Alice".to_string(),
};
assert_eq!(update.path.to_string_path(), "user.name");
}
#[test]
fn test_binding_id_default() {
let id = BindingId::default();
assert_eq!(id.0, 0);
}
#[test]
fn test_update_source_eq() {
assert_eq!(UpdateSource::State, UpdateSource::State);
assert_eq!(UpdateSource::Widget, UpdateSource::Widget);
assert_ne!(UpdateSource::State, UpdateSource::Widget);
}
#[test]
fn test_property_path_empty_string() {
let path = PropertyPath::new("");
assert!(path.is_empty());
assert!(path.is_root());
}
#[test]
fn test_property_path_trailing_dots() {
let path = PropertyPath::new("user.name.");
assert_eq!(path.segments(), &["user", "name"]);
}
#[test]
fn test_property_path_leading_dots() {
let path = PropertyPath::new(".user.name");
assert_eq!(path.segments(), &["user", "name"]);
}
#[test]
fn test_property_path_multiple_dots() {
let path = PropertyPath::new("user..name");
assert_eq!(path.segments(), &["user", "name"]);
}
#[test]
fn test_property_path_parent_of_root() {
let path = PropertyPath::root();
assert!(path.parent().is_none());
}
#[test]
fn test_property_path_leaf_of_root() {
let path = PropertyPath::root();
assert!(path.leaf().is_none());
}
#[test]
fn test_property_path_single_segment() {
let path = PropertyPath::new("count");
assert_eq!(path.len(), 1);
assert_eq!(path.leaf(), Some("count"));
let parent = path.parent().unwrap();
assert!(parent.is_empty());
}
#[test]
fn test_property_path_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(PropertyPath::new("user.name"));
set.insert(PropertyPath::new("user.email"));
assert!(set.contains(&PropertyPath::new("user.name")));
assert!(!set.contains(&PropertyPath::new("other")));
}
#[test]
fn test_property_path_clone() {
let path = PropertyPath::new("a.b.c");
let cloned = path.clone();
assert_eq!(path, cloned);
}
#[test]
fn test_property_path_debug() {
let path = PropertyPath::new("user.name");
let debug = format!("{:?}", path);
assert!(debug.contains("PropertyPath"));
}
#[test]
fn test_property_path_serialize() {
let path = PropertyPath::new("user.name");
let json = serde_json::to_string(&path).unwrap();
assert!(json.contains("user"));
assert!(json.contains("name"));
}
#[test]
fn test_property_path_deserialize() {
let json = r#"{"segments":["user","profile"]}"#;
let path: PropertyPath = serde_json::from_str(json).unwrap();
assert_eq!(path.to_string_path(), "user.profile");
}
#[test]
fn test_binding_direction_default() {
assert_eq!(BindingDirection::default(), BindingDirection::OneWay);
}
#[test]
fn test_binding_direction_all_variants() {
assert_eq!(BindingDirection::OneWay, BindingDirection::OneWay);
assert_eq!(BindingDirection::TwoWay, BindingDirection::TwoWay);
assert_eq!(BindingDirection::OneTime, BindingDirection::OneTime);
}
#[test]
fn test_binding_direction_clone() {
let dir = BindingDirection::TwoWay;
let cloned = dir;
assert_eq!(dir, cloned);
}
#[test]
fn test_binding_direction_debug() {
let dir = BindingDirection::OneTime;
let debug = format!("{:?}", dir);
assert!(debug.contains("OneTime"));
}
#[test]
fn test_binding_config_chained_builders() {
let config = BindingConfig::one_way("count", "label")
.transform("toString")
.fallback("N/A");
assert_eq!(config.transform, Some("toString".to_string()));
assert_eq!(config.fallback, Some("N/A".to_string()));
}
#[test]
fn test_binding_config_clone() {
let config = BindingConfig::two_way("user.name", "value");
let cloned = config.clone();
assert_eq!(cloned.direction, BindingDirection::TwoWay);
}
#[test]
fn test_binding_config_debug() {
let config = BindingConfig::one_way("path", "prop");
let debug = format!("{:?}", config);
assert!(debug.contains("BindingConfig"));
}
#[test]
fn test_reactive_cell_multiple_subscribers() {
use std::sync::atomic::{AtomicI32, Ordering};
let cell = ReactiveCell::new(0);
let count1 = Arc::new(AtomicI32::new(0));
let count2 = Arc::new(AtomicI32::new(0));
let c1 = count1.clone();
let c2 = count2.clone();
cell.subscribe(move |v| {
c1.store(*v, Ordering::SeqCst);
});
cell.subscribe(move |v| {
c2.store(*v * 2, Ordering::SeqCst);
});
cell.set(10);
assert_eq!(count1.load(Ordering::SeqCst), 10);
assert_eq!(count2.load(Ordering::SeqCst), 20);
}
#[test]
fn test_reactive_cell_debug() {
let cell = ReactiveCell::new(42);
let debug = format!("{:?}", cell);
assert!(debug.contains("ReactiveCell"));
assert!(debug.contains("42"));
}
#[test]
fn test_reactive_cell_string() {
let cell = ReactiveCell::new("hello".to_string());
cell.set("world".to_string());
assert_eq!(cell.get(), "world");
}
#[test]
fn test_computed_with_closure_capture() {
let base = 10;
let computed = Computed::new(move || base * 2);
assert_eq!(computed.get(), 20);
}
#[test]
fn test_computed_debug() {
let computed = Computed::new(|| 42);
computed.get(); let debug = format!("{:?}", computed);
assert!(debug.contains("Computed"));
}
#[test]
fn test_computed_invalidate_recomputes() {
use std::sync::atomic::{AtomicI32, Ordering};
let counter = Arc::new(AtomicI32::new(0));
let counter_clone = counter.clone();
let computed = Computed::new(move || counter_clone.fetch_add(1, Ordering::SeqCst) + 1);
assert_eq!(computed.get(), 1);
assert_eq!(computed.get(), 1);
computed.invalidate();
assert_eq!(computed.get(), 2); }
#[test]
fn test_binding_expression_no_deps() {
let expr = BindingExpression::new("Hello World");
assert!(expr.dependencies.is_empty());
}
#[test]
fn test_binding_expression_complex() {
let expr = BindingExpression::new("{{ user.name | uppercase }} ({{ user.age }})");
assert_eq!(expr.dependencies.len(), 2);
}
#[test]
fn test_binding_expression_not_simple() {
let expr = BindingExpression::new("Hello {{ name }}!");
assert!(!expr.is_simple_property());
assert!(expr.as_property().is_none());
}
#[test]
fn test_binding_expression_clone() {
let expr = BindingExpression::property("count");
let cloned = expr.clone();
assert_eq!(cloned.expression, expr.expression);
}
#[test]
fn test_binding_expression_debug() {
let expr = BindingExpression::new("{{ test }}");
let debug = format!("{:?}", expr);
assert!(debug.contains("BindingExpression"));
}
#[test]
fn test_binding_expression_serialize() {
let expr = BindingExpression::property("count");
let json = serde_json::to_string(&expr).unwrap();
assert!(json.contains("expression"));
}
#[test]
fn test_event_binding_clone() {
let binding = EventBinding::on_click(ActionBinding::toggle("visible"));
let cloned = binding.clone();
assert_eq!(cloned.event, "click");
}
#[test]
fn test_event_binding_debug() {
let binding = EventBinding::new("submit", ActionBinding::dispatch("send"));
let debug = format!("{:?}", binding);
assert!(debug.contains("EventBinding"));
}
#[test]
fn test_event_binding_serialize() {
let binding = EventBinding::on_click(ActionBinding::toggle("flag"));
let json = serde_json::to_string(&binding).unwrap();
assert!(json.contains("click"));
}
#[test]
fn test_action_binding_empty_batch() {
let action = ActionBinding::batch([]);
if let ActionBinding::Batch { actions } = action {
assert!(actions.is_empty());
} else {
panic!("Expected Batch");
}
}
#[test]
fn test_action_binding_clone() {
let action = ActionBinding::set("path", "value");
let cloned = action.clone();
if let ActionBinding::SetProperty { path, .. } = cloned {
assert_eq!(path.to_string_path(), "path");
}
}
#[test]
fn test_action_binding_debug() {
let action = ActionBinding::toggle("flag");
let debug = format!("{:?}", action);
assert!(debug.contains("ToggleProperty"));
}
#[test]
fn test_action_binding_serialize() {
let action = ActionBinding::increment("counter");
let json = serde_json::to_string(&action).unwrap();
assert!(json.contains("IncrementProperty"));
}
#[test]
fn test_binding_manager_default() {
let manager = BindingManager::default();
assert_eq!(manager.active_count(), 0);
assert!(manager.debounce_ms.is_none());
}
#[test]
fn test_binding_manager_multiple_registers() {
let mut manager = BindingManager::new();
let id1 = manager.register("w1", BindingConfig::one_way("a", "b"));
let id2 = manager.register("w1", BindingConfig::one_way("c", "d"));
assert_ne!(id1.0, id2.0);
assert_eq!(manager.active_count(), 2);
}
#[test]
fn test_binding_manager_unregister_nonexistent() {
let mut manager = BindingManager::new();
manager.unregister(BindingId(999)); }
#[test]
fn test_binding_manager_inactive_not_counted() {
let mut manager = BindingManager::new();
let id = manager.register("w1", BindingConfig::one_way("a", "b"));
manager.register("w2", BindingConfig::one_way("c", "d"));
manager.unregister(id);
assert_eq!(manager.active_count(), 1);
}
#[test]
fn test_binding_manager_bindings_for_widget_empty() {
let manager = BindingManager::new();
assert!(manager.bindings_for_widget("nonexistent").is_empty());
}
#[test]
fn test_binding_manager_bindings_for_path_empty() {
let manager = BindingManager::new();
let path = PropertyPath::new("nonexistent");
assert!(manager.bindings_for_path(&path).is_empty());
}
#[test]
fn test_binding_manager_on_state_change_nested_path() {
let mut manager = BindingManager::new();
manager.register("w1", BindingConfig::one_way("user", "data"));
let path = PropertyPath::new("user.name");
let updates = manager.on_state_change(&path, "Alice");
assert_eq!(updates.len(), 1);
}
#[test]
fn test_binding_manager_on_state_change_inactive() {
let mut manager = BindingManager::new();
let id = manager.register("w1", BindingConfig::one_way("count", "text"));
manager.unregister(id);
let path = PropertyPath::new("count");
let updates = manager.on_state_change(&path, "42");
assert!(updates.is_empty());
}
#[test]
fn test_binding_manager_queue_widget_update() {
let mut manager = BindingManager::new();
manager.queue_update(
UpdateSource::Widget,
PropertyPath::new("field"),
"value".to_string(),
);
let (widget_updates, state_updates) = manager.flush();
assert!(widget_updates.is_empty());
assert_eq!(state_updates.len(), 1);
}
#[test]
fn test_binding_manager_debug() {
let manager = BindingManager::new();
let debug = format!("{:?}", manager);
assert!(debug.contains("BindingManager"));
}
#[test]
fn test_active_binding_clone() {
let binding = ActiveBinding {
id: BindingId(1),
widget_id: "widget".to_string(),
config: BindingConfig::one_way("path", "prop"),
current_value: Some("value".to_string()),
active: true,
};
let cloned = binding.clone();
assert_eq!(cloned.id, BindingId(1));
}
#[test]
fn test_active_binding_debug() {
let binding = ActiveBinding {
id: BindingId(0),
widget_id: "w".to_string(),
config: BindingConfig::one_way("a", "b"),
current_value: None,
active: true,
};
let debug = format!("{:?}", binding);
assert!(debug.contains("ActiveBinding"));
}
#[test]
fn test_pending_update_clone() {
let update = PendingUpdate {
source: UpdateSource::State,
path: PropertyPath::new("count"),
value: "42".to_string(),
timestamp: 12345,
};
let cloned = update.clone();
assert_eq!(cloned.timestamp, 12345);
}
#[test]
fn test_pending_update_debug() {
let update = PendingUpdate {
source: UpdateSource::Widget,
path: PropertyPath::new("field"),
value: "val".to_string(),
timestamp: 0,
};
let debug = format!("{:?}", update);
assert!(debug.contains("PendingUpdate"));
}
#[test]
fn test_conversion_error_debug() {
let err = ConversionError {
message: "test".to_string(),
};
let debug = format!("{:?}", err);
assert!(debug.contains("ConversionError"));
}
#[test]
fn test_conversion_error_clone() {
let err = ConversionError {
message: "original".to_string(),
};
let cloned = err.clone();
assert_eq!(cloned.message, "original");
}
#[test]
fn test_identity_converter_default() {
let converter = IdentityConverter::default();
assert_eq!(converter.convert("test").unwrap(), "test");
}
#[test]
fn test_identity_converter_debug() {
let converter = IdentityConverter;
let debug = format!("{:?}", converter);
assert!(debug.contains("IdentityConverter"));
}
#[test]
fn test_bool_converter_yes_no() {
let converter = BoolToStringConverter::new();
assert_eq!(converter.convert("yes").unwrap(), "true");
assert_eq!(converter.convert("no").unwrap(), "false");
}
#[test]
fn test_bool_converter_default() {
let converter = BoolToStringConverter::default();
assert_eq!(converter.true_string, "");
assert_eq!(converter.false_string, "");
}
#[test]
fn test_bool_converter_debug() {
let converter = BoolToStringConverter::new();
let debug = format!("{:?}", converter);
assert!(debug.contains("BoolToStringConverter"));
}
#[test]
fn test_bool_converter_convert_back_error() {
let converter = BoolToStringConverter::with_strings("Y", "N");
assert!(converter.convert_back("Maybe").is_err());
}
#[test]
fn test_number_format_default() {
let converter = NumberFormatConverter::default();
assert_eq!(converter.decimals, 0);
assert!(converter.prefix.is_empty());
assert!(converter.suffix.is_empty());
}
#[test]
fn test_number_format_debug() {
let converter = NumberFormatConverter::new().decimals(2);
let debug = format!("{:?}", converter);
assert!(debug.contains("NumberFormatConverter"));
}
#[test]
fn test_number_format_negative() {
let converter = NumberFormatConverter::new().decimals(2);
assert_eq!(converter.convert("-42.5").unwrap(), "-42.50");
}
#[test]
fn test_number_format_convert_back_error() {
let converter = NumberFormatConverter::new();
assert!(converter.convert_back("not-a-number").is_err());
}
#[test]
fn test_number_format_strip_partial() {
let converter = NumberFormatConverter::new().prefix("$");
assert_eq!(converter.convert_back("100").unwrap(), "100");
}
}