use std::collections::BTreeMap;
use shep_core::config::ApplyGroup;
use super::pane::PaneEdit;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum EditKey {
Field(String),
Env(String),
}
impl EditKey {
fn of(edit: &PaneEdit) -> Self {
match edit {
PaneEdit::Set { key, .. } => Self::Field(key.clone()),
PaneEdit::SetEnv { key, .. } => Self::Env(key.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Edit {
edit: PaneEdit,
impact: Option<ApplyGroup>,
}
impl Edit {
#[must_use]
pub const fn edit(&self) -> &PaneEdit {
&self.edit
}
#[cfg_attr(
not(test),
allow(
dead_code,
reason = "read by this module's own tests; no non-test caller reads it"
)
)]
#[must_use]
pub const fn impact(&self) -> Option<ApplyGroup> {
self.impact
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Edits {
entries: BTreeMap<EditKey, Edit>,
order: Vec<EditKey>,
}
impl Edits {
pub fn set(&mut self, edit: PaneEdit, impact: Option<ApplyGroup>) {
let key = EditKey::of(&edit);
self.order.retain(|filed| filed != &key);
self.order.push(key.clone());
self.entries.insert(key, Edit { edit, impact });
}
pub fn remove(&mut self, key: &EditKey) {
self.entries.remove(key);
self.order.retain(|filed| filed != key);
}
pub fn undo(&mut self) -> Option<EditKey> {
let key = self.order.pop()?;
self.entries.remove(&key);
Some(key)
}
#[must_use]
pub fn get(&self, key: &EditKey) -> Option<&Edit> {
self.entries.get(key)
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&EditKey, &Edit)> {
self.entries.iter()
}
#[must_use]
pub fn into_writes(self) -> Vec<PaneEdit> {
self.entries.into_values().map(|entry| entry.edit).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use shep_core::protocol::EnvValue;
use super::super::pane::FieldValue;
fn field(key: &str, value: serde_json::Value) -> PaneEdit {
PaneEdit::Set {
key: key.to_owned(),
value: FieldValue::from(value),
}
}
fn env(key: &str, value: Option<EnvValue>) -> PaneEdit {
PaneEdit::SetEnv {
key: key.to_owned(),
value,
}
}
#[test]
fn an_edit_is_readable_by_the_key_it_was_filed_under() {
let mut edits = Edits::default();
edits.set(
field("cwd", json!("/srv/app")),
Some(ApplyGroup::NeedsRespawn),
);
edits.set(field("autostart", json!(true)), Some(ApplyGroup::NextSpawn));
assert_eq!(edits.len(), 2);
let cwd = edits
.get(&EditKey::Field("cwd".to_owned()))
.expect("entry filed under cwd");
assert_eq!(cwd.impact(), Some(ApplyGroup::NeedsRespawn));
match cwd.edit() {
PaneEdit::Set { key, .. } => assert_eq!(key, "cwd"),
other => panic!("expected a Set edit, got {other:?}"),
}
let autostart = edits
.get(&EditKey::Field("autostart".to_owned()))
.expect("entry filed under autostart");
assert_eq!(autostart.impact(), Some(ApplyGroup::NextSpawn));
match autostart.edit() {
PaneEdit::Set { key, .. } => assert_eq!(key, "autostart"),
other => panic!("expected a Set edit, got {other:?}"),
}
}
#[test]
fn a_field_key_and_an_env_key_of_the_same_name_are_two_entries() {
let mut edits = Edits::default();
edits.set(field("env", json!({})), None);
edits.set(env("env", None), None);
assert_eq!(edits.len(), 2);
}
#[test]
fn undo_pops_the_most_recently_touched_key() {
let mut edits = Edits::default();
edits.set(field("cwd", json!("/a")), None);
edits.set(field("script", json!("b.js")), None);
assert_eq!(edits.undo(), Some(EditKey::Field("script".to_owned())));
assert_eq!(edits.len(), 1);
}
#[test]
fn re_editing_a_field_moves_it_to_the_end_of_the_undo_order() {
let mut edits = Edits::default();
edits.set(field("cwd", json!("/a")), None);
edits.set(field("script", json!("b.js")), None);
edits.set(field("cwd", json!("/b")), None);
assert_eq!(edits.undo(), Some(EditKey::Field("cwd".to_owned())));
assert_eq!(edits.undo(), Some(EditKey::Field("script".to_owned())));
assert_eq!(edits.undo(), None);
}
#[test]
fn undo_on_an_empty_set_reports_nothing_and_does_not_panic() {
assert_eq!(Edits::default().undo(), None);
}
#[test]
fn remove_drops_an_entry_and_its_place_in_the_undo_order() {
let mut edits = Edits::default();
edits.set(field("cwd", json!("/a")), None);
edits.set(field("script", json!("b.js")), None);
edits.remove(&EditKey::Field("script".to_owned()));
assert_eq!(edits.undo(), Some(EditKey::Field("cwd".to_owned())));
assert!(edits.is_empty());
}
#[test]
fn iteration_is_by_key_not_by_undo_order() {
let mut edits = Edits::default();
edits.set(field("script", json!("b.js")), None);
edits.set(field("cwd", json!("/a")), None);
let keys: Vec<_> = edits.iter().map(|(key, _)| key.clone()).collect();
assert_eq!(
keys,
vec![
EditKey::Field("cwd".to_owned()),
EditKey::Field("script".to_owned())
]
);
}
#[test]
fn into_writes_produces_one_wire_edit_per_entry() {
let mut edits = Edits::default();
edits.set(field("cwd", json!("/a")), None);
edits.set(env("NODE_ENV", None), None);
let writes = edits.into_writes();
assert_eq!(writes.len(), 2);
assert!(matches!(writes[0], PaneEdit::Set { .. }));
assert!(matches!(writes[1], PaneEdit::SetEnv { .. }));
}
#[test]
fn debug_prints_the_whole_set_and_no_value_in_it() {
let mut edits = Edits::default();
edits.set(
field("cwd", json!("/home/someone/secret")),
Some(ApplyGroup::NeedsRespawn),
);
let printed = format!("{edits:?}");
assert_eq!(
printed,
"Edits { entries: {Field(\"cwd\"): Edit { edit: Set { key: \"cwd\", value: FieldValue(<string>) }, impact: Some(NeedsRespawn) }}, order: [Field(\"cwd\")] }"
);
}
}