extern crate base32;
extern crate regex;
extern crate serde;
extern crate toml;
use regex::Regex;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
#[derive(Debug, PartialEq)]
pub enum ValidationError {
IllegalCharacter(&'static str), TooShortLength(&'static str), TooLongLength(&'static str), Deplication(&'static str), Requires(&'static str), }
type ValidationResult = Result<(), ValidationError>;
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::IllegalCharacter(msg)
| Self::TooShortLength(msg)
| Self::TooLongLength(msg)
| Self::Deplication(msg)
| Self::Requires(msg) => write!(f, "{}", msg),
}
}
}
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Config {
profiles: Vec<Profile>,
}
impl Config {
pub fn new_profile(&mut self, name: &str, secret: &str) -> ValidationResult {
self.push_profile(Profile::new(name, secret))
}
fn push_profile(&mut self, profile: Profile) -> ValidationResult {
match self.validate_profile(&profile) {
Ok(_) => {
self.profiles.push(profile);
Ok(())
}
Err(err) => Err(err),
}
}
fn validate_profile(&self, profile: &Profile) -> ValidationResult {
if self.find_by_name(&profile.name).is_some() {
return Err(ValidationError::Deplication("This name already exists."));
}
profile.is_vaild()
}
pub fn get_secret_by_name(&self, name: &str) -> Option<Vec<u8>> {
if let Some(profile) = self.find_by_name(name) {
return profile.get_secret();
}
None
}
pub fn get_profiles(&self) -> &Vec<Profile> {
&self.profiles
}
pub fn remove_profile(&mut self, name: &str) -> Result<(), String> {
let mut index: Option<usize> = None;
self.profiles.iter().enumerate().for_each(|(i, profile)| {
if profile.name == name {
index = Some(i);
}
});
match index {
Some(i) => {
self.profiles.remove(i);
Ok(())
}
_ => Err(format!("Can't find this profile: {}", name)),
}
}
fn find_by_name(&self, name: &str) -> Option<&Profile> {
self.profiles
.iter()
.find(|&profile| *profile.get_name() == *name)
}
pub fn serialize(&self) -> Result<String, String> {
match toml::to_string(&self) {
Ok(data) => Ok(data),
Err(err) => Err(err.to_string()),
}
}
pub fn deserialize(&mut self, content: &str) -> Result<(), String> {
match toml::from_str(content) {
Ok(config) => {
*self = config;
Ok(())
}
Err(err) => Err(err.to_string()),
}
}
}
#[derive(Serialize, Deserialize, Default, Debug)]
pub struct Profile {
name: String,
secret: String,
}
impl Profile {
pub fn new(name: &str, secret: &str) -> Self {
Profile {
name: name.to_string(),
secret: secret.to_string(),
}
}
pub fn get_name(&self) -> &String {
&self.name
}
pub fn get_secret(&self) -> Option<Vec<u8>> {
base32::decode(base32::Alphabet::RFC4648 { padding: true }, &self.secret)
}
pub fn is_vaild(&self) -> ValidationResult {
self.is_valid_name()?;
self.is_valid_secret()?;
Ok(())
}
fn is_valid_name(&self) -> ValidationResult {
if self.name.len() < 3 {
return Err(ValidationError::TooShortLength(
"Name requires at least 3 characters.",
));
}
if 20 < self.name.len() {
return Err(ValidationError::TooLongLength(
"Name requires 20 characters or less.",
));
}
const VALID_NAME_PATTERN: &str = r"^[[[:alnum:]]_@-]+\z";
let re = Regex::new(VALID_NAME_PATTERN).unwrap();
if !re.is_match(&self.name) {
return Err(ValidationError::IllegalCharacter(
"Name can contain only alphabet, number and symbol (@-_) .",
));
}
Ok(())
}
fn is_valid_secret(&self) -> ValidationResult {
if self.secret.is_empty() {
return Err(ValidationError::Requires("Secret must be present."));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serialize_profile() {
let profile = Profile::new("test", "secret");
let expected = "name = \"test\"\nsecret = \"secret\"\n";
assert_eq!(toml::to_string(&profile).unwrap(), expected);
}
#[test]
fn serialize_config() {
let config = Config {
profiles: vec![Profile::new("test", "secret")],
};
let expected = r#"[[profiles]]
name = "test"
secret = "secret"
"#;
assert_eq!(config.serialize().unwrap(), expected);
}
#[test]
fn deserialize_config() {
let string_config = "[[profiles]]\nname = \"test\"\nsecret = \"secret\"\n ";
let mut config: Config = Default::default();
config.deserialize(string_config).unwrap();
assert_eq!(config.profiles.len(), 1);
assert_eq!(config.profiles[0].name, "test");
assert_eq!(config.profiles[0].secret, "secret");
}
#[test]
fn push_profile_validation_when_name_duplicates() {
let mut config: Config = Default::default();
config.new_profile("test", "a").unwrap();
let second_time = config.new_profile("test", "");
assert!(second_time.is_err());
}
#[test]
fn push_profile_validation_when_name_contains_multi_byte_char() {
let mut config: Config = Default::default();
let result = config.new_profile("あ", "");
assert_eq!(
result,
Err(ValidationError::IllegalCharacter(
"Name can contain only alphabet, number and symbol (@-_) ."
))
);
}
#[test]
fn push_profile_validation_when_name_contains_symbols_other_than_hyphen_and_underscore_and_at_sign(
) {
let mut config: Config = Default::default();
let result = config.new_profile("!# $%&", "");
assert_eq!(
result,
Err(ValidationError::IllegalCharacter(
"Name can contain only alphabet, number and symbol (@-_) ."
))
);
}
#[test]
fn push_profile_validation_when_name_contains_approved_symbols() {
let mut config: Config = Default::default();
let result = config.new_profile("-_@", "secret");
assert_eq!(result, Ok(()));
}
#[test]
fn push_profile_validation_when_name_is_too_short() {
let mut config: Config = Default::default();
let result = config.new_profile("ab", "");
assert_eq!(
result,
Err(ValidationError::TooShortLength(
"Name requires at least 3 characters."
))
);
}
#[test]
fn push_profile_validation_when_name_is_too_long() {
let mut config: Config = Default::default();
let result = config.new_profile(&"a".repeat(21), "");
assert_eq!(
result,
Err(ValidationError::TooLongLength(
"Name requires 20 characters or less."
))
);
}
#[test]
fn push_profile_validation_when_secret_is_blank() {
let mut config: Config = Default::default();
let result = config.new_profile("aaa", "");
assert_eq!(
result,
Err(ValidationError::Requires("Secret must be present."))
);
}
}