1pub 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_bool,
35 value_as_integer, value_as_string, value_as_string_list,
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)
91 }
92
93 EditCommand::AddHeaderCondition { rule_id, condition } => {
95 let ids = self.cmd_add_header_condition(rule_id, condition)?;
96 (ids, true)
97 }
98 EditCommand::UpdateHeaderCondition { id, condition } => {
99 let ids = self.cmd_update_header_condition(id, condition)?;
100 (ids, true)
101 }
102 EditCommand::RemoveHeaderCondition { id } => {
103 let ids = self.cmd_remove_header_condition(id)?;
104 (ids, true)
105 }
106 EditCommand::AddBodyCondition { rule_id, condition } => {
107 let ids = self.cmd_add_body_condition(rule_id, condition)?;
108 (ids, true)
109 }
110 EditCommand::UpdateBodyCondition { id, condition } => {
111 let ids = self.cmd_update_body_condition(id, condition)?;
112 (ids, true)
113 }
114 EditCommand::RemoveBodyCondition { id } => {
115 let ids = self.cmd_remove_body_condition(id)?;
116 (ids, true)
117 }
118 EditCommand::UpdateRuleSetStrategy { id, strategy } => {
119 let ids = self.cmd_update_rule_set_strategy(id, strategy)?;
120 (ids, true) }
122 };
123
124 let diagnostics = self.collect_diagnostics();
129
130 Ok(ApplyResult {
131 changed_nodes,
132 diagnostics,
133 requires_reload,
134 })
135 }
136
137 fn cmd_add_rule_set(&mut self, path: String) -> Result<Vec<NodeId>, ApplyError> {
140 let relative_dir = self.config_relative_dir().map_err(internal_path_err)?;
143 let joined = Path::new(&relative_dir).join(&path);
144 let path_str = joined.to_str().ok_or_else(|| ApplyError::InvalidPayload {
145 reason: format!(
146 "path contains non-UTF-8 bytes: {}",
147 joined.to_string_lossy()
148 ),
149 })?;
150
151 let next_idx = self.config.service.rule_sets.len();
152 let new_rule_set = RuleSet::new(path_str, relative_dir.as_str(), next_idx)
153 .map_err(|e| ApplyError::InvalidPayload {
154 reason: format!("failed to load rule set `{}`: {}", path, e),
155 })?;
156
157 let file_paths = self
160 .config
161 .service
162 .rule_sets_file_paths
163 .get_or_insert_with(Vec::new);
164 file_paths.push(path.clone());
165
166 let new_len = self.config.service.rule_sets.len() + 1;
167 self.config.service.rule_sets.push(new_rule_set);
168
169 let rs_addr = NodeAddress::RuleSet {
171 rule_set: next_idx,
172 };
173 let rs_id = self.ids.insert(rs_addr);
174 let mut changed = vec![rs_id];
175 let new_rs = &self.config.service.rule_sets[next_idx];
176 for rule_idx in 0..new_rs.rules.len() {
177 let r_id = self.ids.insert(NodeAddress::Rule {
178 rule_set: next_idx,
179 rule: rule_idx,
180 });
181 let resp_id = self.ids.insert(NodeAddress::Respond {
182 rule_set: next_idx,
183 rule: rule_idx,
184 });
185 changed.push(r_id);
186 changed.push(resp_id);
187 }
188 debug_assert_eq!(new_len, self.config.service.rule_sets.len());
191
192 Ok(changed)
193 }
194
195 fn cmd_remove_rule_set(&mut self, id: NodeId) -> Result<Vec<NodeId>, ApplyError> {
196 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
197 let NodeAddress::RuleSet { rule_set: idx } = addr else {
198 return Err(ApplyError::WrongNodeKind {
199 id,
200 reason: "expected a rule set id".to_owned(),
201 });
202 };
203
204 let len = self.config.service.rule_sets.len();
205 if idx >= len {
206 return Err(ApplyError::InvalidPayload {
207 reason: format!("rule set index {} out of range (len={})", idx, len),
208 });
209 }
210
211 let mut changed: Vec<NodeId> = Vec::new();
214 changed.push(id);
216 if let Some(removed_rs) = self.config.service.rule_sets.get(idx) {
217 for rule_idx in 0..removed_rs.rules.len() {
218 if let Some(r_id) = self.ids.id_for(NodeAddress::Rule {
219 rule_set: idx,
220 rule: rule_idx,
221 }) {
222 changed.push(r_id);
223 }
224 if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
225 rule_set: idx,
226 rule: rule_idx,
227 }) {
228 changed.push(resp_id);
229 }
230 }
231 }
232
233 self.config.service.rule_sets.remove(idx);
235 if let Some(paths) = self.config.service.rule_sets_file_paths.as_mut() {
236 if idx < paths.len() {
237 paths.remove(idx);
238 }
239 }
240
241 self.shift_rule_sets_down(idx);
246
247 for shifted_idx in idx..self.config.service.rule_sets.len() {
251 if let Some(shifted_id) = self
252 .ids
253 .id_for(NodeAddress::RuleSet {
254 rule_set: shifted_idx,
255 })
256 {
257 if !changed.contains(&shifted_id) {
258 changed.push(shifted_id);
259 }
260 }
261 }
262
263 Ok(changed)
264 }
265
266 fn cmd_add_rule(
267 &mut self,
268 parent: NodeId,
269 rule_payload: crate::view::RulePayload,
270 ) -> Result<Vec<NodeId>, ApplyError> {
271 let addr = self
272 .ids
273 .lookup(parent)
274 .ok_or(ApplyError::UnknownNode { id: parent })?;
275 let NodeAddress::RuleSet { rule_set: rs_idx } = addr else {
276 return Err(ApplyError::WrongNodeKind {
277 id: parent,
278 reason: "expected a rule set id (parent for AddRule must be a rule set)".to_owned(),
279 });
280 };
281
282 let rule_set = self
283 .config
284 .service
285 .rule_sets
286 .get_mut(rs_idx)
287 .ok_or_else(|| ApplyError::InvalidPayload {
288 reason: format!("rule set index {} out of range", rs_idx),
289 })?;
290
291 let new_rule = build_rule_from_payload(rule_payload, rule_set, rs_idx, None)?;
292 let new_rule_idx = rule_set.rules.len();
293 rule_set.rules.push(new_rule);
294
295 let r_id = self.ids.insert(NodeAddress::Rule {
296 rule_set: rs_idx,
297 rule: new_rule_idx,
298 });
299 let resp_id = self.ids.insert(NodeAddress::Respond {
300 rule_set: rs_idx,
301 rule: new_rule_idx,
302 });
303 Ok(vec![parent, r_id, resp_id])
304 }
305
306 fn cmd_update_rule(
307 &mut self,
308 id: NodeId,
309 rule_payload: crate::view::RulePayload,
310 ) -> Result<Vec<NodeId>, ApplyError> {
311 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
312 let NodeAddress::Rule {
313 rule_set: rs_idx,
314 rule: rule_idx,
315 } = addr
316 else {
317 return Err(ApplyError::WrongNodeKind {
318 id,
319 reason: "expected a rule id".to_owned(),
320 });
321 };
322
323 let rule_set = self
324 .config
325 .service
326 .rule_sets
327 .get_mut(rs_idx)
328 .ok_or_else(|| ApplyError::InvalidPayload {
329 reason: format!("rule set index {} out of range", rs_idx),
330 })?;
331
332 let existing = rule_set.rules.get(rule_idx).cloned();
337 let new_rule = build_rule_from_payload(
338 rule_payload,
339 rule_set,
340 rs_idx,
341 existing.as_ref(),
342 )?;
343 *rule_set
344 .rules
345 .get_mut(rule_idx)
346 .ok_or_else(|| ApplyError::InvalidPayload {
347 reason: format!("rule index {} out of range", rule_idx),
348 })? = new_rule;
349
350 let resp_id = self
351 .ids
352 .id_for(NodeAddress::Respond {
353 rule_set: rs_idx,
354 rule: rule_idx,
355 })
356 .unwrap_or_else(NodeId::new);
357 Ok(vec![id, resp_id])
358 }
359
360 fn cmd_delete_rule(&mut self, id: NodeId) -> Result<Vec<NodeId>, ApplyError> {
361 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
362 let NodeAddress::Rule {
363 rule_set: rs_idx,
364 rule: rule_idx,
365 } = addr
366 else {
367 return Err(ApplyError::WrongNodeKind {
368 id,
369 reason: "expected a rule id".to_owned(),
370 });
371 };
372
373 let rule_set = self
374 .config
375 .service
376 .rule_sets
377 .get_mut(rs_idx)
378 .ok_or_else(|| ApplyError::InvalidPayload {
379 reason: format!("rule set index {} out of range", rs_idx),
380 })?;
381
382 if rule_idx >= rule_set.rules.len() {
383 return Err(ApplyError::InvalidPayload {
384 reason: format!("rule index {} out of range", rule_idx),
385 });
386 }
387
388 let mut changed: Vec<NodeId> = Vec::new();
390 changed.push(id);
391 if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
392 rule_set: rs_idx,
393 rule: rule_idx,
394 }) {
395 changed.push(resp_id);
396 }
397
398 rule_set.rules.remove(rule_idx);
399 self.shift_rules_down(rs_idx, rule_idx);
400
401 let new_rule_count = self.config.service.rule_sets[rs_idx].rules.len();
403 for shifted_idx in rule_idx..new_rule_count {
404 if let Some(r_id) = self.ids.id_for(NodeAddress::Rule {
405 rule_set: rs_idx,
406 rule: shifted_idx,
407 }) {
408 if !changed.contains(&r_id) {
409 changed.push(r_id);
410 }
411 }
412 if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
413 rule_set: rs_idx,
414 rule: shifted_idx,
415 }) {
416 if !changed.contains(&resp_id) {
417 changed.push(resp_id);
418 }
419 }
420 }
421
422 Ok(changed)
423 }
424
425 fn cmd_move_rule(&mut self, id: NodeId, new_index: usize) -> Result<Vec<NodeId>, ApplyError> {
426 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
427 let NodeAddress::Rule {
428 rule_set: rs_idx,
429 rule: old_idx,
430 } = addr
431 else {
432 return Err(ApplyError::WrongNodeKind {
433 id,
434 reason: "expected a rule id".to_owned(),
435 });
436 };
437
438 let rule_set = self
439 .config
440 .service
441 .rule_sets
442 .get_mut(rs_idx)
443 .ok_or_else(|| ApplyError::InvalidPayload {
444 reason: format!("rule set index {} out of range", rs_idx),
445 })?;
446
447 if old_idx >= rule_set.rules.len() || new_index >= rule_set.rules.len() {
448 return Err(ApplyError::InvalidPayload {
449 reason: format!(
450 "move out of bounds: old_idx={}, new_index={}, len={}",
451 old_idx,
452 new_index,
453 rule_set.rules.len()
454 ),
455 });
456 }
457 if old_idx == new_index {
458 return Ok(vec![id]);
459 }
460
461 let rule = rule_set.rules.remove(old_idx);
463 rule_set.rules.insert(new_index, rule);
464
465 self.reorder_rule_ids(rs_idx, old_idx, new_index);
470
471 let lo = old_idx.min(new_index);
474 let hi = old_idx.max(new_index);
475 let mut changed: Vec<NodeId> = Vec::new();
476 for idx in lo..=hi {
477 if let Some(r_id) = self.ids.id_for(NodeAddress::Rule {
478 rule_set: rs_idx,
479 rule: idx,
480 }) {
481 changed.push(r_id);
482 }
483 if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
484 rule_set: rs_idx,
485 rule: idx,
486 }) {
487 changed.push(resp_id);
488 }
489 }
490 Ok(changed)
491 }
492
493 fn cmd_update_respond(
494 &mut self,
495 id: NodeId,
496 respond: crate::view::RespondPayload,
497 ) -> Result<Vec<NodeId>, ApplyError> {
498 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
499 let NodeAddress::Respond {
500 rule_set: rs_idx,
501 rule: rule_idx,
502 } = addr
503 else {
504 return Err(ApplyError::WrongNodeKind {
505 id,
506 reason: "expected a respond id".to_owned(),
507 });
508 };
509
510 let rule = self
511 .config
512 .service
513 .rule_sets
514 .get_mut(rs_idx)
515 .and_then(|rs| rs.rules.get_mut(rule_idx))
516 .ok_or_else(|| ApplyError::InvalidPayload {
517 reason: format!(
518 "rule at rule_set={}, rule={} not found",
519 rs_idx, rule_idx
520 ),
521 })?;
522
523 rule.respond = build_respond_from_payload(respond);
524
525 let rule_set = &self.config.service.rule_sets[rs_idx];
528 let derived = rule_set.rules[rule_idx].compute_derived_fields(rule_set, rule_idx, rs_idx);
529 self.config.service.rule_sets[rs_idx].rules[rule_idx] = derived;
530
531 Ok(vec![id])
532 }
533
534 fn cmd_update_root_setting(
535 &mut self,
536 key: crate::view::RootSettingKey,
537 value: EditValue,
538 ) -> Result<Vec<NodeId>, ApplyError> {
539 use crate::view::RootSettingKey::*;
540
541 match key {
542 ListenerIpAddress => {
543 let s = value_as_string(&value)?;
544 let listener = self.config.listener.get_or_insert_with(Default::default);
545 listener.ip_address = s;
546 }
547 ListenerPort => {
548 let n = value_as_integer(&value)?;
549 if !(0..=u16::MAX as i64).contains(&n) {
550 return Err(ApplyError::InvalidPayload {
551 reason: format!("port {} not in 0..=65535", n),
552 });
553 }
554 let listener = self.config.listener.get_or_insert_with(Default::default);
555 listener.port = n as u16;
556 }
557 ServiceFallbackRespondDir => {
558 let s = value_as_string(&value)?;
559 self.config.service.fallback_respond_dir = s;
560 }
561 ServiceStrategy => {
562 let s = value_as_string(&value)?;
563 use apimock_routing::Strategy;
564 let strategy = match s.as_str() {
565 "first_match" => Strategy::FirstMatch,
566 "uniform_random" => Strategy::UniformRandom { seed: None },
567 "weighted_random" => Strategy::WeightedRandom { seed: None },
568 "priority" => Strategy::Priority {
569 tiebreaker: apimock_routing::strategy::PriorityTiebreaker::FirstMatch,
570 },
571 "round_robin" => Strategy::RoundRobin,
572 other => {
573 return Err(ApplyError::InvalidPayload {
574 reason: format!("unknown strategy: `{}`", other),
575 });
576 }
577 };
578 self.config.service.strategy = Some(strategy);
579 }
580
581 TlsEnabled => {
583 let enabled = value_as_bool(&value)?;
584 if !enabled {
585 if let Some(listener) = self.config.listener.as_mut() {
587 listener.tls = None;
588 }
589 }
590 }
595 TlsCertFile => {
596 let s = value_as_string(&value)?;
597 let listener = self.config.listener.get_or_insert_with(Default::default);
598 let tls = listener.tls.get_or_insert_with(|| {
599 crate::config::listener_config::tls_config::TlsConfig {
600 cert: String::new(),
601 key: String::new(),
602 port: None,
603 }
604 });
605 tls.cert = s;
606 }
607 TlsKeyFile => {
608 let s = value_as_string(&value)?;
609 let listener = self.config.listener.get_or_insert_with(Default::default);
610 let tls = listener.tls.get_or_insert_with(|| {
611 crate::config::listener_config::tls_config::TlsConfig {
612 cert: String::new(),
613 key: String::new(),
614 port: None,
615 }
616 });
617 tls.key = s;
618 }
619
620 LogLevel => {
622 let s = value_as_string(&value)?;
623 let valid_levels = ["trace", "debug", "info", "warn", "error"];
624 if !valid_levels.contains(&s.as_str()) {
625 return Err(ApplyError::InvalidPayload {
626 reason: format!(
627 "invalid log level `{}` — valid: trace, debug, info, warn, error",
628 s
629 ),
630 });
631 }
632 let _ = s; }
638 LogFile => {
639 let s = value_as_string(&value)?;
640 let _ = s; }
642 LogFormat => {
643 let s = value_as_string(&value)?;
644 let valid_formats = ["text", "json"];
645 if !valid_formats.contains(&s.as_str()) {
646 return Err(ApplyError::InvalidPayload {
647 reason: format!(
648 "invalid log format `{}` — valid: text, json",
649 s
650 ),
651 });
652 }
653 let _ = s; }
655
656 FileTreeShowHidden => {
658 let b = value_as_bool(&value)?;
659 self.config
660 .file_tree_view
661 .get_or_insert_with(Default::default)
662 .show_hidden = b;
663 }
664 FileTreeBuiltinExcludes => {
665 let b = value_as_bool(&value)?;
666 self.config
667 .file_tree_view
668 .get_or_insert_with(Default::default)
669 .builtin_excludes = b;
670 }
671 FileTreeExtraExcludes => {
672 let list = value_as_string_list(&value)?;
673 self.config
674 .file_tree_view
675 .get_or_insert_with(Default::default)
676 .extra_excludes = list;
677 }
678 FileTreeInclude => {
679 let list = value_as_string_list(&value)?;
680 self.config
681 .file_tree_view
682 .get_or_insert_with(Default::default)
683 .include = list;
684 }
685 FileTreeRespectGitignore => {
686 let b = value_as_bool(&value)?;
687 self.config
688 .file_tree_view
689 .get_or_insert_with(Default::default)
690 .respect_gitignore = b;
691 }
692 TraceCaptureBody => {
693 log::info!("trace.capture_body updated (effective on next server start)");
696 }
697 TraceMaxBodyBytes => {
698 log::info!("trace.max_body_bytes updated (effective on next server start)");
699 }
700 }
701
702 let id = self
703 .ids
704 .id_for(NodeAddress::Root)
705 .expect("root id seeded at load");
706 Ok(vec![id])
707 }
708
709 fn cmd_add_header_condition(
712 &mut self,
713 rule_id: crate::view::NodeId,
714 payload: crate::view::HeaderConditionPayload,
715 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
716 use apimock_routing::rule_set::rule::when::request::headers::HeaderConditionStatement;
717
718 let (rs_idx, rule_idx) = self.find_rule_indices(rule_id)?;
719 let op = payload::header_op_to_routing_pub(payload.op);
720 let value = payload.value.unwrap_or_default();
721 let stmt = HeaderConditionStatement { op: Some(op), value };
722 let name = payload.name.to_lowercase();
723
724 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
726 let headers = rule.when.request.headers.get_or_insert_with(|| {
727 apimock_routing::rule_set::rule::when::request::headers::Headers(
728 indexmap::IndexMap::new(),
729 )
730 });
731 headers.0.insert(name.clone(), stmt);
732
733 let cond_id = self.ids.insert(NodeAddress::HeaderCondition {
734 rule_set: rs_idx, rule: rule_idx, header_name: name,
735 });
736 let rule_id_out = self
737 .ids
738 .id_for(NodeAddress::Rule { rule_set: rs_idx, rule: rule_idx })
739 .unwrap_or(rule_id);
740 Ok(vec![rule_id_out, cond_id])
741 }
742
743 fn cmd_update_header_condition(
744 &mut self,
745 id: crate::view::NodeId,
746 payload: crate::view::HeaderConditionPayload,
747 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
748 use apimock_routing::rule_set::rule::when::request::headers::HeaderConditionStatement;
749
750 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
751 let (rs_idx, rule_idx, old_name) = match addr {
752 NodeAddress::HeaderCondition { rule_set, rule, header_name } => {
753 (rule_set, rule, header_name)
754 }
755 _ => return Err(ApplyError::InvalidPayload {
756 reason: "id does not refer to a header condition".to_owned(),
757 }),
758 };
759
760 let op = payload::header_op_to_routing_pub(payload.op);
761 let value = payload.value.unwrap_or_default();
762 let new_name = payload.name.to_lowercase();
763 let stmt = HeaderConditionStatement { op: Some(op), value };
764
765 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
766 let headers = rule.when.request.headers.get_or_insert_with(|| {
767 apimock_routing::rule_set::rule::when::request::headers::Headers(
768 indexmap::IndexMap::new(),
769 )
770 });
771
772 headers.0.shift_remove(&old_name);
774 headers.0.insert(new_name.clone(), stmt);
775
776 let new_id = self.ids.insert(NodeAddress::HeaderCondition {
778 rule_set: rs_idx, rule: rule_idx, header_name: new_name,
779 });
780 Ok(vec![new_id])
781 }
782
783 fn cmd_remove_header_condition(
784 &mut self,
785 id: crate::view::NodeId,
786 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
787 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
788 let (rs_idx, rule_idx, name) = match addr {
789 NodeAddress::HeaderCondition { rule_set, rule, header_name } => {
790 (rule_set, rule, header_name)
791 }
792 _ => return Err(ApplyError::InvalidPayload {
793 reason: "id does not refer to a header condition".to_owned(),
794 }),
795 };
796
797 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
798 if let Some(headers) = rule.when.request.headers.as_mut() {
799 headers.0.shift_remove(&name);
800 if headers.0.is_empty() {
801 rule.when.request.headers = None;
802 }
803 }
804
805 let rule_id = self
806 .ids
807 .id_for(NodeAddress::Rule { rule_set: rs_idx, rule: rule_idx })
808 .unwrap_or(id);
809 Ok(vec![rule_id])
810 }
811
812 fn cmd_add_body_condition(
813 &mut self,
814 rule_id: crate::view::NodeId,
815 payload: crate::view::BodyConditionPayload,
816 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
817 use apimock_routing::rule_set::rule::when::request::body::{
818 Body, BodyConditionStatement, body_kind::BodyKind,
819 };
820
821 let (rs_idx, rule_idx) = self.find_rule_indices(rule_id)?;
822 let op = payload::body_op_to_routing_pub(payload.op);
823 let value = payload::json_value_to_string_pub(&payload.value);
824 let stmt = BodyConditionStatement { op: Some(op), value };
825 let path = payload.path.clone();
826
827 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
828 if rule.when.request.body.is_none() {
829 rule.when.request.body = Some(Body(std::collections::HashMap::new()));
830 }
831 let body_map = rule.when.request.body.as_mut().unwrap();
832 body_map
833 .0
834 .entry(BodyKind::Json)
835 .or_insert_with(indexmap::IndexMap::new)
836 .insert(path.clone(), stmt);
837
838 let cond_id = self.ids.insert(NodeAddress::BodyCondition {
839 rule_set: rs_idx, rule: rule_idx, path,
840 });
841 let rule_id_out = self
842 .ids
843 .id_for(NodeAddress::Rule { rule_set: rs_idx, rule: rule_idx })
844 .unwrap_or(rule_id);
845 Ok(vec![rule_id_out, cond_id])
846 }
847
848 fn cmd_update_body_condition(
849 &mut self,
850 id: crate::view::NodeId,
851 payload: crate::view::BodyConditionPayload,
852 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
853 use apimock_routing::rule_set::rule::when::request::body::BodyConditionStatement;
854
855 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
856 let (rs_idx, rule_idx, old_path) = match addr {
857 NodeAddress::BodyCondition { rule_set, rule, path } => (rule_set, rule, path),
858 _ => return Err(ApplyError::InvalidPayload {
859 reason: "id does not refer to a body condition".to_owned(),
860 }),
861 };
862
863 let op = payload::body_op_to_routing_pub(payload.op);
864 let value = payload::json_value_to_string_pub(&payload.value);
865 let new_path = payload.path.clone();
866 let stmt = BodyConditionStatement { op: Some(op), value };
867
868 use apimock_routing::rule_set::rule::when::request::body::body_kind::BodyKind;
869 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
870 if let Some(body) = rule.when.request.body.as_mut() {
871 if let Some(json_map) = body.0.get_mut(&BodyKind::Json) {
872 json_map.shift_remove(&old_path);
873 json_map.insert(new_path.clone(), stmt);
874 }
875 }
876
877 let new_id = self.ids.insert(NodeAddress::BodyCondition {
878 rule_set: rs_idx, rule: rule_idx, path: new_path,
879 });
880 Ok(vec![new_id])
881 }
882
883 fn cmd_remove_body_condition(
884 &mut self,
885 id: crate::view::NodeId,
886 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
887 use apimock_routing::rule_set::rule::when::request::body::body_kind::BodyKind;
888
889 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
890 let (rs_idx, rule_idx, path) = match addr {
891 NodeAddress::BodyCondition { rule_set, rule, path } => (rule_set, rule, path),
892 _ => return Err(ApplyError::InvalidPayload {
893 reason: "id does not refer to a body condition".to_owned(),
894 }),
895 };
896
897 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
898 if let Some(body) = rule.when.request.body.as_mut() {
899 if let Some(json_map) = body.0.get_mut(&BodyKind::Json) {
900 json_map.shift_remove(&path);
901 }
902 }
903
904 let rule_id = self
905 .ids
906 .id_for(NodeAddress::Rule { rule_set: rs_idx, rule: rule_idx })
907 .unwrap_or(id);
908 Ok(vec![rule_id])
909 }
910
911 fn find_rule_indices(
913 &self,
914 rule_id: crate::view::NodeId,
915 ) -> Result<(usize, usize), ApplyError> {
916 match self.ids.lookup(rule_id) {
917 Some(NodeAddress::Rule { rule_set, rule }) => Ok((rule_set, rule)),
918 _ => Err(ApplyError::UnknownNode { id: rule_id }),
919 }
920 }
921
922 fn cmd_update_rule_set_strategy(
925 &mut self,
926 id: crate::view::NodeId,
927 strategy_name: Option<String>,
928 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
929 use apimock_routing::strategy::{PriorityTiebreaker, Strategy};
930
931 let rs_idx = match self.ids.lookup(id) {
932 Some(NodeAddress::RuleSet { rule_set }) => rule_set,
933 _ => return Err(ApplyError::UnknownNode { id }),
934 };
935
936 let strategy = match strategy_name.as_deref() {
937 None | Some("") => None,
938 Some(s) => {
939 let parsed = match s {
940 "first_match" => Strategy::FirstMatch,
941 "uniform_random" => Strategy::UniformRandom { seed: None },
942 "weighted_random" => Strategy::WeightedRandom { seed: None },
943 "priority" => Strategy::Priority { tiebreaker: PriorityTiebreaker::FirstMatch },
944 "round_robin" => Strategy::RoundRobin,
945 _ => return Err(ApplyError::InvalidPayload {
946 reason: format!(
947 "unknown strategy {:?}; expected one of: \
948 first_match, uniform_random, weighted_random, \
949 priority, round_robin",
950 s
951 ),
952 }),
953 };
954 Some(parsed)
955 }
956 };
957
958 self.config.service.rule_sets[rs_idx].strategy = strategy;
959
960 let rs_id = self
961 .ids
962 .id_for(NodeAddress::RuleSet { rule_set: rs_idx })
963 .unwrap_or(id);
964 Ok(vec![rs_id])
965 }
966}