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}