most of the infrastructure for custom formatters

This commit is contained in:
Abraham Toriz 2021-09-06 20:08:53 -05:00
parent 407162536d
commit d3128a93bf
No known key found for this signature in database
GPG Key ID: D5B4A746DB5DD42A
4 changed files with 213 additions and 6 deletions

View File

@ -74,6 +74,7 @@ where
format.print_formatted(
entries,
&mut streams.out,
&mut streams.err,
facts,
ids,
)?;

View File

@ -4,6 +4,15 @@ use std::io;
use thiserror::Error;
use chrono::NaiveDateTime;
use itertools::Itertools;
fn format_paths(paths: &[PathBuf]) -> String {
paths
.iter()
.map(|p| format!(" - {}", p.display()))
.collect_vec()
.join("\n")
}
#[derive(Debug, Error)]
pub enum Error {
@ -11,9 +20,9 @@ pub enum Error {
UnimplementedCommand(String),
/// Sometimes a specific variant for an error is not necessary if the error
/// can only happen in one place in the code. This is that the generic error
/// can only happen in one place in the code. This is what the generic error
/// is for and nothing else.
#[error("Error: {0}")]
#[error("{0}")]
GenericFailure(String),
#[error("Sqlite error: {0}")]
@ -180,6 +189,42 @@ Is there a permissions issue? Here's the underlaying error:
#[error("Trying to run system command sqlite3 failed")]
Sqlite3CommandFailedUnkown,
#[error("The specified name for a custom formatter \"{0}\" is not valid. Only ascii
letters, numbers, dots, dashes and underscores are allowed.")]
InvalidCustomFormatter(String),
#[error("You have specified a custom formatter \"{0}\" but your config file doesn't say
where to look for it.
You can set a path using
t config --formatter-search-paths <path>..")]
NoFormatterSearchPaths(String),
#[error("Could not find a formatter with name '{name}' in any of the following paths:
{0}
which where taken from your config file located at
{config_at}
Perhaps is mispelled?", format_paths(.paths))]
FormatterNotFound {
name: String,
paths: Vec<PathBuf>,
config_at: PathBuf,
},
#[error("The custom formatter located at:
{0}
failed with error:
{1}")]
CustomFormatterFailed(PathBuf, std::io::Error),
}
pub type Result<T> = result::Result<T, Error>;

View File

@ -12,6 +12,7 @@ pub mod csv;
pub mod json;
pub mod ids;
pub mod ical;
pub mod custom;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
@ -37,16 +38,18 @@ impl Formatter {
/// to the local timezone is given in `offset` to prevent this function from
/// using a secondary effect to retrieve the time and conver dates. This
/// also makes it easier to test.
pub fn print_formatted<W: Write>(&self, entries: Vec<Entry>, out: &mut W, facts: &Facts, ids: bool) -> error::Result<()> {
pub fn print_formatted<O, E>(&self, entries: Vec<Entry>, out: &mut O, err: &mut E, facts: &Facts, ids: bool) -> error::Result<()>
where
O: Write,
E: Write,
{
match &self {
Formatter::Text => text::print_formatted(entries, out, facts, ids)?,
Formatter::Csv => csv::print_formatted(entries, out, ids)?,
Formatter::Json => json::print_formatted(entries, out)?,
Formatter::Ids => ids::print_formatted(entries, out)?,
Formatter::Ical => ical::print_formatted(entries, out, facts.now)?,
Formatter::Custom(name) => {
panic!("attempted custom formatter with name {} which is not implemented", name);
}
Formatter::Custom(name) => custom::print_formatted(name, entries, out, err, facts)?,
}
Ok(())

158
src/formatters/custom.rs Normal file
View File

@ -0,0 +1,158 @@
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::io::Read;
use csv::Writer;
use chrono::SecondsFormat;
use crate::error::{Result, Error::*};
use crate::models::Entry;
use crate::commands::Facts;
/// 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
}
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 = "{}";
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);
let mut captured_out = Vec::new();
let mut captured_err = Vec::new();
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,
])?;
// read process' stdout and stderr
stdout.read_to_end(&mut captured_out)?;
write!(out, "{}", String::from_utf8_lossy(&captured_out))?;
captured_out.clear();
stderr.read_to_end(&mut captured_err)?;
write!(err, "{}", String::from_utf8_lossy(&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 <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_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?");
}
}