iforgor 0.3.0

The CLI tool for all those commands you forget about
Documentation
use {
    serde::{Deserialize, Serialize},
    std::collections::BTreeMap,
};

use crate::command::{CommandId, UserCommand};

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct History {
    pub history: Vec<CommandId>,
}

impl History {
    const MAX_ENTRIES: usize = 100;

    pub fn add_entry(&mut self, id: &CommandId) {
        self.history.retain(|hid| hid != id);
        self.history.push(id.clone());

        // Cap history size.
        if self.history.len() > Self::MAX_ENTRIES {
            let excess = self.history.len() - Self::MAX_ENTRIES;
            self.history.drain(..excess);
        }
    }

    pub fn prune(&mut self, valid_ids: &BTreeMap<CommandId, UserCommand>) {
        self.history.retain(|id| valid_ids.contains_key(id));
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_entry_to_empty() {
        let mut h = History::default();
        h.add_entry(&"a".to_string());
        assert_eq!(h.history, vec!["a"]);
    }

    #[test]
    fn add_entry_deduplicates_and_moves_to_end() {
        let mut h = History {
            history: vec!["a".into(), "b".into(), "c".into()],
        };
        h.add_entry(&"a".to_string());
        assert_eq!(h.history, vec!["b", "c", "a"]);
    }

    #[test]
    fn add_entry_new_goes_to_end() {
        let mut h = History {
            history: vec!["a".into(), "b".into()],
        };
        h.add_entry(&"c".to_string());
        assert_eq!(h.history, vec!["a", "b", "c"]);
    }

    fn make_commands(ids: &[&str]) -> BTreeMap<CommandId, UserCommand> {
        ids.iter()
            .map(|id| {
                (
                    id.to_string(),
                    UserCommand {
                        name: id.to_string(),
                        script: String::new(),
                        ..default_command()
                    },
                )
            })
            .collect()
    }

    fn default_command() -> UserCommand {
        UserCommand {
            id: None,
            name: String::new(),
            description: None,
            script: String::new(),
            args: vec![],
            tags: vec![],
            only_on: None,
            shell: None,
            only_in_dir: vec![],
            risky: false,
            after_run: None,
            working_dir: None,
            source_path: None,
            project_dir: None,
            domain: None,
        }
    }

    #[test]
    fn prune_removes_stale_ids() {
        let mut h = History {
            history: vec!["a".into(), "b".into(), "c".into()],
        };
        let commands = make_commands(&["a", "c"]);
        h.prune(&commands);
        assert_eq!(h.history, vec!["a", "c"]);
    }

    #[test]
    fn prune_empty_history() {
        let mut h = History::default();
        let commands = make_commands(&["a"]);
        h.prune(&commands);
        assert!(h.history.is_empty());
    }

    #[test]
    fn prune_all_stale() {
        let mut h = History {
            history: vec!["x".into(), "y".into()],
        };
        let commands = make_commands(&["a"]);
        h.prune(&commands);
        assert!(h.history.is_empty());
    }
}