tiempo-rs/src/formatters/custom.rs

172 lines
5.0 KiB
Rust

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<PathBuf> {
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(config_for_formatter) = config.formatters.extra.get(formatter) {
config_for_formatter.to_string()
} else {
String::from("{}")
}
}
pub fn print_formatted<O, E>(formatter: &str, entries: Vec<Entry>, 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 pretty_assertions::assert_str_eq;
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 <path>..");
}
#[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_str_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 it is mispelled?");
}
}