apimock_config/workspace/
address.rs1use crate::view::{DiffItem, NodeId};
29
30use super::Workspace;
31use super::id_index::NodeAddress;
32
33impl Workspace {
34 pub fn rule_set_id_at(&self, rule_set: usize) -> Option<NodeId> {
36 self.ids.id_for(NodeAddress::RuleSet { rule_set })
37 }
38
39 pub fn rule_id_at(&self, rule_set: usize, rule: usize) -> Option<NodeId> {
44 self.ids.id_for(NodeAddress::Rule { rule_set, rule })
45 }
46
47 pub fn respond_id_at(&self, rule_set: usize, rule: usize) -> Option<NodeId> {
52 self.ids.id_for(NodeAddress::Respond { rule_set, rule })
53 }
54
55 pub fn describe(&self, id: NodeId) -> Option<String> {
69 let rule_set_label = |rule_set: usize| -> String {
70 self.config
71 .service
72 .rule_sets
73 .get(rule_set)
74 .map(|rs| rs.file_path.clone())
75 .unwrap_or_else(|| format!("rule set #{rule_set}"))
76 };
77 let addr = self.ids.lookup(id)?;
78 Some(match addr {
79 NodeAddress::Root => "root config".to_owned(),
80 NodeAddress::RuleSet { rule_set } => {
81 format!("rule set `{}`", rule_set_label(rule_set))
82 }
83 NodeAddress::Rule { rule_set, rule } => {
84 format!("rule set `{}`, rule #{rule}", rule_set_label(rule_set))
85 }
86 NodeAddress::Respond { rule_set, rule } => {
87 format!(
88 "rule set `{}`, rule #{rule} respond",
89 rule_set_label(rule_set)
90 )
91 }
92 NodeAddress::Middleware { middleware } => format!("middleware #{middleware}"),
93 NodeAddress::FallbackRespondDir => "fallback_respond_dir".to_owned(),
94 NodeAddress::HeaderCondition {
95 rule_set,
96 rule,
97 header_name,
98 } => format!(
99 "rule set `{}`, rule #{rule}, header `{header_name}`",
100 rule_set_label(rule_set)
101 ),
102 NodeAddress::BodyCondition {
103 rule_set,
104 rule,
105 path,
106 } => format!(
107 "rule set `{}`, rule #{rule}, body `{path}`",
108 rule_set_label(rule_set)
109 ),
110 })
111 }
112
113 pub fn preview_changes(&self) -> Vec<DiffItem> {
119 self.compute_diff_summary()
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use std::path::PathBuf;
126
127 use crate::{
128 Workspace,
129 view::{EditCommand, RespondPayload, RulePayload},
130 };
131
132 fn workspace_with_two_rules() -> (tempfile::TempDir, PathBuf) {
137 let dir = tempfile::tempdir().expect("tempdir");
138 let rs_toml = concat!(
139 "[[rules]]\n",
140 "when.request.url_path = \"/a\"\n",
141 "respond = { text = \"a\" }\n",
142 "\n",
143 "[[rules]]\n",
144 "when.request.url_path = \"/b\"\n",
145 "respond = { text = \"b\" }\n",
146 );
147 let rs_path = dir.path().join("apimock-rule-set.toml");
148 std::fs::write(&rs_path, rs_toml).unwrap();
149 let root_toml =
150 "[service]\nrule_sets = [\"apimock-rule-set.toml\"]\nfallback_respond_dir = \".\"\n";
151 let root_path = dir.path().join("apimock.toml");
152 std::fs::write(&root_path, root_toml).unwrap();
153 (dir, root_path)
154 }
155
156 #[test]
157 fn rule_set_id_at_resolves_a_freshly_loaded_rule_set() {
158 let (_dir, root) = workspace_with_two_rules();
159 let ws = Workspace::load(root).expect("load");
160 assert!(ws.rule_set_id_at(0).is_some());
161 assert!(ws.rule_set_id_at(1).is_none(), "only one rule set exists");
162 }
163
164 #[test]
165 fn rule_id_at_resolves_a_rule_seeded_at_load_not_just_one_added_this_session() {
166 let (_dir, root) = workspace_with_two_rules();
167 let ws = Workspace::load(root).expect("load");
168 assert!(ws.rule_id_at(0, 0).is_some());
169 assert!(ws.rule_id_at(0, 1).is_some());
170 assert!(
171 ws.rule_id_at(0, 2).is_none(),
172 "out-of-range rule index must resolve to None, not panic"
173 );
174 }
175
176 #[test]
177 fn describe_never_contains_a_uuid_shaped_substring() {
178 let (_dir, root) = workspace_with_two_rules();
179 let mut ws = Workspace::load(root).expect("load");
180 let parent = ws.rule_set_id_at(0).expect("rule set 0 exists");
181 let result = ws
182 .apply(EditCommand::AddRule {
183 parent,
184 rule: RulePayload {
185 url_path: Some("/new".to_owned()),
186 url_path_op: None,
187 method: None,
188 priority: None,
189 headers: None,
190 body: None,
191 respond: RespondPayload {
192 text: Some("ok".to_owned()),
193 ..Default::default()
194 },
195 },
196 })
197 .expect("apply");
198
199 for id in result.changed_nodes {
200 let label = ws.describe(id).expect("every changed node describes");
201 assert!(
202 !looks_like_a_uuid(&label),
203 "describe() must never render a UUID: {label}"
204 );
205 }
206 }
207
208 fn looks_like_a_uuid(s: &str) -> bool {
209 let groups: Vec<&str> = s.split('-').collect();
211 groups.len() >= 5
212 && groups.windows(5).any(|w| {
213 w.iter()
214 .all(|g| !g.is_empty() && g.chars().all(|c| c.is_ascii_hexdigit()))
215 })
216 }
217
218 #[test]
219 fn preview_changes_matches_what_save_would_report() {
220 let (_dir, root) = workspace_with_two_rules();
221 let mut ws = Workspace::load(root).expect("load");
222 let parent = ws.rule_set_id_at(0).expect("rule set 0 exists");
223 ws.apply(EditCommand::AddRule {
224 parent,
225 rule: RulePayload {
226 url_path: Some("/preview".to_owned()),
227 url_path_op: None,
228 method: None,
229 priority: None,
230 headers: None,
231 body: None,
232 respond: RespondPayload {
233 text: Some("ok".to_owned()),
234 ..Default::default()
235 },
236 },
237 })
238 .expect("apply");
239
240 let preview = ws.preview_changes();
241 assert!(!preview.is_empty(), "an applied add should show in preview");
242 let save = ws.save().expect("save");
243 assert_eq!(
244 preview.len(),
245 save.diff_summary.len(),
246 "preview_changes must match what save() then actually reports"
247 );
248 }
249}