pub mod id_shift;
pub mod payload;
use std::path::Path;
use apimock_routing::RuleSet;
use crate::error::ApplyError;
use crate::view::{ApplyResult, EditCommand, EditValue, NodeId};
use super::Workspace;
use super::id_index::NodeAddress;
use payload::{
build_rule_from_payload, build_respond_from_payload, internal_path_err, value_as_integer,
value_as_string,
};
impl Workspace {
pub fn apply(&mut self, cmd: EditCommand) -> Result<ApplyResult, ApplyError> {
let (changed_nodes, requires_reload) = match cmd {
EditCommand::AddRuleSet { path } => {
let ids = self.cmd_add_rule_set(path)?;
(ids, true)
}
EditCommand::RemoveRuleSet { id } => {
let ids = self.cmd_remove_rule_set(id)?;
(ids, true)
}
EditCommand::AddRule { parent, rule } => {
let ids = self.cmd_add_rule(parent, rule)?;
(ids, true)
}
EditCommand::UpdateRule { id, rule } => {
let ids = self.cmd_update_rule(id, rule)?;
(ids, true)
}
EditCommand::DeleteRule { id } => {
let ids = self.cmd_delete_rule(id)?;
(ids, true)
}
EditCommand::MoveRule { id, new_index } => {
let ids = self.cmd_move_rule(id, new_index)?;
(ids, true)
}
EditCommand::UpdateRespond { id, respond } => {
let ids = self.cmd_update_respond(id, respond)?;
(ids, true)
}
EditCommand::UpdateRootSetting { key, value } => {
let ids = self.cmd_update_root_setting(key, value)?;
(ids, true)
}
};
let diagnostics = self.collect_diagnostics();
Ok(ApplyResult {
changed_nodes,
diagnostics,
requires_reload,
})
}
fn cmd_add_rule_set(&mut self, path: String) -> Result<Vec<NodeId>, ApplyError> {
let relative_dir = self.config_relative_dir().map_err(internal_path_err)?;
let joined = Path::new(&relative_dir).join(&path);
let path_str = joined.to_str().ok_or_else(|| ApplyError::InvalidPayload {
reason: format!(
"path contains non-UTF-8 bytes: {}",
joined.to_string_lossy()
),
})?;
let next_idx = self.config.service.rule_sets.len();
let new_rule_set = RuleSet::new(path_str, relative_dir.as_str(), next_idx)
.map_err(|e| ApplyError::InvalidPayload {
reason: format!("failed to load rule set `{}`: {}", path, e),
})?;
let file_paths = self
.config
.service
.rule_sets_file_paths
.get_or_insert_with(Vec::new);
file_paths.push(path.clone());
let new_len = self.config.service.rule_sets.len() + 1;
self.config.service.rule_sets.push(new_rule_set);
let rs_addr = NodeAddress::RuleSet {
rule_set: next_idx,
};
let rs_id = self.ids.insert(rs_addr);
let mut changed = vec![rs_id];
let new_rs = &self.config.service.rule_sets[next_idx];
for rule_idx in 0..new_rs.rules.len() {
let r_id = self.ids.insert(NodeAddress::Rule {
rule_set: next_idx,
rule: rule_idx,
});
let resp_id = self.ids.insert(NodeAddress::Respond {
rule_set: next_idx,
rule: rule_idx,
});
changed.push(r_id);
changed.push(resp_id);
}
debug_assert_eq!(new_len, self.config.service.rule_sets.len());
Ok(changed)
}
fn cmd_remove_rule_set(&mut self, id: NodeId) -> Result<Vec<NodeId>, ApplyError> {
let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
let NodeAddress::RuleSet { rule_set: idx } = addr else {
return Err(ApplyError::WrongNodeKind {
id,
reason: "expected a rule set id".to_owned(),
});
};
let len = self.config.service.rule_sets.len();
if idx >= len {
return Err(ApplyError::InvalidPayload {
reason: format!("rule set index {} out of range (len={})", idx, len),
});
}
let mut changed: Vec<NodeId> = Vec::new();
changed.push(id);
if let Some(removed_rs) = self.config.service.rule_sets.get(idx) {
for rule_idx in 0..removed_rs.rules.len() {
if let Some(r_id) = self.ids.id_for(NodeAddress::Rule {
rule_set: idx,
rule: rule_idx,
}) {
changed.push(r_id);
}
if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
rule_set: idx,
rule: rule_idx,
}) {
changed.push(resp_id);
}
}
}
self.config.service.rule_sets.remove(idx);
if let Some(paths) = self.config.service.rule_sets_file_paths.as_mut() {
if idx < paths.len() {
paths.remove(idx);
}
}
self.shift_rule_sets_down(idx);
for shifted_idx in idx..self.config.service.rule_sets.len() {
if let Some(shifted_id) = self
.ids
.id_for(NodeAddress::RuleSet {
rule_set: shifted_idx,
})
{
if !changed.contains(&shifted_id) {
changed.push(shifted_id);
}
}
}
Ok(changed)
}
fn cmd_add_rule(
&mut self,
parent: NodeId,
rule_payload: crate::view::RulePayload,
) -> Result<Vec<NodeId>, ApplyError> {
let addr = self
.ids
.lookup(parent)
.ok_or(ApplyError::UnknownNode { id: parent })?;
let NodeAddress::RuleSet { rule_set: rs_idx } = addr else {
return Err(ApplyError::WrongNodeKind {
id: parent,
reason: "expected a rule set id (parent for AddRule must be a rule set)".to_owned(),
});
};
let rule_set = self
.config
.service
.rule_sets
.get_mut(rs_idx)
.ok_or_else(|| ApplyError::InvalidPayload {
reason: format!("rule set index {} out of range", rs_idx),
})?;
let new_rule = build_rule_from_payload(rule_payload, rule_set, rs_idx, None)?;
let new_rule_idx = rule_set.rules.len();
rule_set.rules.push(new_rule);
let r_id = self.ids.insert(NodeAddress::Rule {
rule_set: rs_idx,
rule: new_rule_idx,
});
let resp_id = self.ids.insert(NodeAddress::Respond {
rule_set: rs_idx,
rule: new_rule_idx,
});
Ok(vec![parent, r_id, resp_id])
}
fn cmd_update_rule(
&mut self,
id: NodeId,
rule_payload: crate::view::RulePayload,
) -> Result<Vec<NodeId>, ApplyError> {
let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
let NodeAddress::Rule {
rule_set: rs_idx,
rule: rule_idx,
} = addr
else {
return Err(ApplyError::WrongNodeKind {
id,
reason: "expected a rule id".to_owned(),
});
};
let rule_set = self
.config
.service
.rule_sets
.get_mut(rs_idx)
.ok_or_else(|| ApplyError::InvalidPayload {
reason: format!("rule set index {} out of range", rs_idx),
})?;
let existing = rule_set.rules.get(rule_idx).cloned();
let new_rule = build_rule_from_payload(
rule_payload,
rule_set,
rs_idx,
existing.as_ref(),
)?;
*rule_set
.rules
.get_mut(rule_idx)
.ok_or_else(|| ApplyError::InvalidPayload {
reason: format!("rule index {} out of range", rule_idx),
})? = new_rule;
let resp_id = self
.ids
.id_for(NodeAddress::Respond {
rule_set: rs_idx,
rule: rule_idx,
})
.unwrap_or_else(NodeId::new);
Ok(vec![id, resp_id])
}
fn cmd_delete_rule(&mut self, id: NodeId) -> Result<Vec<NodeId>, ApplyError> {
let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
let NodeAddress::Rule {
rule_set: rs_idx,
rule: rule_idx,
} = addr
else {
return Err(ApplyError::WrongNodeKind {
id,
reason: "expected a rule id".to_owned(),
});
};
let rule_set = self
.config
.service
.rule_sets
.get_mut(rs_idx)
.ok_or_else(|| ApplyError::InvalidPayload {
reason: format!("rule set index {} out of range", rs_idx),
})?;
if rule_idx >= rule_set.rules.len() {
return Err(ApplyError::InvalidPayload {
reason: format!("rule index {} out of range", rule_idx),
});
}
let mut changed: Vec<NodeId> = Vec::new();
changed.push(id);
if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
rule_set: rs_idx,
rule: rule_idx,
}) {
changed.push(resp_id);
}
rule_set.rules.remove(rule_idx);
self.shift_rules_down(rs_idx, rule_idx);
let new_rule_count = self.config.service.rule_sets[rs_idx].rules.len();
for shifted_idx in rule_idx..new_rule_count {
if let Some(r_id) = self.ids.id_for(NodeAddress::Rule {
rule_set: rs_idx,
rule: shifted_idx,
}) {
if !changed.contains(&r_id) {
changed.push(r_id);
}
}
if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
rule_set: rs_idx,
rule: shifted_idx,
}) {
if !changed.contains(&resp_id) {
changed.push(resp_id);
}
}
}
Ok(changed)
}
fn cmd_move_rule(&mut self, id: NodeId, new_index: usize) -> Result<Vec<NodeId>, ApplyError> {
let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
let NodeAddress::Rule {
rule_set: rs_idx,
rule: old_idx,
} = addr
else {
return Err(ApplyError::WrongNodeKind {
id,
reason: "expected a rule id".to_owned(),
});
};
let rule_set = self
.config
.service
.rule_sets
.get_mut(rs_idx)
.ok_or_else(|| ApplyError::InvalidPayload {
reason: format!("rule set index {} out of range", rs_idx),
})?;
if old_idx >= rule_set.rules.len() || new_index >= rule_set.rules.len() {
return Err(ApplyError::InvalidPayload {
reason: format!(
"move out of bounds: old_idx={}, new_index={}, len={}",
old_idx,
new_index,
rule_set.rules.len()
),
});
}
if old_idx == new_index {
return Ok(vec![id]);
}
let rule = rule_set.rules.remove(old_idx);
rule_set.rules.insert(new_index, rule);
self.reorder_rule_ids(rs_idx, old_idx, new_index);
let lo = old_idx.min(new_index);
let hi = old_idx.max(new_index);
let mut changed: Vec<NodeId> = Vec::new();
for idx in lo..=hi {
if let Some(r_id) = self.ids.id_for(NodeAddress::Rule {
rule_set: rs_idx,
rule: idx,
}) {
changed.push(r_id);
}
if let Some(resp_id) = self.ids.id_for(NodeAddress::Respond {
rule_set: rs_idx,
rule: idx,
}) {
changed.push(resp_id);
}
}
Ok(changed)
}
fn cmd_update_respond(
&mut self,
id: NodeId,
respond: crate::view::RespondPayload,
) -> Result<Vec<NodeId>, ApplyError> {
let addr = self.ids.lookup(id).ok_or(ApplyError::UnknownNode { id })?;
let NodeAddress::Respond {
rule_set: rs_idx,
rule: rule_idx,
} = addr
else {
return Err(ApplyError::WrongNodeKind {
id,
reason: "expected a respond id".to_owned(),
});
};
let rule = self
.config
.service
.rule_sets
.get_mut(rs_idx)
.and_then(|rs| rs.rules.get_mut(rule_idx))
.ok_or_else(|| ApplyError::InvalidPayload {
reason: format!(
"rule at rule_set={}, rule={} not found",
rs_idx, rule_idx
),
})?;
rule.respond = build_respond_from_payload(respond);
let rule_set = &self.config.service.rule_sets[rs_idx];
let derived = rule_set.rules[rule_idx].compute_derived_fields(rule_set, rule_idx, rs_idx);
self.config.service.rule_sets[rs_idx].rules[rule_idx] = derived;
Ok(vec![id])
}
fn cmd_update_root_setting(
&mut self,
key: crate::view::RootSettingKey,
value: EditValue,
) -> Result<Vec<NodeId>, ApplyError> {
use crate::view::RootSettingKey::*;
match key {
ListenerIpAddress => {
let s = value_as_string(&value)?;
let listener = self.config.listener.get_or_insert_with(Default::default);
listener.ip_address = s;
}
ListenerPort => {
let n = value_as_integer(&value)?;
if !(0..=u16::MAX as i64).contains(&n) {
return Err(ApplyError::InvalidPayload {
reason: format!("port {} not in 0..=65535", n),
});
}
let listener = self.config.listener.get_or_insert_with(Default::default);
listener.port = n as u16;
}
ServiceFallbackRespondDir => {
let s = value_as_string(&value)?;
self.config.service.fallback_respond_dir = s;
}
ServiceStrategy => {
let s = value_as_string(&value)?;
match s.as_str() {
"first_match" => {
self.config.service.strategy =
Some(apimock_routing::Strategy::FirstMatch);
}
other => {
return Err(ApplyError::InvalidPayload {
reason: format!("unknown strategy: {}", other),
});
}
}
}
}
let id = self
.ids
.id_for(NodeAddress::Root)
.expect("root id seeded at load");
Ok(vec![id])
}
}