most of the infrastructure for custom formatters
This commit is contained in:
parent
407162536d
commit
d3128a93bf
|
@ -74,6 +74,7 @@ where
|
|||
format.print_formatted(
|
||||
entries,
|
||||
&mut streams.out,
|
||||
&mut streams.err,
|
||||
facts,
|
||||
ids,
|
||||
)?;
|
||||
|
|
49
src/error.rs
49
src/error.rs
|
@ -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>;
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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?");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue