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 {
pub(crate) fn get(&self, key: &String) -> Option<String> {
self.0.get(key).cloned()
}
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()
))
}
}