2021-09-06 20:08:53 -05:00
|
|
|
use std::path::PathBuf;
|
|
|
|
use std::process::{Command, Stdio};
|
2021-09-07 14:03:51 -05:00
|
|
|
use std::io::{Read, Write};
|
2021-09-06 20:08:53 -05:00
|
|
|
|
|
|
|
use csv::Writer;
|
|
|
|
use chrono::SecondsFormat;
|
|
|
|
|
|
|
|
use crate::error::{Result, Error::*};
|
|
|
|
use crate::models::Entry;
|
|
|
|
use crate::commands::Facts;
|
2021-09-06 20:53:18 -05:00
|
|
|
use crate::config::Config;
|
2021-09-06 20:08:53 -05:00
|
|
|
|
|
|
|
/// 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
|
|
|
|
}
|
|
|
|
|
2021-09-06 20:53:18 -05:00
|
|
|
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("{}")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-06 20:08:53 -05:00
|
|
|
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()),
|
|
|
|
})?;
|
|
|
|
|
2021-09-06 20:53:18 -05:00
|
|
|
let config = get_formatter_config(&facts.config, formatter);
|
2021-09-06 20:08:53 -05:00
|
|
|
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");
|
|
|
|
|
2021-09-07 14:03:51 -05:00
|
|
|
{
|
|
|
|
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()?;
|
|
|
|
|
2021-09-06 20:53:50 -05:00
|
|
|
let mut captured_out = String::new();
|
|
|
|
let mut captured_err = String::new();
|
2021-09-06 20:08:53 -05:00
|
|
|
|
2021-09-07 14:03:51 -05:00
|
|
|
// 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();
|
2021-09-06 20:08:53 -05:00
|
|
|
|
|
|
|
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
|
2021-09-07 14:03:51 -05:00
|
|
|
letters, numbers, dots, dashes and underscores are allowed.");
|
2021-09-06 20:08:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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
|
|
|
|
|
2021-09-07 14:03:51 -05:00
|
|
|
t config --formatter-search-paths <path>..");
|
2021-09-06 20:08:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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?");
|
|
|
|
}
|
|
|
|
}
|