apimock_config/workspace/
edit.rs1pub mod id_shift;
22pub mod payload;
23
24use std::path::Path;
25
26use apimock_routing::RuleSet;
27
28use crate::error::ApplyError;
29use crate::view::{ApplyResult, EditCommand, EditValue, NodeId};
30
31use super::Workspace;
32use super::id_index::NodeAddress;
33use payload::{
34 build_rule_from_payload, build_respond_from_payload, internal_path_err, value_as_integer,
35 value_as_string,
36};
37
38impl Workspace {
39 pub fn apply(&mut self, cmd: EditCommand) -> Result<ApplyResult, ApplyError> {
59 let (changed_nodes, requires_reload) = match cmd {
60 EditCommand::AddRuleSet { path } => {
61 let ids = self.cmd_add_rule_set(path)?;
62 (ids, true)
63 }
64 EditCommand::RemoveRuleSet { id } => {
65 let ids = self.cmd_remove_rule_set(id)?;
66 (ids, true)
67 }
68 EditCommand::AddRule { parent, rule } => {
69 let ids = self.cmd_add_rule(parent, rule)?;
70 (ids, true)
71 }
72 EditCommand::UpdateRule { id, rule } => {
73 let ids = self.cmd_update_rule(id, rule)?;
74 (ids, true)
75 }
76 EditCommand::DeleteRule { id } => {
77 let ids = self.cmd_delete_rule(id)?;
78 (ids, true)
79 }
80 EditCommand::MoveRule { id, new_index } => {
81 let ids = self.cmd_move_rule(id, new_index)?;
82 (ids, true)
83 }
84 EditCommand::UpdateRespond { id, respond } => {
85 let ids = self.cmd_update_respond(id, respond)?;
86 (ids, true)
87 }
88 EditCommand::UpdateRootSetting { key, value } => {
89 let ids = self.cmd_update_root_setting(key, value)?;
90 (ids, true)
96 }
97 };
98
99 let diagnostics = self.collect_diagnostics();
104
105 Ok(ApplyResult {
106 changed_nodes,
107 diagnostics,
108 requires_reload,
109 })
110 }
111
112 fn cmd_add_rule_set(&mut self, path: String) -> Result<Vec<NodeId>, ApplyError> {
115 let relative_dir = self.config_relative_dir().map_err(internal_path_err)?;
118 let joined = Path::new(&relative_dir).join(&path);
119 let path_str = joined.to_str().ok_or_else(|| ApplyError::InvalidPayload {
120 reason: format!(
121 "path contains non-UTF-8 bytes: {}",
122 joined.to_string_lossy()
123 ),
124 })?;
125
126 let next_idx = self.config.service.rule_sets.len();
127 let new_rule_set = RuleSet::new(path_str, relative_dir.as_str(), next_idx)
128 .map_err(|e| ApplyError::InvalidPayload {
129 reason: format!("failed to load rule set `{}`: {}", path, e),
130 })?;
131
132 let file_paths = self
135 .config
136 .service
137 .rule_sets_file_paths
138 .get_or_insert_with(Vec::new);
139 file_paths.push(path.clone());
140
141 let new_len = self.config.service.rule_sets.len() + 1;
142 self.config.service.rule_sets.push(new_rule_set);
143
144 let rs_addr = NodeAddress::RuleSet {
146 rule_set: next_idx,
147 };
148 let rs_id = self.ids.insert(rs_addr);
149 let mut changed = vec![rs_id];
150 let new_rs = &self.config.service.rule_sets[next_idx];
151 for rule_idx in 0..new_rs.rules.len() {
152 let r_id = self.ids.insert(NodeAddress::Rule {
153 rule_set: next_idx,
154 rule: rule_idx,
155 });
156 let resp_id = self.ids.insert(NodeAddress::Respond {
157 rule_set: next_idx,
158 rule: rule_idx,
159 });
160 changed.push(r_id);
161 changed.push(resp_id);
162 }
163 debug_assert_eq!(new_len, self.config.service.rule_sets.len());
166
167 Ok(changed)
168 }
169
170 fn cmd_remove_rule_set(&mut self, id: NodeId) -> Result<Vec<NodeId>, ApplyError> {
171 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
172 let NodeAddress::RuleSet { rule_set: idx } = addr else {
173 return Err(ApplyError::WrongNodeKind {
174 id,
175 reason: "expected a rule set id".to_owned(),
176 });
177 };
178
179 let len = self.config.service.rule_sets.len();
180 if idx >= len {
181 return Err(ApplyError::InvalidPayload {
182 reason: format!("rule set index {} out of range (len={})", idx, len),
183 });
184 }
185
186 let mut changed: Vec<NodeId> = Vec::new();
189 changed.push(id);
191 if let Some(removed_rs) = self.config.service.rule_sets.get(idx) {
192 for rule_idx in 0..removed_rs.rules.len() {
193 if let Some(r_id) = self.ids.id_for(NodeAddress::Rule {
194 rule_set: idx,
195 rule: rule_idx,
196 }) {
197 changed.push(r_id);
198 }
199 if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
200 rule_set: idx,
201 rule: rule_idx,
202 }) {
203 changed.push(resp_id);
204 }
205 }
206 }
207
208 self.config.service.rule_sets.remove(idx);
210 if let Some(paths) = self.config.service.rule_sets_file_paths.as_mut() {
211 if idx < paths.len() {
212 paths.remove(idx);
213 }
214 }
215
216 self.shift_rule_sets_down(idx);
221
222 for shifted_idx in idx..self.config.service.rule_sets.len() {
226 if let Some(shifted_id) = self
227 .ids
228 .id_for(NodeAddress::RuleSet {
229 rule_set: shifted_idx,
230 })
231 {
232 if !changed.contains(&shifted_id) {
233 changed.push(shifted_id);
234 }
235 }
236 }
237
238 Ok(changed)
239 }
240
241 fn cmd_add_rule(
242 &mut self,
243 parent: NodeId,
244 rule_payload: crate::view::RulePayload,
245 ) -> Result<Vec<NodeId>, ApplyError> {
246 let addr = self
247 .ids
248 .lookup(parent)
249 .ok_or(ApplyError::UnknownNode { id: parent })?;
250 let NodeAddress::RuleSet { rule_set: rs_idx } = addr else {
251 return Err(ApplyError::WrongNodeKind {
252 id: parent,
253 reason: "expected a rule set id (parent for AddRule must be a rule set)".to_owned(),
254 });
255 };
256
257 let rule_set = self
258 .config
259 .service
260 .rule_sets
261 .get_mut(rs_idx)
262 .ok_or_else(|| ApplyError::InvalidPayload {
263 reason: format!("rule set index {} out of range", rs_idx),
264 })?;
265
266 let new_rule = build_rule_from_payload(rule_payload, rule_set, rs_idx, None)?;
267 let new_rule_idx = rule_set.rules.len();
268 rule_set.rules.push(new_rule);
269
270 let r_id = self.ids.insert(NodeAddress::Rule {
271 rule_set: rs_idx,
272 rule: new_rule_idx,
273 });
274 let resp_id = self.ids.insert(NodeAddress::Respond {
275 rule_set: rs_idx,
276 rule: new_rule_idx,
277 });
278 Ok(vec![parent, r_id, resp_id])
279 }
280
281 fn cmd_update_rule(
282 &mut self,
283 id: NodeId,
284 rule_payload: crate::view::RulePayload,
285 ) -> Result<Vec<NodeId>, ApplyError> {
286 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
287 let NodeAddress::Rule {
288 rule_set: rs_idx,
289 rule: rule_idx,
290 } = addr
291 else {
292 return Err(ApplyError::WrongNodeKind {
293 id,
294 reason: "expected a rule id".to_owned(),
295 });
296 };
297
298 let rule_set = self
299 .config
300 .service
301 .rule_sets
302 .get_mut(rs_idx)
303 .ok_or_else(|| ApplyError::InvalidPayload {
304 reason: format!("rule set index {} out of range", rs_idx),
305 })?;
306
307 let existing = rule_set.rules.get(rule_idx).cloned();
312 let new_rule = build_rule_from_payload(
313 rule_payload,
314 rule_set,
315 rs_idx,
316 existing.as_ref(),
317 )?;
318 *rule_set
319 .rules
320 .get_mut(rule_idx)
321 .ok_or_else(|| ApplyError::InvalidPayload {
322 reason: format!("rule index {} out of range", rule_idx),
323 })? = new_rule;
324
325 let resp_id = self
326 .ids
327 .id_for(NodeAddress::Respond {
328 rule_set: rs_idx,
329 rule: rule_idx,
330 })
331 .unwrap_or_else(NodeId::new);
332 Ok(vec![id, resp_id])
333 }
334
335 fn cmd_delete_rule(&mut self, id: NodeId) -> Result<Vec<NodeId>, ApplyError> {
336 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
337 let NodeAddress::Rule {
338 rule_set: rs_idx,
339 rule: rule_idx,
340 } = addr
341 else {
342 return Err(ApplyError::WrongNodeKind {
343 id,
344 reason: "expected a rule id".to_owned(),
345 });
346 };
347
348 let rule_set = self
349 .config
350 .service
351 .rule_sets
352 .get_mut(rs_idx)
353 .ok_or_else(|| ApplyError::InvalidPayload {
354 reason: format!("rule set index {} out of range", rs_idx),
355 })?;
356
357 if rule_idx >= rule_set.rules.len() {
358 return Err(ApplyError::InvalidPayload {
359 reason: format!("rule index {} out of range", rule_idx),
360 });
361 }
362
363 let mut changed: Vec<NodeId> = Vec::new();
365 changed.push(id);
366 if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
367 rule_set: rs_idx,
368 rule: rule_idx,
369 }) {
370 changed.push(resp_id);
371 }
372
373 rule_set.rules.remove(rule_idx);
374 self.shift_rules_down(rs_idx, rule_idx);
375
376 let new_rule_count = self.config.service.rule_sets[rs_idx].rules.len();
378 for shifted_idx in rule_idx..new_rule_count {
379 if let Some(r_id) = self.ids.id_for(NodeAddress::Rule {
380 rule_set: rs_idx,
381 rule: shifted_idx,
382 }) {
383 if !changed.contains(&r_id) {
384 changed.push(r_id);
385 }
386 }
387 if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
388 rule_set: rs_idx,
389 rule: shifted_idx,
390 }) {
391 if !changed.contains(&resp_id) {
392 changed.push(resp_id);
393 }
394 }
395 }
396
397 Ok(changed)
398 }
399
400 fn cmd_move_rule(&mut self, id: NodeId, new_index: usize) -> Result<Vec<NodeId>, ApplyError> {
401 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
402 let NodeAddress::Rule {
403 rule_set: rs_idx,
404 rule: old_idx,
405 } = addr
406 else {
407 return Err(ApplyError::WrongNodeKind {
408 id,
409 reason: "expected a rule id".to_owned(),
410 });
411 };
412
413 let rule_set = self
414 .config
415 .service
416 .rule_sets
417 .get_mut(rs_idx)
418 .ok_or_else(|| ApplyError::InvalidPayload {
419 reason: format!("rule set index {} out of range", rs_idx),
420 })?;
421
422 if old_idx >= rule_set.rules.len() || new_index >= rule_set.rules.len() {
423 return Err(ApplyError::InvalidPayload {
424 reason: format!(
425 "move out of bounds: old_idx={}, new_index={}, len={}",
426 old_idx,
427 new_index,
428 rule_set.rules.len()
429 ),
430 });
431 }
432 if old_idx == new_index {
433 return Ok(vec![id]);
434 }
435
436 let rule = rule_set.rules.remove(old_idx);
438 rule_set.rules.insert(new_index, rule);
439
440 self.reorder_rule_ids(rs_idx, old_idx, new_index);
445
446 let lo = old_idx.min(new_index);
449 let hi = old_idx.max(new_index);
450 let mut changed: Vec<NodeId> = Vec::new();
451 for idx in lo..=hi {
452 if let Some(r_id) = self.ids.id_for(NodeAddress::Rule {
453 rule_set: rs_idx,
454 rule: idx,
455 }) {
456 changed.push(r_id);
457 }
458 if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
459 rule_set: rs_idx,
460 rule: idx,
461 }) {
462 changed.push(resp_id);
463 }
464 }
465 Ok(changed)
466 }
467
468 fn cmd_update_respond(
469 &mut self,
470 id: NodeId,
471 respond: crate::view::RespondPayload,
472 ) -> Result<Vec<NodeId>, ApplyError> {
473 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
474 let NodeAddress::Respond {
475 rule_set: rs_idx,
476 rule: rule_idx,
477 } = addr
478 else {
479 return Err(ApplyError::WrongNodeKind {
480 id,
481 reason: "expected a respond id".to_owned(),
482 });
483 };
484
485 let rule = self
486 .config
487 .service
488 .rule_sets
489 .get_mut(rs_idx)
490 .and_then(|rs| rs.rules.get_mut(rule_idx))
491 .ok_or_else(|| ApplyError::InvalidPayload {
492 reason: format!(
493 "rule at rule_set={}, rule={} not found",
494 rs_idx, rule_idx
495 ),
496 })?;
497
498 rule.respond = build_respond_from_payload(respond);
499
500 let rule_set = &self.config.service.rule_sets[rs_idx];
503 let derived = rule_set.rules[rule_idx].compute_derived_fields(rule_set, rule_idx, rs_idx);
504 self.config.service.rule_sets[rs_idx].rules[rule_idx] = derived;
505
506 Ok(vec![id])
507 }
508
509 fn cmd_update_root_setting(
510 &mut self,
511 key: crate::view::RootSettingKey,
512 value: EditValue,
513 ) -> Result<Vec<NodeId>, ApplyError> {
514 use crate::view::RootSettingKey::*;
515
516 match key {
517 ListenerIpAddress => {
518 let s = value_as_string(&value)?;
519 let listener = self.config.listener.get_or_insert_with(Default::default);
520 listener.ip_address = s;
521 }
522 ListenerPort => {
523 let n = value_as_integer(&value)?;
524 if !(0..=u16::MAX as i64).contains(&n) {
525 return Err(ApplyError::InvalidPayload {
526 reason: format!("port {} not in 0..=65535", n),
527 });
528 }
529 let listener = self.config.listener.get_or_insert_with(Default::default);
530 listener.port = n as u16;
531 }
532 ServiceFallbackRespondDir => {
533 let s = value_as_string(&value)?;
534 self.config.service.fallback_respond_dir = s;
535 }
536 ServiceStrategy => {
537 let s = value_as_string(&value)?;
538 match s.as_str() {
542 "first_match" => {
543 self.config.service.strategy =
544 Some(apimock_routing::Strategy::FirstMatch);
545 }
546 other => {
547 return Err(ApplyError::InvalidPayload {
548 reason: format!("unknown strategy: {}", other),
549 });
550 }
551 }
552 }
553 }
554
555 let id = self
557 .ids
558 .id_for(NodeAddress::Root)
559 .expect("root id seeded at load");
560 Ok(vec![id])
561 }
562}