Skip to main content

apimock_config/workspace/
edit.rs

1//! `Workspace::apply()` dispatch, plus the per-command handlers split
2//! across sibling modules (RFC 043).
3//!
4//! # Layout
5//!
6//! `apply()` below is the only public surface: a `match` over
7//! `EditCommand` that dispatches to one `cmd_*` method per variant.
8//! The `cmd_*` methods themselves live in sibling modules, grouped by
9//! the same seams the RFCs that added them already drew:
10//!
11//! - [`rule_set`] — add/remove a rule set, and the RFC 025 per-rule-set
12//!   strategy override.
13//! - [`rule`] — add/update/delete/move a rule, and update its `respond`.
14//! - [`root_setting`] — `cmd_update_root_setting`, kept as one function
15//!   (171 lines, the largest body in the crate) since decomposing it is
16//!   a question about that function, not about module layout.
17//! - [`condition`] — RFC 016's six header/body condition commands.
18//!
19//! Every `cmd_*` method is `pub(super)`, visible only within
20//! `workspace::edit` and its descendants — none of them is part of
21//! this crate's public API, and the split doesn't change that.
22//!
23//! ID migration helpers (the `shift_*` and `reorder_*` methods) live
24//! in [`id_shift`] because they're a self-contained concern: they
25//! don't read or mutate `self.config` directly, only `self.ids`.
26//! Splitting them out makes the `cmd_*` bodies shorter and the helpers
27//! separately testable.
28//!
29//! Payload-to-model converters live in [`payload`] — pure functions
30//! translating GUI-shaped `EditValue` / `RulePayload` into the routing
31//! crate's runtime types.
32
33pub mod condition;
34pub mod id_shift;
35pub mod payload;
36pub mod root_setting;
37pub mod rule;
38pub mod rule_set;
39
40use crate::error::ApplyError;
41use crate::view::{ApplyResult, EditCommand};
42
43use super::Workspace;
44
45impl Workspace {
46    /// Apply one edit command, mutating the in-memory workspace.
47    ///
48    /// # Shape of the implementation
49    ///
50    /// Each `EditCommand` variant maps to a small helper method. The
51    /// helpers return `Result<Vec<NodeId>, ApplyError>`; `apply` wraps
52    /// the ok-path in an `ApplyResult` with the right `requires_reload`
53    /// flag and reruns validation so the result carries up-to-date
54    /// diagnostics.
55    ///
56    /// # ID stability on structural changes
57    ///
58    /// Commands that change positional layout (Remove / Delete / Move
59    /// / Add) touch `self.ids` carefully so NodeIds that refer to the
60    /// *same logical node* survive the operation. For example, after
61    /// `RemoveRuleSet { id }` at index `i`, rule sets at positions
62    /// `i+1..` shift down by one: the code below explicitly migrates
63    /// their IDs so a GUI that selected rule-set #3 before the edit
64    /// still has the same ID pointing at what is now rule-set #2.
65    pub fn apply(&mut self, cmd: EditCommand) -> Result<ApplyResult, ApplyError> {
66        let (changed_nodes, requires_reload) = match cmd {
67            EditCommand::AddRuleSet { path } => {
68                let ids = self.cmd_add_rule_set(path)?;
69                (ids, true)
70            }
71            EditCommand::RemoveRuleSet { id } => {
72                let ids = self.cmd_remove_rule_set(id)?;
73                (ids, true)
74            }
75            EditCommand::AddRule { parent, rule } => {
76                let ids = self.cmd_add_rule(parent, rule)?;
77                (ids, true)
78            }
79            EditCommand::UpdateRule { id, rule } => {
80                let ids = self.cmd_update_rule(id, rule)?;
81                (ids, true)
82            }
83            EditCommand::DeleteRule { id } => {
84                let ids = self.cmd_delete_rule(id)?;
85                (ids, true)
86            }
87            EditCommand::MoveRule { id, new_index } => {
88                let ids = self.cmd_move_rule(id, new_index)?;
89                (ids, true)
90            }
91            EditCommand::UpdateRespond { id, respond } => {
92                let ids = self.cmd_update_respond(id, respond)?;
93                (ids, true)
94            }
95            EditCommand::UpdateRootSetting { key, value } => {
96                let ids = self.cmd_update_root_setting(key, value)?;
97                (ids, true)
98            }
99
100            // ── Per-condition commands (RFC 016) ──────────────────────
101            EditCommand::AddHeaderCondition { rule_id, condition } => {
102                let ids = self.cmd_add_header_condition(rule_id, condition)?;
103                (ids, true)
104            }
105            EditCommand::UpdateHeaderCondition { id, condition } => {
106                let ids = self.cmd_update_header_condition(id, condition)?;
107                (ids, true)
108            }
109            EditCommand::RemoveHeaderCondition { id } => {
110                let ids = self.cmd_remove_header_condition(id)?;
111                (ids, true)
112            }
113            EditCommand::AddBodyCondition { rule_id, condition } => {
114                let ids = self.cmd_add_body_condition(rule_id, condition)?;
115                (ids, true)
116            }
117            EditCommand::UpdateBodyCondition { id, condition } => {
118                let ids = self.cmd_update_body_condition(id, condition)?;
119                (ids, true)
120            }
121            EditCommand::RemoveBodyCondition { id } => {
122                let ids = self.cmd_remove_body_condition(id)?;
123                (ids, true)
124            }
125            EditCommand::UpdateRuleSetStrategy { id, strategy } => {
126                let ids = self.cmd_update_rule_set_strategy(id, strategy)?;
127                (ids, true) // SoftReload — strategy change takes effect at next match
128            }
129        };
130
131        // After any mutation, refresh per-node validation so the
132        // `ApplyResult.diagnostics` reflects the new state. This is the
133        // Step-3 piece: validation is now per-node and GUI-ready, not a
134        // bare boolean.
135        let diagnostics = self.collect_diagnostics();
136
137        Ok(ApplyResult {
138            changed_nodes,
139            diagnostics,
140            requires_reload,
141        })
142    }
143}