#![deny(
non_camel_case_types,
non_snake_case,
path_statements,
trivial_numeric_casts,
unstable_features,
unused_allocation,
unused_import_braces,
unused_imports,
unused_must_use,
unused_mut,
unused_qualifications,
while_true,
)]
use std::collections::BTreeMap;
use std::io::Read;
use std::io::Write;
use std::path::PathBuf;
use std::fs::OpenOptions;
use vobject::vcard::Vcard;
use vobject::vcard::VcardBuilder;
use vobject::write_component;
use toml_query::read::TomlValueReadExt;
use toml_query::read::Partial;
use toml::Value;
use uuid::Uuid;
use failure::Error;
use failure::err_msg;
use failure::Fallible as Result;
use failure::ResultExt;
use libimagcontact::store::ContactStore;
use libimagrt::runtime::Runtime;
use libimagutil::warn_result::WarnResult;
const TEMPLATE : &str = include_str!("../static/new-contact-template.toml");
#[cfg(test)]
mod test {
use toml::Value;
use super::TEMPLATE;
const TEMPLATE_WITH_DATA : &str = include_str!("../static/new-contact-template-test.toml");
#[test]
fn test_validity_template_toml() {
let _ : Value = ::toml::de::from_str(TEMPLATE).unwrap();
}
#[test]
fn test_validity_template_toml_without_comments() {
let _ : Value = ::toml::de::from_str(TEMPLATE_WITH_DATA).unwrap();
}
}
fn ask_continue(inputstream: &mut dyn Read, outputstream: &mut dyn Write) -> Result<bool> {
::libimaginteraction::ask::ask_bool("Edit tempfile", Some(true), inputstream, outputstream)
}
pub fn create(rt: &Runtime) -> Result<()> {
let scmd = rt.cli().subcommand_matches("create").unwrap();
let mut template = String::from(TEMPLATE);
let collection_name = rt.cli().value_of("ref-collection-name").unwrap_or("contacts");
let collection_name = String::from(collection_name);
let ref_config = rt .config()
.ok_or_else(|| err_msg("Configuration missing, cannot continue!"))?
.read_partial::<libimagentryref::reference::Config>()?
.ok_or_else(|| format_err!("Configuration missing: {}", libimagentryref::reference::Config::LOCATION))?;
let (mut dest, location, uuid) : (Box<dyn Write>, Option<PathBuf>, String) = {
if let Some(mut fl) = scmd.value_of("file-location").map(PathBuf::from) {
let uuid = if fl.is_file() {
return Err(err_msg("File does exist, cannot create/override"))
} else if fl.is_dir() {
let uuid = Uuid::new_v4().to_hyphenated().to_string();
fl.push(uuid.clone());
fl.set_extension("vcf");
info!("Creating file: {:?}", fl);
Some(uuid)
} else {
let has_vcf_ext = fl
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext == "vcf")
.unwrap_or(false);
if !has_vcf_ext {
let f = fl.file_name()
.and_then(|ext| ext.to_str())
.map(|f| format!(" '{}' ", f)) .unwrap_or_else(|| String::from(" "));
warn!("File {} has no extension 'vcf'", f); warn!("other tools might not recognize this as contact.");
warn!("Continuing...");
}
None
};
debug!("Destination = {:?}", fl);
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(fl.clone())
.map_err(Error::from)
.context("Cannot create/open destination File. Stopping.")?;
let uuid_string = match uuid {
Some(s) => s,
None => fl.file_name()
.and_then(|fname| fname.to_str())
.map(String::from)
.ok_or_else(|| err_msg("Cannot calculate UUID for vcard"))?,
};
(Box::new(file), Some(fl), uuid_string)
} else {
let uuid = Uuid::new_v4().to_hyphenated().to_string();
(Box::new(rt.stdout()), None, uuid)
}
};
let mut input = rt.stdin().ok_or_else(|| {
err_msg("No input stream. Cannot ask for permission")
})?;
let mut output = rt.stdout();
loop {
::libimagentryedit::edit::edit_in_tmpfile(&rt, &mut template)?;
if template == TEMPLATE || template.is_empty() {
return Err(err_msg("No (changed) content in tempfile. Not doing anything."))
}
match ::toml::de::from_str(&template)
.map_err(Error::from)
.and_then(|toml| parse_toml_into_vcard(&mut output, &mut input, toml, uuid.clone()))
{
Err(e) => {
error!("Error parsing template");
if ask_continue(&mut input, &mut output)? {
continue;
} else {
return Err(e)
}
},
Ok(None) => continue,
Ok(Some(vcard)) => {
if template == TEMPLATE || template.is_empty() {
let q = "Abort contact creating";
if ::libimaginteraction::ask::ask_bool(q, Some(false), &mut input, &mut output)? {
return Ok(())
} else {
continue;
}
}
let vcard_string = write_component(&vcard);
dest.write_all(&vcard_string.as_bytes())?;
break;
}
}
}
if let Some(location) = location {
if !scmd.is_present("dont-track") {
let entry = rt.store().create_from_path(&location, &ref_config, &collection_name)?;
rt.report_touched(entry.get_location())?;
info!("Created entry in store");
} else {
info!("Not creating entry in store");
}
} else {
info!("Cannot track stdout-created contact information");
}
info!("Ready");
Ok(())
}
#[clippy::cognitive_complexity = "71"]
fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Value, uuid: String) -> Result<Option<Vcard>> {
let mut vcard = VcardBuilder::new().with_uid(uuid);
{ debug!("Parsing name");
let firstname = read_str_from_toml(&toml, "name.first", true)?;
trace!("firstname = {:?}", firstname);
let lastname = read_str_from_toml(&toml, "name.last", true)?;
trace!("lastname = {:?}", lastname);
vcard = vcard.with_name(parameters!(),
read_str_from_toml(&toml, "name.prefix", false)?,
firstname.clone(),
read_str_from_toml(&toml, "name.additional", false)?,
lastname.clone(),
read_str_from_toml(&toml, "name.suffix", false)?);
if let (Some(first), Some(last)) = (firstname, lastname) {
trace!("Building fullname: '{} {}'", first, last);
vcard = vcard.with_fullname(format!("{} {}", first, last));
}
}
{ debug!("Parsing person information");
let birthday = read_str_from_toml(&toml, "person.birthday", false)?;
trace!("birthday = {:?}", birthday);
if let Some(bday) = birthday {
vcard = vcard.with_bday(parameters!(), bday);
}
}
{ debug!("Parsing nicknames");
match toml.read("nickname").map_err(Error::from)? {
Some(&Value::Array(ref ary)) => {
for (i, element) in ary.iter().enumerate() {
let nicktype = match read_str_from_toml(element, "type", false)? {
None => BTreeMap::new(),
Some(p) => {
let mut m = BTreeMap::new();
m.insert("TYPE".into(), p);
m
},
};
let name = match read_str_from_toml(element, "name", false)? {
Some(p) => p,
None => {
error!("Key 'nickname.[{}].name' missing", i);
if ask_continue(input, output)? {
return Ok(None)
} else {
return Err(format_err!("Key 'nickname.[{}].name' missing", i))
}
},
};
trace!("nick type = {:?}", nicktype);
trace!("name = {:?}", name);
vcard = vcard.with_nickname(nicktype, name);
}
},
Some(&Value::String(ref name)) => {
vcard = vcard.with_nickname(parameters!(), name.clone());
}
Some(_) => {
error!("Type Error: Expected Array or String at 'nickname'");
if ask_continue(input, output)? {
return Ok(None)
} else {
return Err(format_err!("Type Error: Expected Array or String at 'nickname'"))
}
},
None => {
},
}
}
{ debug!("Parsing organisation");
if let Some(orgs) = read_strary_from_toml(&toml, "organisation.name")? {
trace!("orgs = {:?}", orgs);
vcard = vcard.with_org(orgs);
}
if let Some(title) = read_str_from_toml(&toml, "organisation.title", false)? {
trace!("title = {:?}", title);
vcard = vcard.with_title(title);
}
if let Some(role) = read_str_from_toml(&toml, "organisation.role", false)? {
trace!("role = {:?}", role);
vcard = vcard.with_role(role);
}
}
{ debug!("Parse phone");
match toml.read("person.phone")? {
Some(&Value::Array(ref ary)) => {
for (i, element) in ary.iter().enumerate() {
let phonetype = match read_str_from_toml(element, "type", false)? {
Some(p) => p,
None => {
error!("Key 'phones.[{}].type' missing", i);
if ask_continue(input, output)? {
return Ok(None)
} else {
return Err(format_err!("Key 'phones.[{}].type' missing", i))
}
}
};
let number = match read_str_from_toml(element, "number", false)? {
Some(p) => p,
None => {
error!("Key 'phones.[{}].number' missing", i);
if ask_continue(input, output)? {
return Ok(None)
} else {
return Err(format_err!("Key 'phones.[{}].number' missing", i))
}
}
};
trace!("phonetype = {:?}", phonetype);
trace!("number = {:?}", number);
vcard = vcard.with_tel(parameters!("TYPE" => phonetype), number);
}
},
Some(_) => {
error!("Expected Array at 'phones'.");
if ask_continue(input, output)? {
return Ok(None)
} else {
return Err(format_err!("Expected Array at 'phones'."))
}
},
None => {
},
}
}
{ debug!("Parsing address");
match toml.read("addresses")? {
Some(&Value::Array(ref ary)) => {
for (i, element) in ary.iter().enumerate() {
let adrtype = match read_str_from_toml(element, "type", false)? {
None => {
error!("Key 'adresses.[{}].type' missing", i);
if ask_continue(input, output)? {
return Ok(None)
} else {
return Err(format_err!("Key 'adresses.[{}].type' missing", i))
}
},
Some(p) => p,
};
trace!("adrtype = {:?}", adrtype);
let bx = read_str_from_toml(element, "box", false)?;
let extended = read_str_from_toml(element, "extended", false)?;
let street = read_str_from_toml(element, "street", false)?;
let code = read_str_from_toml(element, "code", false)?;
let city = read_str_from_toml(element, "city", false)?;
let region = read_str_from_toml(element, "region", false)?;
let country = read_str_from_toml(element, "country", false)?;
trace!("bx = {:?}", bx);
trace!("extended = {:?}", extended);
trace!("street = {:?}", street);
trace!("code = {:?}", code);
trace!("city = {:?}", city);
trace!("region = {:?}", region);
trace!("country = {:?}", country);
vcard = vcard.with_adr(
parameters!("TYPE" => adrtype),
bx, extended, street, code, city, region, country
);
}
},
Some(_) => {
error!("Type Error: Expected Array at 'addresses'");
if ask_continue(input, output)? {
return Ok(None)
} else {
return Err(format_err!("Type Error: Expected Array at 'addresses'"))
}
},
None => {
},
}
}
{ debug!("Parsing email");
match toml.read("person.email")? {
Some(&Value::Array(ref ary)) => {
for (i, element) in ary.iter().enumerate() {
let mailtype = match read_str_from_toml(element, "type", false)? {
None => {
error!("Error: 'email.[{}].type' missing", i);
if ask_continue(input, output)? {
return Ok(None)
} else {
return Err(format_err!("Error: 'email.[{}].type' missing", i))
}
},
Some(p) => p,
};
let mail = match read_str_from_toml(element, "addr", false)? {
None => {
error!("Error: 'email.[{}].addr' missing", i);
if ask_continue(input, output)? {
return Ok(None)
} else {
return Err(format_err!("Error: 'email.[{}].addr' missing", i))
}
},
Some(p) => p,
};
trace!("mailtype = {:?} (UNUSED)", mailtype);
trace!("mail = {:?}", mail);
vcard = vcard.with_email(mail);
}
},
Some(_) => {
error!("Type Error: Expected Array at 'email'");
if ask_continue(input, output)? {
return Ok(None)
} else {
return Err(format_err!("Type Error: Expected Array at 'email'"))
}
},
None => {
},
}
}
{ debug!("Parsing others");
if let Some(categories) = read_strary_from_toml(&toml, "other.categories")? {
vcard = vcard.with_categories(categories);
} else {
debug!("No categories");
}
if let Some(webpage) = read_str_from_toml(&toml, "other.webpage", false)? {
vcard = vcard.with_url(webpage);
} else {
debug!("No webpage");
}
if let Some(note) = read_str_from_toml(&toml, "other.note", false)? {
vcard = vcard.with_note(note);
} else {
debug!("No note");
}
}
let vcard = vcard
.build()
.unwrap(); Ok(Some(vcard))
}
fn read_strary_from_toml(toml: &Value, path: &'static str) -> Result<Option<Vec<String>>> {
match toml.read(path).map_err(Error::from).map_warn_err_str(&format!("Failed to read value at '{}'", path)) {
Ok(Some(&Value::Array(ref vec))) => {
let mut v = Vec::new();
for elem in vec {
match *elem {
Value::String(ref s) => v.push(s.clone()),
_ => {
return Err(format_err!("Type Error: '{}' must be Array<String>", path))
},
}
}
Ok(Some(v))
}
Ok(Some(&Value::String(ref s))) => {
warn!("Having String, wanting Array<String> ... going to auto-fix");
Ok(Some(vec![s.clone()]))
},
Ok(Some(_)) => Err(format_err!("Type Error: '{}' must be Array<String>", path)),
Ok(None) => Ok(None),
Err(_) => Ok(None),
}
}
fn read_str_from_toml(toml: &Value, path: &'static str, must_be_there: bool) -> Result<Option<String>> {
match toml.read(path)? {
Some(&Value::String(ref s)) => Ok(Some(s.clone())),
Some(_) => {
Err(format_err!("Type Error: '{}' must be String", path))
},
None => {
if must_be_there {
return Err(format_err!("Expected '{}' to be present, but is not.", path))
}
Ok(None)
},
}
}
#[cfg(test)]
mod test_parsing {
use super::parse_toml_into_vcard;
use std::io::empty;
const TEMPLATE : &str = include_str!("../static/new-contact-template-test.toml");
#[test]
fn test_template_names() {
let uid = String::from("uid");
let mut output = Vec::new();
let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
assert!(vcard.name().is_some());
assert_eq!(vcard.uid().unwrap().raw(), "uid");
assert_eq!(vcard.name().unwrap().surname().unwrap(), "test");
assert_eq!(vcard.name().unwrap().given_name().unwrap(), "test");
assert_eq!(vcard.name().unwrap().additional_names().unwrap(), "test");
assert_eq!(vcard.name().unwrap().honorific_prefixes().unwrap(), "test");
assert_eq!(vcard.name().unwrap().honorific_suffixes().unwrap(), "test");
}
#[test]
fn test_template_person() {
let uid = String::from("uid");
let mut output = Vec::new();
let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
assert!(vcard.bday().is_some());
assert_eq!(vcard.bday().unwrap().raw(), "2017-01-01");
assert_eq!(vcard.nickname().len(), 1);
assert_eq!(vcard.nickname()[0].raw(), "boss");
}
#[test]
fn test_template_organization() {
let uid = String::from("uid");
let mut output = Vec::new();
let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
assert_eq!(vcard.org().len(), 1);
assert_eq!(vcard.org()[0].raw(), "test");
assert_eq!(vcard.title().len(), 1);
assert_eq!(vcard.title()[0].raw(), "test");
assert_eq!(vcard.role().len(), 1);
assert_eq!(vcard.role()[0].raw(), "test");
}
#[test]
fn test_template_phone() {
let uid = String::from("uid");
let mut output = Vec::new();
let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
assert_eq!(vcard.tel().len(), 1);
assert_eq!(vcard.tel()[0].raw(), "0123 123456789");
}
#[test]
fn test_template_email() {
let uid = String::from("uid");
let mut output = Vec::new();
let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
assert_eq!(vcard.email().len(), 1);
assert_eq!(vcard.email()[0].raw(), "examle@examplemail.org");
}
#[test]
fn test_template_addresses() {
let uid = String::from("uid");
let mut output = Vec::new();
let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
assert_eq!(vcard.adr().len(), 1);
assert_eq!(vcard.adr()[0].raw(), "testbox;testextended;teststreet;testcode;testcity;testregion;testcountry");
}
#[test]
fn test_template_other() {
let uid = String::from("uid");
let mut output = Vec::new();
let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
assert_eq!(vcard.categories().len(), 1);
assert_eq!(vcard.categories()[0].raw(), "test");
assert_eq!(vcard.url().len(), 1);
assert_eq!(vcard.url()[0].raw(), "test");
assert_eq!(vcard.note().len(), 1);
assert_eq!(vcard.note()[0].raw(), "test");
}
}