use std::path::PathBuf; use std::process::{Command, Stdio}; use std::io::{Read, Write}; use csv::Writer; use chrono::SecondsFormat; use crate::error::{Result, Error::*}; use crate::models::Entry; use crate::commands::Facts; use crate::config::Config; /// tries to find a formatter with the given name in one of the possible paths fn formatter_path(formatters: &[PathBuf], name: &str) -> Option { formatters.iter().find_map(|f| { let mut path = f.clone(); path.push(name); if path.exists() { Some(path) } else { None } }) } fn is_valid_formatter_name(formatter: &str) -> bool { formatter.chars().filter(|&c| { !(c.is_alphanumeric() || c == '.' || c == '-' || c == '_') }).count() == 0 } fn get_formatter_config(config: &Config, formatter: &str) -> String { if let Some(formatters) = config.extra.get("formatters") { if let Some(config_for_formatter) = formatters.get(formatter) { config_for_formatter.to_string() } else { String::from("{}") } } else { String::from("{}") } } pub fn print_formatted(formatter: &str, entries: Vec, out: &mut O, err: &mut E, facts: &Facts) -> Result<()> where O: Write, E: Write, { if !is_valid_formatter_name(formatter) { return Err(InvalidCustomFormatter(formatter.into())); } if facts.config.formatter_search_paths.is_empty() { return Err(NoFormatterSearchPaths(formatter.into())); } let path = formatter_path(&facts.config.formatter_search_paths, formatter).ok_or_else(|| FormatterNotFound { name: formatter.into(), paths: facts.config.formatter_search_paths.clone(), config_at: facts.config.path.clone().unwrap_or_else(|| "in memory".into()), })?; let config = get_formatter_config(&facts.config, formatter); let mut command = Command::new(&path); command .arg(config) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); let mut child = command.spawn().map_err(|e| CustomFormatterFailed(path, e))?; let stdin = child.stdin.take().expect("Failed to take stdin"); let mut stdout = child.stdout.take().expect("Failed to take stdout"); let mut stderr = child.stderr.take().expect("Failed to take stdout"); { let mut wtr = Writer::from_writer(stdin); for entry in entries { // write to process' stdin wtr.write_record(&[ entry.id.to_string(), entry.start.to_rfc3339_opts(SecondsFormat::Micros, true), entry.end.map(|t| t.to_rfc3339_opts(SecondsFormat::Micros, true)).unwrap_or_else(|| "".into()), entry.note.unwrap_or_else(|| "".into()), entry.sheet, ])?; } } // All the input has been fed to the child process, now we wait for it to // finish and then read its output child.wait()?; let mut captured_out = String::new(); let mut captured_err = String::new(); // read process' stdout and stderr stdout.read_to_string(&mut captured_out)?; write!(out, "{}", &captured_out)?; captured_out.clear(); stderr.read_to_string(&mut captured_err)?; write!(err, "{}", &captured_err)?; captured_err.clear(); Ok(()) } #[cfg(test)] mod tests { use crate::config::Config; use super::*; #[test] fn errors_if_invalid_formatter_name() { let mut out = Vec::new(); let mut err = Vec::new(); let facts = Facts::new(); let err = print_formatted("pol/lo", Vec::new(), &mut out, &mut err, &facts).unwrap_err(); assert_eq!(err.to_string(), "\ The specified name for a custom formatter \"pol/lo\" is not valid. Only ascii letters, numbers, dots, dashes and underscores are allowed."); } #[test] fn errors_if_no_formatter_search_paths() { let mut out = Vec::new(); let mut err = Vec::new(); let facts = Facts::new(); let err = print_formatted("pollo", Vec::new(), &mut out, &mut err, &facts).unwrap_err(); assert_eq!(err.to_string(), "\ You have specified a custom formatter \"pollo\" but your config file doesn't say where to look for it. You can set a path using t config --formatter-search-paths .."); } #[test] fn nice_error_if_no_such_formatter() { let mut out = Vec::new(); let mut err = Vec::new(); let facts = Facts::new().with_config(Config { formatter_search_paths: vec![ PathBuf::from("/not/a/path"), PathBuf::from("/tmp/foo/var"), ], path: Some(PathBuf::from("/etc/tiempo/config.toml")), ..Default::default() }); let err = print_formatted("pollo", Vec::new(), &mut out, &mut err, &facts).unwrap_err(); assert_eq!(err.to_string(), "\ Could not find a formatter with name 'pollo' in any of the following paths: - /not/a/path - /tmp/foo/var which where taken from your config file located at /etc/tiempo/config.toml Perhaps is mispelled?"); } }