mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
Parse reports from config, with defaults
This commit is contained in:
parent
2928bab41c
commit
46c3b31208
6 changed files with 624 additions and 92 deletions
|
@ -1,6 +1,7 @@
|
||||||
use super::args::{arg_matching, id_list, minus_tag, plus_tag, status_colon, TaskId};
|
use super::args::{arg_matching, id_list, minus_tag, plus_tag, status_colon, TaskId};
|
||||||
use super::ArgList;
|
use super::ArgList;
|
||||||
use crate::usage;
|
use crate::usage;
|
||||||
|
use failure::{bail, Fallible};
|
||||||
use nom::{branch::alt, combinator::*, multi::fold_many0, IResult};
|
use nom::{branch::alt, combinator::*, multi::fold_many0, IResult};
|
||||||
use taskchampion::Status;
|
use taskchampion::Status;
|
||||||
|
|
||||||
|
@ -32,15 +33,61 @@ pub(crate) enum Condition {
|
||||||
IdList(Vec<TaskId>),
|
IdList(Vec<TaskId>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Condition {
|
||||||
|
fn parse(input: ArgList) -> IResult<ArgList, Condition> {
|
||||||
|
alt((
|
||||||
|
Self::parse_id_list,
|
||||||
|
Self::parse_plus_tag,
|
||||||
|
Self::parse_minus_tag,
|
||||||
|
Self::parse_status,
|
||||||
|
))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a single condition string
|
||||||
|
pub(crate) fn parse_str(input: &str) -> Fallible<Condition> {
|
||||||
|
let input = &[input];
|
||||||
|
Ok(match Condition::parse(input) {
|
||||||
|
Ok((&[], cond)) => cond,
|
||||||
|
Ok(_) => unreachable!(), // input only has one element
|
||||||
|
Err(nom::Err::Incomplete(_)) => unreachable!(),
|
||||||
|
Err(nom::Err::Error(e)) => bail!("invalid filter condition: {:?}", e),
|
||||||
|
Err(nom::Err::Failure(e)) => bail!("invalid filter condition: {:?}", e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_id_list(input: ArgList) -> IResult<ArgList, Condition> {
|
||||||
|
fn to_condition(input: Vec<TaskId>) -> Result<Condition, ()> {
|
||||||
|
Ok(Condition::IdList(input))
|
||||||
|
}
|
||||||
|
map_res(arg_matching(id_list), to_condition)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_plus_tag(input: ArgList) -> IResult<ArgList, Condition> {
|
||||||
|
fn to_condition(input: &str) -> Result<Condition, ()> {
|
||||||
|
Ok(Condition::HasTag(input.to_owned()))
|
||||||
|
}
|
||||||
|
map_res(arg_matching(plus_tag), to_condition)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_minus_tag(input: ArgList) -> IResult<ArgList, Condition> {
|
||||||
|
fn to_condition(input: &str) -> Result<Condition, ()> {
|
||||||
|
Ok(Condition::NoTag(input.to_owned()))
|
||||||
|
}
|
||||||
|
map_res(arg_matching(minus_tag), to_condition)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_status(input: ArgList) -> IResult<ArgList, Condition> {
|
||||||
|
fn to_condition(input: Status) -> Result<Condition, ()> {
|
||||||
|
Ok(Condition::Status(input))
|
||||||
|
}
|
||||||
|
map_res(arg_matching(status_colon), to_condition)(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Filter {
|
impl Filter {
|
||||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Filter> {
|
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Filter> {
|
||||||
fold_many0(
|
fold_many0(
|
||||||
alt((
|
Condition::parse,
|
||||||
Self::parse_id_list,
|
|
||||||
Self::parse_plus_tag,
|
|
||||||
Self::parse_minus_tag,
|
|
||||||
Self::parse_status,
|
|
||||||
)),
|
|
||||||
Filter {
|
Filter {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -80,36 +127,6 @@ impl Filter {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsers
|
|
||||||
|
|
||||||
fn parse_id_list(input: ArgList) -> IResult<ArgList, Condition> {
|
|
||||||
fn to_condition(input: Vec<TaskId>) -> Result<Condition, ()> {
|
|
||||||
Ok(Condition::IdList(input))
|
|
||||||
}
|
|
||||||
map_res(arg_matching(id_list), to_condition)(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_plus_tag(input: ArgList) -> IResult<ArgList, Condition> {
|
|
||||||
fn to_condition(input: &str) -> Result<Condition, ()> {
|
|
||||||
Ok(Condition::HasTag(input.to_owned()))
|
|
||||||
}
|
|
||||||
map_res(arg_matching(plus_tag), to_condition)(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_minus_tag(input: ArgList) -> IResult<ArgList, Condition> {
|
|
||||||
fn to_condition(input: &str) -> Result<Condition, ()> {
|
|
||||||
Ok(Condition::NoTag(input.to_owned()))
|
|
||||||
}
|
|
||||||
map_res(arg_matching(minus_tag), to_condition)(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_status(input: ArgList) -> IResult<ArgList, Condition> {
|
|
||||||
fn to_condition(input: Status) -> Result<Condition, ()> {
|
|
||||||
Ok(Condition::Status(input))
|
|
||||||
}
|
|
||||||
map_res(arg_matching(status_colon), to_condition)(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
// usage
|
// usage
|
||||||
|
|
||||||
pub(super) fn get_usage(u: &mut usage::Usage) {
|
pub(super) fn get_usage(u: &mut usage::Usage) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::argparse::Filter;
|
use crate::argparse::Filter;
|
||||||
use crate::invocation::display_report;
|
use crate::invocation::display_report;
|
||||||
|
use config::Config;
|
||||||
use failure::Fallible;
|
use failure::Fallible;
|
||||||
use taskchampion::Replica;
|
use taskchampion::Replica;
|
||||||
use termcolor::WriteColor;
|
use termcolor::WriteColor;
|
||||||
|
@ -7,10 +8,11 @@ use termcolor::WriteColor;
|
||||||
pub(crate) fn execute<W: WriteColor>(
|
pub(crate) fn execute<W: WriteColor>(
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
replica: &mut Replica,
|
replica: &mut Replica,
|
||||||
|
settings: &Config,
|
||||||
report_name: String,
|
report_name: String,
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
) -> Fallible<()> {
|
) -> Fallible<()> {
|
||||||
display_report(w, replica, report_name, filter)
|
display_report(w, replica, settings, report_name, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -29,11 +31,13 @@ mod test {
|
||||||
// The function being tested is only one line long, so this is sort of an integration test
|
// The function being tested is only one line long, so this is sort of an integration test
|
||||||
// for display_report.
|
// for display_report.
|
||||||
|
|
||||||
|
let settings = crate::settings::default_settings().unwrap();
|
||||||
let report_name = "next".to_owned();
|
let report_name = "next".to_owned();
|
||||||
let filter = Filter {
|
let filter = Filter {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
execute(&mut w, &mut replica, report_name, filter).unwrap();
|
|
||||||
|
execute(&mut w, &mut replica, &settings, report_name, filter).unwrap();
|
||||||
assert!(w.into_string().contains("my task"));
|
assert!(w.into_string().contains("my task"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ pub(crate) fn invoke(command: Command, settings: Config) -> Fallible<()> {
|
||||||
filter,
|
filter,
|
||||||
},
|
},
|
||||||
..
|
..
|
||||||
} => return cmd::report::execute(&mut w, &mut replica, report_name, filter),
|
} => return cmd::report::execute(&mut w, &mut replica, &settings, report_name, filter),
|
||||||
|
|
||||||
Command {
|
Command {
|
||||||
subcommand: Subcommand::Info { filter, debug },
|
subcommand: Subcommand::Info { filter, debug },
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use crate::argparse::{Condition, Filter};
|
use crate::argparse::Filter;
|
||||||
use crate::invocation::filtered_tasks;
|
use crate::invocation::filtered_tasks;
|
||||||
use crate::report::{Column, Property, Report, Sort, SortBy};
|
use crate::report::{Column, Property, Report, SortBy};
|
||||||
use crate::table;
|
use crate::table;
|
||||||
use failure::{bail, Fallible};
|
use config::Config;
|
||||||
|
use failure::{format_err, Fallible};
|
||||||
use prettytable::{Row, Table};
|
use prettytable::{Row, Table};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use taskchampion::{Replica, Status, Task, Uuid};
|
use taskchampion::{Replica, Task, Uuid};
|
||||||
use termcolor::WriteColor;
|
use termcolor::WriteColor;
|
||||||
|
|
||||||
// pending #123, this is a non-fallible way of looking up a task's working set index
|
// pending #123, this is a non-fallible way of looking up a task's working set index
|
||||||
|
@ -102,61 +103,23 @@ fn task_column(task: &Task, column: &Column, working_set: &WorkingSet) -> String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_report(report_name: String, filter: Filter) -> Fallible<Report> {
|
|
||||||
let columns = vec![
|
|
||||||
Column {
|
|
||||||
label: "Id".to_owned(),
|
|
||||||
property: Property::Id,
|
|
||||||
},
|
|
||||||
Column {
|
|
||||||
label: "Description".to_owned(),
|
|
||||||
property: Property::Description,
|
|
||||||
},
|
|
||||||
Column {
|
|
||||||
label: "Active".to_owned(),
|
|
||||||
property: Property::Active,
|
|
||||||
},
|
|
||||||
Column {
|
|
||||||
label: "Tags".to_owned(),
|
|
||||||
property: Property::Tags,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
let sort = vec![Sort {
|
|
||||||
ascending: false,
|
|
||||||
sort_by: SortBy::Uuid,
|
|
||||||
}];
|
|
||||||
let mut report = match report_name.as_ref() {
|
|
||||||
"list" => Report {
|
|
||||||
columns,
|
|
||||||
sort,
|
|
||||||
filter: Default::default(),
|
|
||||||
},
|
|
||||||
"next" => Report {
|
|
||||||
columns,
|
|
||||||
sort,
|
|
||||||
filter: Filter {
|
|
||||||
conditions: vec![Condition::Status(Status::Pending)],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_ => bail!("Unknown report {:?}", report_name),
|
|
||||||
};
|
|
||||||
|
|
||||||
// intersect the report's filter with the user-supplied filter
|
|
||||||
report.filter = report.filter.intersect(filter);
|
|
||||||
|
|
||||||
Ok(report)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn display_report<W: WriteColor>(
|
pub(super) fn display_report<W: WriteColor>(
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
replica: &mut Replica,
|
replica: &mut Replica,
|
||||||
|
settings: &Config,
|
||||||
report_name: String,
|
report_name: String,
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
) -> Fallible<()> {
|
) -> Fallible<()> {
|
||||||
let mut t = Table::new();
|
let mut t = Table::new();
|
||||||
let report = get_report(report_name, filter)?;
|
|
||||||
let working_set = WorkingSet::new(replica)?;
|
let working_set = WorkingSet::new(replica)?;
|
||||||
|
|
||||||
|
// Get the report from settings
|
||||||
|
let mut report = Report::from_config(settings.get(&format!("reports.{}", report_name))?)
|
||||||
|
.map_err(|e| format_err!("report.{}{}", report_name, e))?;
|
||||||
|
|
||||||
|
// include any user-supplied filter conditions
|
||||||
|
report.filter = report.filter.intersect(filter);
|
||||||
|
|
||||||
// Get the tasks from the filter
|
// Get the tasks from the filter
|
||||||
let mut tasks: Vec<_> = filtered_tasks(replica, &report.filter)?.collect();
|
let mut tasks: Vec<_> = filtered_tasks(replica, &report.filter)?.collect();
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
//! This module contains the data structures used to define reports.
|
//! This module contains the data structures used to define reports.
|
||||||
|
|
||||||
use crate::argparse::Filter;
|
use crate::argparse::{Condition, Filter};
|
||||||
|
use failure::{bail, format_err, Fallible};
|
||||||
|
|
||||||
/// A report specifies a filter as well as a sort order and information about which
|
/// A report specifies a filter as well as a sort order and information about which
|
||||||
/// task attributes to display
|
/// task attributes to display
|
||||||
|
@ -68,3 +69,510 @@ pub(crate) enum SortBy {
|
||||||
/// The task's description
|
/// The task's description
|
||||||
Description,
|
Description,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Conversions from config::Value. Note that these cannot ergonomically use TryFrom/TryInto; see
|
||||||
|
// https://github.com/mehcode/config-rs/issues/162
|
||||||
|
|
||||||
|
impl Report {
|
||||||
|
/// Create a Report from a config value. This should be the `report.<report_name>` value.
|
||||||
|
/// The error message begins with any additional path information, e.g., `.sort[1].sort_by:
|
||||||
|
/// ..`.
|
||||||
|
pub(crate) fn from_config(cfg: config::Value) -> Fallible<Report> {
|
||||||
|
let mut map = cfg.into_table().map_err(|e| format_err!(": {}", e))?;
|
||||||
|
let sort = if let Some(sort_array) = map.remove("sort") {
|
||||||
|
sort_array
|
||||||
|
.into_array()
|
||||||
|
.map_err(|e| format_err!(".sort: {}", e))?
|
||||||
|
.drain(..)
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, v)| Sort::from_config(v).map_err(|e| format_err!(".sort[{}]{}", i, e)))
|
||||||
|
.collect::<Fallible<Vec<_>>>()?
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
let columns = map
|
||||||
|
.remove("columns")
|
||||||
|
.ok_or_else(|| format_err!(": 'columns' property is required"))?
|
||||||
|
.into_array()
|
||||||
|
.map_err(|e| format_err!(".columns: {}", e))?
|
||||||
|
.drain(..)
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, v)| Column::from_config(v).map_err(|e| format_err!(".columns[{}]{}", i, e)))
|
||||||
|
.collect::<Fallible<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let conditions = if let Some(conditions) = map.remove("filter") {
|
||||||
|
conditions
|
||||||
|
.into_array()
|
||||||
|
.map_err(|e| format_err!(".filter: {}", e))?
|
||||||
|
.drain(..)
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, v)| {
|
||||||
|
v.into_str()
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
.and_then(|s| Condition::parse_str(&s))
|
||||||
|
.map_err(|e| format_err!(".filter[{}]: {}", i, e))
|
||||||
|
})
|
||||||
|
.collect::<Fallible<Vec<_>>>()?
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
let filter = Filter { conditions };
|
||||||
|
|
||||||
|
if !map.is_empty() {
|
||||||
|
bail!(": unknown properties");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Report {
|
||||||
|
columns,
|
||||||
|
sort,
|
||||||
|
filter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Column {
|
||||||
|
pub(crate) fn from_config(cfg: config::Value) -> Fallible<Column> {
|
||||||
|
let mut map = cfg.into_table().map_err(|e| format_err!(": {}", e))?;
|
||||||
|
let label = map
|
||||||
|
.remove("label")
|
||||||
|
.ok_or_else(|| format_err!(": 'label' property is required"))?
|
||||||
|
.into_str()
|
||||||
|
.map_err(|e| format_err!(".label: {}", e))?;
|
||||||
|
let property: config::Value = map
|
||||||
|
.remove("property")
|
||||||
|
.ok_or_else(|| format_err!(": 'property' property is required"))?;
|
||||||
|
let property =
|
||||||
|
Property::from_config(property).map_err(|e| format_err!(".property{}", e))?;
|
||||||
|
|
||||||
|
if !map.is_empty() {
|
||||||
|
bail!(": unknown properties");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Column { label, property })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Property {
|
||||||
|
pub(crate) fn from_config(cfg: config::Value) -> Fallible<Property> {
|
||||||
|
let s = cfg.into_str().map_err(|e| format_err!(": {}", e))?;
|
||||||
|
Ok(match s.as_ref() {
|
||||||
|
"id" => Property::Id,
|
||||||
|
"uuid" => Property::Uuid,
|
||||||
|
"active" => Property::Active,
|
||||||
|
"description" => Property::Description,
|
||||||
|
"tags" => Property::Tags,
|
||||||
|
_ => bail!(": unknown property {}", s),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sort {
|
||||||
|
pub(crate) fn from_config(cfg: config::Value) -> Fallible<Sort> {
|
||||||
|
let mut map = cfg.into_table().map_err(|e| format_err!(": {}", e))?;
|
||||||
|
let ascending = match map.remove("ascending") {
|
||||||
|
Some(v) => v
|
||||||
|
.into_bool()
|
||||||
|
.map_err(|e| format_err!(".ascending: {}", e))?,
|
||||||
|
None => true, // default
|
||||||
|
};
|
||||||
|
let sort_by: config::Value = map
|
||||||
|
.remove("sort_by")
|
||||||
|
.ok_or_else(|| format_err!(": 'sort_by' property is required"))?;
|
||||||
|
let sort_by = SortBy::from_config(sort_by).map_err(|e| format_err!(".sort_by{}", e))?;
|
||||||
|
|
||||||
|
if !map.is_empty() {
|
||||||
|
bail!(": unknown properties");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Sort { ascending, sort_by })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SortBy {
|
||||||
|
pub(crate) fn from_config(cfg: config::Value) -> Fallible<SortBy> {
|
||||||
|
let s = cfg.into_str().map_err(|e| format_err!(": {}", e))?;
|
||||||
|
Ok(match s.as_ref() {
|
||||||
|
"id" => SortBy::Id,
|
||||||
|
"uuid" => SortBy::Uuid,
|
||||||
|
"description" => SortBy::Description,
|
||||||
|
_ => bail!(": unknown sort_by {}", s),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use config::{Config, File, FileFormat, FileSourceString};
|
||||||
|
use taskchampion::Status;
|
||||||
|
use textwrap::{dedent, indent};
|
||||||
|
|
||||||
|
fn config_from(cfg: &str) -> config::Value {
|
||||||
|
// wrap this in a "table" so that we can get any type of value at the top level.
|
||||||
|
let yaml = format!("val:\n{}", indent(&dedent(&cfg), " "));
|
||||||
|
let mut settings = Config::new();
|
||||||
|
let cfg_file: File<FileSourceString> = File::from_str(&yaml, FileFormat::Yaml);
|
||||||
|
settings.merge(cfg_file).unwrap();
|
||||||
|
settings.cache.into_table().unwrap().remove("val").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_report_ok() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
filter: []
|
||||||
|
sort: []
|
||||||
|
columns: []
|
||||||
|
filter:
|
||||||
|
- status:pending",
|
||||||
|
);
|
||||||
|
let report = Report::from_config(val).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
report.filter,
|
||||||
|
Filter {
|
||||||
|
conditions: vec![Condition::Status(Status::Pending),],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(report.columns, vec![]);
|
||||||
|
assert_eq!(report.sort, vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_report_no_sort() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
filter: []
|
||||||
|
columns: []",
|
||||||
|
);
|
||||||
|
let report = Report::from_config(val).unwrap();
|
||||||
|
assert_eq!(report.sort, vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_report_sort_not_array() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
filter: []
|
||||||
|
sort: true
|
||||||
|
columns: []",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Report::from_config(val).unwrap_err().to_string(),
|
||||||
|
".sort: invalid type: boolean `true`, expected an array"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_report_sort_error() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
filter: []
|
||||||
|
sort:
|
||||||
|
- sort_by: id
|
||||||
|
- true
|
||||||
|
columns: []",
|
||||||
|
);
|
||||||
|
assert!(&Report::from_config(val)
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.starts_with(".sort[1]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_report_unknown_prop() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
columns: []
|
||||||
|
filter: []
|
||||||
|
sort: []
|
||||||
|
nosuch: true
|
||||||
|
",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Report::from_config(val).unwrap_err().to_string(),
|
||||||
|
": unknown properties"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_report_no_columns() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
filter: []
|
||||||
|
sort: []",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Report::from_config(val).unwrap_err().to_string(),
|
||||||
|
": \'columns\' property is required"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_report_columns_not_array() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
filter: []
|
||||||
|
sort: []
|
||||||
|
columns: true",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Report::from_config(val).unwrap_err().to_string(),
|
||||||
|
".columns: invalid type: boolean `true`, expected an array"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_report_column_error() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
filter: []
|
||||||
|
sort: []
|
||||||
|
columns:
|
||||||
|
- label: ID
|
||||||
|
property: id
|
||||||
|
- true",
|
||||||
|
);
|
||||||
|
assert!(&Report::from_config(val)
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.starts_with(".columns[1]:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_report_filter_not_array() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
filter: []
|
||||||
|
sort: []
|
||||||
|
columns: []
|
||||||
|
filter: true",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Report::from_config(val).unwrap_err().to_string(),
|
||||||
|
".filter: invalid type: boolean `true`, expected an array"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_report_filter_error() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
filter: []
|
||||||
|
sort: []
|
||||||
|
columns: []
|
||||||
|
filter:
|
||||||
|
- nosuchfilter",
|
||||||
|
);
|
||||||
|
assert!(&Report::from_config(val)
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.starts_with(".filter[0]: invalid filter condition:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_column() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
label: ID
|
||||||
|
property: id",
|
||||||
|
);
|
||||||
|
let column = Column::from_config(val).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
column,
|
||||||
|
Column {
|
||||||
|
label: "ID".to_owned(),
|
||||||
|
property: Property::Id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_column_unknown_prop() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
label: ID
|
||||||
|
property: id
|
||||||
|
nosuch: foo",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Column::from_config(val).unwrap_err().to_string(),
|
||||||
|
": unknown properties"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_column_no_label() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
property: id",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Column::from_config(val).unwrap_err().to_string(),
|
||||||
|
": 'label' property is required"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_column_invalid_label() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
label: []
|
||||||
|
property: id",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Column::from_config(val).unwrap_err().to_string(),
|
||||||
|
".label: invalid type: sequence, expected a string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_column_no_property() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
label: ID",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Column::from_config(val).unwrap_err().to_string(),
|
||||||
|
": 'property' property is required"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_column_invalid_property() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
label: ID
|
||||||
|
property: []",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Column::from_config(val).unwrap_err().to_string(),
|
||||||
|
".property: invalid type: sequence, expected a string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_property() {
|
||||||
|
let val = config_from("uuid");
|
||||||
|
let prop = Property::from_config(val).unwrap();
|
||||||
|
assert_eq!(prop, Property::Uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_property_invalid_type() {
|
||||||
|
let val = config_from("{}");
|
||||||
|
assert_eq!(
|
||||||
|
&Property::from_config(val).unwrap_err().to_string(),
|
||||||
|
": invalid type: map, expected a string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
ascending: false
|
||||||
|
sort_by: id",
|
||||||
|
);
|
||||||
|
let sort = Sort::from_config(val).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
sort,
|
||||||
|
Sort {
|
||||||
|
ascending: false,
|
||||||
|
sort_by: SortBy::Id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_no_ascending() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
sort_by: id",
|
||||||
|
);
|
||||||
|
let sort = Sort::from_config(val).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
sort,
|
||||||
|
Sort {
|
||||||
|
ascending: true,
|
||||||
|
sort_by: SortBy::Id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_unknown_prop() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
sort_by: id
|
||||||
|
nosuch: foo",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Sort::from_config(val).unwrap_err().to_string(),
|
||||||
|
": unknown properties"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_no_sort_by() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
ascending: true",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Sort::from_config(val).unwrap_err().to_string(),
|
||||||
|
": 'sort_by' property is required"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_invalid_ascending() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
sort_by: id
|
||||||
|
ascending: {}",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Sort::from_config(val).unwrap_err().to_string(),
|
||||||
|
".ascending: invalid type: map, expected a boolean"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_invalid_sort_by() {
|
||||||
|
let val = config_from(
|
||||||
|
"
|
||||||
|
sort_by: {}",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&Sort::from_config(val).unwrap_err().to_string(),
|
||||||
|
".sort_by: invalid type: map, expected a string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_by() {
|
||||||
|
let val = config_from("uuid");
|
||||||
|
let prop = SortBy::from_config(val).unwrap();
|
||||||
|
assert_eq!(prop, SortBy::Uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_by_unknown() {
|
||||||
|
let val = config_from("nosuch");
|
||||||
|
assert_eq!(
|
||||||
|
&SortBy::from_config(val).unwrap_err().to_string(),
|
||||||
|
": unknown sort_by nosuch"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_by_invalid_type() {
|
||||||
|
let val = config_from("{}");
|
||||||
|
assert_eq!(
|
||||||
|
&SortBy::from_config(val).unwrap_err().to_string(),
|
||||||
|
": invalid type: map, expected a string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,40 @@
|
||||||
use config::{Config, Environment, File, FileSourceFile};
|
use config::{Config, Environment, File, FileFormat, FileSourceFile, FileSourceString};
|
||||||
use failure::Fallible;
|
use failure::Fallible;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub(crate) fn read_settings() -> Fallible<Config> {
|
const DEFAULTS: &str = r#"
|
||||||
|
reports:
|
||||||
|
list:
|
||||||
|
sort:
|
||||||
|
- sort_by: uuid
|
||||||
|
columns:
|
||||||
|
- label: Id
|
||||||
|
property: id
|
||||||
|
- label: Description
|
||||||
|
property: description
|
||||||
|
- label: Active
|
||||||
|
property: active
|
||||||
|
- label: Tags
|
||||||
|
property: tags
|
||||||
|
next:
|
||||||
|
filter:
|
||||||
|
- "status:pending"
|
||||||
|
sort:
|
||||||
|
- sort_by: uuid
|
||||||
|
columns:
|
||||||
|
- label: Id
|
||||||
|
property: id
|
||||||
|
- label: Description
|
||||||
|
property: description
|
||||||
|
- label: Active
|
||||||
|
property: active
|
||||||
|
- label: Tags
|
||||||
|
property: tags
|
||||||
|
"#;
|
||||||
|
|
||||||
|
/// Get the default settings for this application
|
||||||
|
pub(crate) fn default_settings() -> Fallible<Config> {
|
||||||
let mut settings = Config::default();
|
let mut settings = Config::default();
|
||||||
|
|
||||||
// set up defaults
|
// set up defaults
|
||||||
|
@ -25,6 +56,15 @@ pub(crate) fn read_settings() -> Fallible<Config> {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let defaults: File<FileSourceString> = File::from_str(DEFAULTS, FileFormat::Yaml);
|
||||||
|
settings.merge(defaults)?;
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_settings() -> Fallible<Config> {
|
||||||
|
let mut settings = default_settings()?;
|
||||||
|
|
||||||
// load either from the path in TASKCHAMPION_CONFIG, or from CONFIG_DIR/taskchampion
|
// load either from the path in TASKCHAMPION_CONFIG, or from CONFIG_DIR/taskchampion
|
||||||
if let Some(config_file) = env::var_os("TASKCHAMPION_CONFIG") {
|
if let Some(config_file) = env::var_os("TASKCHAMPION_CONFIG") {
|
||||||
log::debug!("Loading configuration from {:?}", config_file);
|
log::debug!("Loading configuration from {:?}", config_file);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue