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_respond_from_payload, build_rule_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 =
153 RuleSet::new(path_str, relative_dir.as_str(), next_idx).map_err(|e| {
154 ApplyError::InvalidPayload {
155 reason: format!("failed to load rule set `{}`: {}", path, e),
156 }
157 })?;
158
159 let file_paths = self
162 .config
163 .service
164 .rule_sets_file_paths
165 .get_or_insert_with(Vec::new);
166 file_paths.push(path.clone());
167
168 let new_len = self.config.service.rule_sets.len() + 1;
169 self.config.service.rule_sets.push(new_rule_set);
170
171 let rs_addr = NodeAddress::RuleSet { rule_set: next_idx };
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 && idx < paths.len()
237 {
238 paths.remove(idx);
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.ids.id_for(NodeAddress::RuleSet {
252 rule_set: shifted_idx,
253 }) && !changed.contains(&shifted_id)
254 {
255 changed.push(shifted_id);
256 }
257 }
258
259 Ok(changed)
260 }
261
262 fn cmd_add_rule(
263 &mut self,
264 parent: NodeId,
265 rule_payload: crate::view::RulePayload,
266 ) -> Result<Vec<NodeId>, ApplyError> {
267 let addr = self
268 .ids
269 .lookup(parent)
270 .ok_or(ApplyError::UnknownNode { id: parent })?;
271 let NodeAddress::RuleSet { rule_set: rs_idx } = addr else {
272 return Err(ApplyError::WrongNodeKind {
273 id: parent,
274 reason: "expected a rule set id (parent for AddRule must be a rule set)".to_owned(),
275 });
276 };
277
278 let rule_set = self
279 .config
280 .service
281 .rule_sets
282 .get_mut(rs_idx)
283 .ok_or_else(|| ApplyError::InvalidPayload {
284 reason: format!("rule set index {} out of range", rs_idx),
285 })?;
286
287 let new_rule = build_rule_from_payload(rule_payload, rule_set, rs_idx, None)?;
288 let new_rule_idx = rule_set.rules.len();
289 rule_set.rules.push(new_rule);
290
291 let r_id = self.ids.insert(NodeAddress::Rule {
292 rule_set: rs_idx,
293 rule: new_rule_idx,
294 });
295 let resp_id = self.ids.insert(NodeAddress::Respond {
296 rule_set: rs_idx,
297 rule: new_rule_idx,
298 });
299 Ok(vec![parent, r_id, resp_id])
300 }
301
302 fn cmd_update_rule(
303 &mut self,
304 id: NodeId,
305 rule_payload: crate::view::RulePayload,
306 ) -> Result<Vec<NodeId>, ApplyError> {
307 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
308 let NodeAddress::Rule {
309 rule_set: rs_idx,
310 rule: rule_idx,
311 } = addr
312 else {
313 return Err(ApplyError::WrongNodeKind {
314 id,
315 reason: "expected a rule id".to_owned(),
316 });
317 };
318
319 let rule_set = self
320 .config
321 .service
322 .rule_sets
323 .get_mut(rs_idx)
324 .ok_or_else(|| ApplyError::InvalidPayload {
325 reason: format!("rule set index {} out of range", rs_idx),
326 })?;
327
328 let existing = rule_set.rules.get(rule_idx).cloned();
333 let new_rule = build_rule_from_payload(rule_payload, rule_set, rs_idx, existing.as_ref())?;
334 *rule_set
335 .rules
336 .get_mut(rule_idx)
337 .ok_or_else(|| ApplyError::InvalidPayload {
338 reason: format!("rule index {} out of range", rule_idx),
339 })? = new_rule;
340
341 let resp_id = self
342 .ids
343 .id_for(NodeAddress::Respond {
344 rule_set: rs_idx,
345 rule: rule_idx,
346 })
347 .unwrap_or_default();
348 Ok(vec![id, resp_id])
349 }
350
351 fn cmd_delete_rule(&mut self, id: NodeId) -> Result<Vec<NodeId>, ApplyError> {
352 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
353 let NodeAddress::Rule {
354 rule_set: rs_idx,
355 rule: rule_idx,
356 } = addr
357 else {
358 return Err(ApplyError::WrongNodeKind {
359 id,
360 reason: "expected a rule id".to_owned(),
361 });
362 };
363
364 let rule_set = self
365 .config
366 .service
367 .rule_sets
368 .get_mut(rs_idx)
369 .ok_or_else(|| ApplyError::InvalidPayload {
370 reason: format!("rule set index {} out of range", rs_idx),
371 })?;
372
373 if rule_idx >= rule_set.rules.len() {
374 return Err(ApplyError::InvalidPayload {
375 reason: format!("rule index {} out of range", rule_idx),
376 });
377 }
378
379 let mut changed: Vec<NodeId> = Vec::new();
381 changed.push(id);
382 if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
383 rule_set: rs_idx,
384 rule: rule_idx,
385 }) {
386 changed.push(resp_id);
387 }
388
389 rule_set.rules.remove(rule_idx);
390 self.shift_rules_down(rs_idx, rule_idx);
391
392 let new_rule_count = self.config.service.rule_sets[rs_idx].rules.len();
394 for shifted_idx in rule_idx..new_rule_count {
395 if let Some(r_id) = self.ids.id_for(NodeAddress::Rule {
396 rule_set: rs_idx,
397 rule: shifted_idx,
398 }) && !changed.contains(&r_id)
399 {
400 changed.push(r_id);
401 }
402 if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
403 rule_set: rs_idx,
404 rule: shifted_idx,
405 }) && !changed.contains(&resp_id)
406 {
407 changed.push(resp_id);
408 }
409 }
410
411 Ok(changed)
412 }
413
414 fn cmd_move_rule(&mut self, id: NodeId, new_index: usize) -> Result<Vec<NodeId>, ApplyError> {
415 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
416 let NodeAddress::Rule {
417 rule_set: rs_idx,
418 rule: old_idx,
419 } = addr
420 else {
421 return Err(ApplyError::WrongNodeKind {
422 id,
423 reason: "expected a rule id".to_owned(),
424 });
425 };
426
427 let rule_set = self
428 .config
429 .service
430 .rule_sets
431 .get_mut(rs_idx)
432 .ok_or_else(|| ApplyError::InvalidPayload {
433 reason: format!("rule set index {} out of range", rs_idx),
434 })?;
435
436 if old_idx >= rule_set.rules.len() || new_index >= rule_set.rules.len() {
437 return Err(ApplyError::InvalidPayload {
438 reason: format!(
439 "move out of bounds: old_idx={}, new_index={}, len={}",
440 old_idx,
441 new_index,
442 rule_set.rules.len()
443 ),
444 });
445 }
446 if old_idx == new_index {
447 return Ok(vec![id]);
448 }
449
450 let rule = rule_set.rules.remove(old_idx);
452 rule_set.rules.insert(new_index, rule);
453
454 self.reorder_rule_ids(rs_idx, old_idx, new_index);
459
460 let lo = old_idx.min(new_index);
463 let hi = old_idx.max(new_index);
464 let mut changed: Vec<NodeId> = Vec::new();
465 for idx in lo..=hi {
466 if let Some(r_id) = self.ids.id_for(NodeAddress::Rule {
467 rule_set: rs_idx,
468 rule: idx,
469 }) {
470 changed.push(r_id);
471 }
472 if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
473 rule_set: rs_idx,
474 rule: idx,
475 }) {
476 changed.push(resp_id);
477 }
478 }
479 Ok(changed)
480 }
481
482 fn cmd_update_respond(
483 &mut self,
484 id: NodeId,
485 respond: crate::view::RespondPayload,
486 ) -> Result<Vec<NodeId>, ApplyError> {
487 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
488 let NodeAddress::Respond {
489 rule_set: rs_idx,
490 rule: rule_idx,
491 } = addr
492 else {
493 return Err(ApplyError::WrongNodeKind {
494 id,
495 reason: "expected a respond id".to_owned(),
496 });
497 };
498
499 let rule = self
500 .config
501 .service
502 .rule_sets
503 .get_mut(rs_idx)
504 .and_then(|rs| rs.rules.get_mut(rule_idx))
505 .ok_or_else(|| ApplyError::InvalidPayload {
506 reason: format!("rule at rule_set={}, rule={} not found", rs_idx, rule_idx),
507 })?;
508
509 rule.respond = build_respond_from_payload(respond);
510
511 let rule_set = &self.config.service.rule_sets[rs_idx];
514 let derived = rule_set.rules[rule_idx].compute_derived_fields(rule_set, rule_idx, rs_idx);
515 self.config.service.rule_sets[rs_idx].rules[rule_idx] = derived;
516
517 Ok(vec![id])
518 }
519
520 fn cmd_update_root_setting(
521 &mut self,
522 key: crate::view::RootSettingKey,
523 value: EditValue,
524 ) -> Result<Vec<NodeId>, ApplyError> {
525 use crate::view::RootSettingKey::*;
526
527 match key {
528 ListenerIpAddress => {
529 let s = value_as_string(&value)?;
530 let listener = self.config.listener.get_or_insert_with(Default::default);
531 listener.ip_address = s;
532 }
533 ListenerPort => {
534 let n = value_as_integer(&value)?;
535 if !(0..=u16::MAX as i64).contains(&n) {
536 return Err(ApplyError::InvalidPayload {
537 reason: format!("port {} not in 0..=65535", n),
538 });
539 }
540 let listener = self.config.listener.get_or_insert_with(Default::default);
541 listener.port = n as u16;
542 }
543 ServiceFallbackRespondDir => {
544 let s = value_as_string(&value)?;
545 self.config.service.fallback_respond_dir = s;
546 }
547 ServiceStrategy => {
548 let s = value_as_string(&value)?;
549 use apimock_routing::Strategy;
550 let strategy = match s.as_str() {
551 "first_match" => Strategy::FirstMatch,
552 "uniform_random" => Strategy::UniformRandom { seed: None },
553 "weighted_random" => Strategy::WeightedRandom { seed: None },
554 "priority" => Strategy::Priority {
555 tiebreaker: apimock_routing::strategy::PriorityTiebreaker::FirstMatch,
556 },
557 "round_robin" => Strategy::RoundRobin,
558 other => {
559 return Err(ApplyError::InvalidPayload {
560 reason: format!("unknown strategy: `{}`", other),
561 });
562 }
563 };
564 self.config.service.strategy = Some(strategy);
565 }
566
567 TlsEnabled => {
569 let enabled = value_as_bool(&value)?;
570 if !enabled {
571 if let Some(listener) = self.config.listener.as_mut() {
573 listener.tls = None;
574 }
575 }
576 }
581 TlsCertFile => {
582 let s = value_as_string(&value)?;
583 let listener = self.config.listener.get_or_insert_with(Default::default);
584 let tls = listener.tls.get_or_insert_with(|| {
585 crate::config::listener_config::tls_config::TlsConfig {
586 cert: String::new(),
587 key: String::new(),
588 port: None,
589 }
590 });
591 tls.cert = s;
592 }
593 TlsKeyFile => {
594 let s = value_as_string(&value)?;
595 let listener = self.config.listener.get_or_insert_with(Default::default);
596 let tls = listener.tls.get_or_insert_with(|| {
597 crate::config::listener_config::tls_config::TlsConfig {
598 cert: String::new(),
599 key: String::new(),
600 port: None,
601 }
602 });
603 tls.key = s;
604 }
605
606 LogLevel => {
608 let s = value_as_string(&value)?;
609 let valid_levels = ["trace", "debug", "info", "warn", "error"];
610 if !valid_levels.contains(&s.as_str()) {
611 return Err(ApplyError::InvalidPayload {
612 reason: format!(
613 "invalid log level `{}` — valid: trace, debug, info, warn, error",
614 s
615 ),
616 });
617 }
618 let _ = s; }
624 LogFile => {
625 let s = value_as_string(&value)?;
626 let _ = s; }
628 LogFormat => {
629 let s = value_as_string(&value)?;
630 let valid_formats = ["text", "json"];
631 if !valid_formats.contains(&s.as_str()) {
632 return Err(ApplyError::InvalidPayload {
633 reason: format!("invalid log format `{}` — valid: text, json", s),
634 });
635 }
636 let _ = s; }
638
639 FileTreeShowHidden => {
641 let b = value_as_bool(&value)?;
642 self.config
643 .file_tree_view
644 .get_or_insert_with(Default::default)
645 .show_hidden = b;
646 }
647 FileTreeBuiltinExcludes => {
648 let b = value_as_bool(&value)?;
649 self.config
650 .file_tree_view
651 .get_or_insert_with(Default::default)
652 .builtin_excludes = b;
653 }
654 FileTreeExtraExcludes => {
655 let list = value_as_string_list(&value)?;
656 self.config
657 .file_tree_view
658 .get_or_insert_with(Default::default)
659 .extra_excludes = list;
660 }
661 FileTreeInclude => {
662 let list = value_as_string_list(&value)?;
663 self.config
664 .file_tree_view
665 .get_or_insert_with(Default::default)
666 .include = list;
667 }
668 FileTreeRespectGitignore => {
669 let b = value_as_bool(&value)?;
670 self.config
671 .file_tree_view
672 .get_or_insert_with(Default::default)
673 .respect_gitignore = b;
674 }
675 TraceCaptureBody => {
676 log::info!("trace.capture_body updated (effective on next server start)");
679 }
680 TraceMaxBodyBytes => {
681 log::info!("trace.max_body_bytes updated (effective on next server start)");
682 }
683 }
684
685 let id = self
686 .ids
687 .id_for(NodeAddress::Root)
688 .expect("root id seeded at load");
689 Ok(vec![id])
690 }
691
692 fn cmd_add_header_condition(
695 &mut self,
696 rule_id: crate::view::NodeId,
697 payload: crate::view::HeaderConditionPayload,
698 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
699 use apimock_routing::rule_set::rule::when::request::headers::HeaderConditionStatement;
700
701 let (rs_idx, rule_idx) = self.find_rule_indices(rule_id)?;
702 let op = payload::header_op_to_routing_pub(payload.op);
703 let value = payload.value.unwrap_or_default();
704 let stmt = HeaderConditionStatement {
705 op: Some(op),
706 value,
707 };
708 let name = payload.name.to_lowercase();
709
710 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
712 let headers = rule.when.request.headers.get_or_insert_with(|| {
713 apimock_routing::rule_set::rule::when::request::headers::Headers(
714 indexmap::IndexMap::new(),
715 )
716 });
717 headers.0.insert(name.clone(), stmt);
718
719 let cond_id = self.ids.insert(NodeAddress::HeaderCondition {
720 rule_set: rs_idx,
721 rule: rule_idx,
722 header_name: name,
723 });
724 let rule_id_out = self
725 .ids
726 .id_for(NodeAddress::Rule {
727 rule_set: rs_idx,
728 rule: rule_idx,
729 })
730 .unwrap_or(rule_id);
731 Ok(vec![rule_id_out, cond_id])
732 }
733
734 fn cmd_update_header_condition(
735 &mut self,
736 id: crate::view::NodeId,
737 payload: crate::view::HeaderConditionPayload,
738 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
739 use apimock_routing::rule_set::rule::when::request::headers::HeaderConditionStatement;
740
741 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
742 let (rs_idx, rule_idx, old_name) = match addr {
743 NodeAddress::HeaderCondition {
744 rule_set,
745 rule,
746 header_name,
747 } => (rule_set, rule, header_name),
748 _ => {
749 return Err(ApplyError::InvalidPayload {
750 reason: "id does not refer to a header condition".to_owned(),
751 });
752 }
753 };
754
755 let op = payload::header_op_to_routing_pub(payload.op);
756 let value = payload.value.unwrap_or_default();
757 let new_name = payload.name.to_lowercase();
758 let stmt = HeaderConditionStatement {
759 op: Some(op),
760 value,
761 };
762
763 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
764 let headers = rule.when.request.headers.get_or_insert_with(|| {
765 apimock_routing::rule_set::rule::when::request::headers::Headers(
766 indexmap::IndexMap::new(),
767 )
768 });
769
770 headers.0.shift_remove(&old_name);
772 headers.0.insert(new_name.clone(), stmt);
773
774 let new_id = self.ids.insert(NodeAddress::HeaderCondition {
776 rule_set: rs_idx,
777 rule: rule_idx,
778 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 {
790 rule_set,
791 rule,
792 header_name,
793 } => (rule_set, rule, header_name),
794 _ => {
795 return Err(ApplyError::InvalidPayload {
796 reason: "id does not refer to a header condition".to_owned(),
797 });
798 }
799 };
800
801 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
802 if let Some(headers) = rule.when.request.headers.as_mut() {
803 headers.0.shift_remove(&name);
804 if headers.0.is_empty() {
805 rule.when.request.headers = None;
806 }
807 }
808
809 let rule_id = self
810 .ids
811 .id_for(NodeAddress::Rule {
812 rule_set: rs_idx,
813 rule: rule_idx,
814 })
815 .unwrap_or(id);
816 Ok(vec![rule_id])
817 }
818
819 fn cmd_add_body_condition(
820 &mut self,
821 rule_id: crate::view::NodeId,
822 payload: crate::view::BodyConditionPayload,
823 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
824 use apimock_routing::rule_set::rule::when::request::body::{
825 Body, BodyConditionStatement, body_kind::BodyKind,
826 };
827
828 let (rs_idx, rule_idx) = self.find_rule_indices(rule_id)?;
829 let op = payload::body_op_to_routing_pub(payload.op);
830 let value = payload::json_value_to_string_pub(&payload.value);
831 let stmt = BodyConditionStatement {
832 op: Some(op),
833 value,
834 };
835 let path = payload.path.clone();
836
837 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
838 if rule.when.request.body.is_none() {
839 rule.when.request.body = Some(Body(std::collections::HashMap::new()));
840 }
841 let body_map = rule.when.request.body.as_mut().unwrap();
842 body_map
843 .0
844 .entry(BodyKind::Json)
845 .or_default()
846 .insert(path.clone(), stmt);
847
848 let cond_id = self.ids.insert(NodeAddress::BodyCondition {
849 rule_set: rs_idx,
850 rule: rule_idx,
851 path,
852 });
853 let rule_id_out = self
854 .ids
855 .id_for(NodeAddress::Rule {
856 rule_set: rs_idx,
857 rule: rule_idx,
858 })
859 .unwrap_or(rule_id);
860 Ok(vec![rule_id_out, cond_id])
861 }
862
863 fn cmd_update_body_condition(
864 &mut self,
865 id: crate::view::NodeId,
866 payload: crate::view::BodyConditionPayload,
867 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
868 use apimock_routing::rule_set::rule::when::request::body::BodyConditionStatement;
869
870 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
871 let (rs_idx, rule_idx, old_path) = match addr {
872 NodeAddress::BodyCondition {
873 rule_set,
874 rule,
875 path,
876 } => (rule_set, rule, path),
877 _ => {
878 return Err(ApplyError::InvalidPayload {
879 reason: "id does not refer to a body condition".to_owned(),
880 });
881 }
882 };
883
884 let op = payload::body_op_to_routing_pub(payload.op);
885 let value = payload::json_value_to_string_pub(&payload.value);
886 let new_path = payload.path.clone();
887 let stmt = BodyConditionStatement {
888 op: Some(op),
889 value,
890 };
891
892 use apimock_routing::rule_set::rule::when::request::body::body_kind::BodyKind;
893 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
894 if let Some(body) = rule.when.request.body.as_mut()
895 && let Some(json_map) = body.0.get_mut(&BodyKind::Json)
896 {
897 json_map.shift_remove(&old_path);
898 json_map.insert(new_path.clone(), stmt);
899 }
900
901 let new_id = self.ids.insert(NodeAddress::BodyCondition {
902 rule_set: rs_idx,
903 rule: rule_idx,
904 path: new_path,
905 });
906 Ok(vec![new_id])
907 }
908
909 fn cmd_remove_body_condition(
910 &mut self,
911 id: crate::view::NodeId,
912 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
913 use apimock_routing::rule_set::rule::when::request::body::body_kind::BodyKind;
914
915 let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
916 let (rs_idx, rule_idx, path) = match addr {
917 NodeAddress::BodyCondition {
918 rule_set,
919 rule,
920 path,
921 } => (rule_set, rule, path),
922 _ => {
923 return Err(ApplyError::InvalidPayload {
924 reason: "id does not refer to a body condition".to_owned(),
925 });
926 }
927 };
928
929 let rule = &mut self.config.service.rule_sets[rs_idx].rules[rule_idx];
930 if let Some(body) = rule.when.request.body.as_mut()
931 && let Some(json_map) = body.0.get_mut(&BodyKind::Json)
932 {
933 json_map.shift_remove(&path);
934 }
935
936 let rule_id = self
937 .ids
938 .id_for(NodeAddress::Rule {
939 rule_set: rs_idx,
940 rule: rule_idx,
941 })
942 .unwrap_or(id);
943 Ok(vec![rule_id])
944 }
945
946 fn find_rule_indices(
948 &self,
949 rule_id: crate::view::NodeId,
950 ) -> Result<(usize, usize), ApplyError> {
951 match self.ids.lookup(rule_id) {
952 Some(NodeAddress::Rule { rule_set, rule }) => Ok((rule_set, rule)),
953 _ => Err(ApplyError::UnknownNode { id: rule_id }),
954 }
955 }
956
957 fn cmd_update_rule_set_strategy(
960 &mut self,
961 id: crate::view::NodeId,
962 strategy_name: Option<String>,
963 ) -> Result<Vec<crate::view::NodeId>, ApplyError> {
964 use apimock_routing::strategy::{PriorityTiebreaker, Strategy};
965
966 let rs_idx = match self.ids.lookup(id) {
967 Some(NodeAddress::RuleSet { rule_set }) => rule_set,
968 _ => return Err(ApplyError::UnknownNode { id }),
969 };
970
971 let strategy = match strategy_name.as_deref() {
972 None | Some("") => None,
973 Some(s) => {
974 let parsed = match s {
975 "first_match" => Strategy::FirstMatch,
976 "uniform_random" => Strategy::UniformRandom { seed: None },
977 "weighted_random" => Strategy::WeightedRandom { seed: None },
978 "priority" => Strategy::Priority {
979 tiebreaker: PriorityTiebreaker::FirstMatch,
980 },
981 "round_robin" => Strategy::RoundRobin,
982 _ => {
983 return Err(ApplyError::InvalidPayload {
984 reason: format!(
985 "unknown strategy {:?}; expected one of: \
986 first_match, uniform_random, weighted_random, \
987 priority, round_robin",
988 s
989 ),
990 });
991 }
992 };
993 Some(parsed)
994 }
995 };
996
997 self.config.service.rule_sets[rs_idx].strategy = strategy;
998
999 let rs_id = self
1000 .ids
1001 .id_for(NodeAddress::RuleSet { rule_set: rs_idx })
1002 .unwrap_or(id);
1003 Ok(vec![rs_id])
1004 }
1005}