Skip to main content

rosace_widgets/template/
swap.rs

1//! Applying a hot-swap (D103 / D102 Tier 1 — rollout step 4).
2//!
3//! The watcher hands an edited [`Template`] (from `parse_file_templates`) to
4//! [`apply_swap`], which is the one place the diff safety-gate meets the live
5//! registry. If the edit is a safe data swap, it REPLACES the site's registry
6//! entry; the next frame's `view!` inflates the new descriptor with that
7//! frame's compiled holes (no in-place tree surgery — the reactive rebuild does
8//! the work). If the edit touched compiled logic, it escalates instead, leaving
9//! the running descriptor untouched so nothing breaks before a Tier 0 restart.
10
11use super::diff::{diff, EscalationReason, TemplateDiff};
12use super::registry;
13use super::Template;
14
15/// The result of trying to apply an edited template to the running app.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum SwapOutcome {
18    /// Safe data edit — the registry entry was replaced; next frame shows it.
19    Applied,
20    /// The edit didn't change the shape — nothing to do.
21    Unchanged,
22    /// The edit touched compiled logic — registry left as-is; the caller must
23    /// escalate (Tier 2 dylib swap or Tier 0 restart).
24    Escalate(EscalationReason),
25    /// No running template for this site key — nothing to swap against (a new
26    /// `view!`, or a key that never registered). Also an escalation.
27    UnknownSite,
28}
29
30/// Diff an edited template against the running one and, if it is a safe data
31/// swap, install it. Keyed by `new.key`.
32pub fn apply_swap(new: Template) -> SwapOutcome {
33    match registry::get(&new.key) {
34        None => SwapOutcome::UnknownSite,
35        Some(running) => match diff(&running, &new) {
36            TemplateDiff::Unchanged => SwapOutcome::Unchanged,
37            TemplateDiff::Swappable => {
38                registry::register(new);
39                SwapOutcome::Applied
40            }
41            TemplateDiff::Escalate(reason) => SwapOutcome::Escalate(reason),
42        },
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use crate::template::{registry, PropValue, StaticValue, Template, TemplateKey, TemplateNode};
50
51    fn col(key: &TemplateKey, spacing: PropValue) -> Template {
52        Template::new(key.clone(), TemplateNode::new("Column").with_prop("spacing", spacing))
53    }
54
55    // Small helper on TemplateNode-by-value for the tests.
56    trait WithProp {
57        fn with_prop(self, k: &str, v: PropValue) -> Self;
58    }
59    impl WithProp for TemplateNode {
60        fn with_prop(mut self, k: &str, v: PropValue) -> Self {
61            self.props.push((k.to_string(), v));
62            self
63        }
64    }
65
66    #[test]
67    fn safe_static_edit_is_applied_and_replaces_the_registry_entry() {
68        let key = TemplateKey::new("src/swap_a.rs", 1, 1);
69        registry::register(col(&key, PropValue::Static(StaticValue::Float(4.0))));
70
71        let edited = col(&key, PropValue::Static(StaticValue::Float(40.0)));
72        assert_eq!(apply_swap(edited), SwapOutcome::Applied);
73
74        // The registry now holds the edited value.
75        let now = registry::get(&key).unwrap();
76        assert_eq!(now.root.props[0].1, PropValue::Static(StaticValue::Float(40.0)));
77    }
78
79    #[test]
80    fn adding_a_hole_escalates_and_leaves_the_registry_untouched() {
81        let key = TemplateKey::new("src/swap_b.rs", 2, 1);
82        registry::register(col(&key, PropValue::Static(StaticValue::Float(4.0))));
83
84        // Edit turns spacing into a hole → hole count 0→1 → escalate.
85        let edited = col(&key, PropValue::Hole(0));
86        assert_eq!(
87            apply_swap(edited),
88            SwapOutcome::Escalate(EscalationReason::HoleCountChanged { old: 0, new: 1 })
89        );
90        // Running descriptor unchanged.
91        assert_eq!(
92            registry::get(&key).unwrap().root.props[0].1,
93            PropValue::Static(StaticValue::Float(4.0))
94        );
95    }
96
97    #[test]
98    fn unknown_site_is_reported() {
99        let key = TemplateKey::new("src/never_registered_swap.rs", 99, 1);
100        assert_eq!(apply_swap(col(&key, PropValue::Static(StaticValue::Float(1.0)))), SwapOutcome::UnknownSite);
101    }
102
103    #[test]
104    fn identical_edit_is_unchanged() {
105        let key = TemplateKey::new("src/swap_c.rs", 3, 1);
106        registry::register(col(&key, PropValue::Static(StaticValue::Float(4.0))));
107        assert_eq!(apply_swap(col(&key, PropValue::Static(StaticValue::Float(4.0)))), SwapOutcome::Unchanged);
108    }
109}