fuzzel-pass 0.1.0

A password-store frontend for auto-typing passwords
use std::{collections::HashMap, str::FromStr};

use rust_yaml::{Value, Yaml};

use crate::{error::Error, shell::Result};

#[derive(Debug, PartialEq)]
pub(crate) struct Password(HashMap<String, String>);

impl FromStr for Password {
    type Err = Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        let mut lines = s.lines();
        let password = if let Some(entry) = lines.next() {
            entry.to_string()
        } else {
            return Err(Error::EmptyPassword);
        };

        let mut data = HashMap::new();

        data.insert("password".to_string(), password);

        let raw: Vec<&str> = lines.collect();
        let raw_doc = raw.join("\n");

        populate_data(&mut data, raw_doc)?;

        Ok(Self(data))
    }
}

impl Password {
    /// A simple way to get a value matching a given key
    pub(crate) fn get(&self, key: &String) -> Option<String> {
        self.0.get(key).cloned()
    }

    /// generates a menu string to be usez with [`Fuzzel::pick`] function
    ///
    /// [`Fuzzel::pick`]: crate::shell::fuzzel::Fuzzel::pick.
    pub(crate) fn menu(&self) -> String {
        let mut provided = self
            .0
            .keys()
            .filter(|key| *key != "autotype" && *key != "password")
            .cloned()
            .collect::<Vec<_>>();
        let mut all: Vec<String> = vec!["password".into(), "autotype".into()];
        provided.sort();
        all.append(&mut provided);
        all.join("\n")
    }
}

fn populate_data(data: &mut HashMap<String, String>, raw_doc: String) -> Result<()> {
    let yaml = Yaml::new();

    if let Value::Mapping(doc) = yaml
        .load_str(raw_doc.as_str())
        .map_err(|err| Error::Yaml(err.to_string()))?
    {
        for pair in doc {
            if let (Value::String(key), Value::String(val)) = pair {
                data.insert(key, val);
            }
        }
    };

    Ok(())
}

#[cfg(test)]
mod test {
    use super::*;
    use common_macros::hash_map;
    use indoc::indoc;

    use pretty_assertions::assert_eq;

    #[test]
    fn test_parse() {
        let doc = indoc! {"
        Very Secret Password
        ---
        user: me@example.com
        url: https://bank.example.com/login
        autotype: user :tab :password :enter
        "}
        .to_string();

        let expected = sample();

        if let Ok(actual) = doc.parse() {
            assert_eq!(expected, actual);
        }
    }

    #[test]
    fn test_get() {
        let password = sample();

        for case in [
            ("password", Some("Very Secret Password")),
            ("user", Some("me@example.com")),
            ("url", Some("https://bank.example.com/login")),
            ("autotype", Some("user :tab :password :enter")),
            ("other", None),
        ] {
            let (key, expected) = (case.0.to_string(), case.1.map(|val| val.to_string()));
            assert_eq!(expected, password.get(&key));
        }
    }

    #[test]
    fn test_parse_empty_password() {
        let result: Result<Password> = String::new().parse();

        assert!(result.is_err());

        if let Err(err) = result {
            assert_eq!(Error::EmptyPassword, err);
        }
    }

    #[test]
    fn test_menu() {
        let password = sample();
        let expected = indoc! {"password
        autotype
        url
        user"};
        assert_eq!(expected, password.menu());
    }

    fn sample() -> Password {
        Password(hash_map!(
            "password".into() => "Very Secret Password".into(),
            "user".into() => "me@example.com".into(),
            "url".into() => "https://bank.example.com/login".into(),
            "autotype".into() => "user :tab :password :enter".into()
        ))
    }
}