wekan-cli 2.0.0

Agent-first, human-friendly command-line client for Wekan
Documentation
use secrecy::{ExposeSecret, SecretString};

#[derive(Default)]
pub struct Redactor<'a> {
    secrets: Vec<&'a SecretString>,
}

impl<'a> Redactor<'a> {
    pub fn with_secret(secret: &'a SecretString) -> Self {
        Self {
            secrets: vec![secret],
        }
    }

    pub fn and_secret(mut self, secret: &'a SecretString) -> Self {
        self.secrets.push(secret);
        self
    }

    pub fn redact(&self, value: &str) -> String {
        let mut secrets: Vec<_> = self
            .secrets
            .iter()
            .map(|secret| secret.expose_secret())
            .filter(|secret| !secret.is_empty())
            .collect();
        secrets.sort_unstable_by_key(|secret| std::cmp::Reverse(secret.len()));

        secrets.into_iter().fold(value.to_owned(), |text, secret| {
            text.replace(secret, "[REDACTED]")
        })
    }
}

#[cfg(test)]
mod tests {
    use secrecy::SecretString;

    use super::Redactor;

    #[test]
    fn redacts_all_occurrences_without_exposing_debug_values() {
        let secret = SecretString::from("do not print me".to_owned());
        let redactor = Redactor::with_secret(&secret);
        let result = redactor.redact("do not print me / do not print me");

        assert_eq!(result, "[REDACTED] / [REDACTED]");
        assert!(!format!("{secret:?}").contains("do not print me"));
    }

    #[test]
    fn redacts_overlapping_secrets_without_leaking_a_suffix() {
        let password = SecretString::from("123".to_owned());
        let code = SecretString::from("123456".to_owned());
        let redactor = Redactor::with_secret(&password).and_secret(&code);

        assert_eq!(
            redactor.redact("code 123456 was rejected"),
            "code [REDACTED] was rejected"
        );
    }
}