Switch to TOML for configuration
This commit is contained in:
227
cli/src/settings/mod.rs
Normal file
227
cli/src/settings/mod.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
//! Support for the CLI's configuration file, including default settings.
|
||||
//!
|
||||
//! Configuration is stored in a "parsed" format, meaning that any syntax errors will be caught on
|
||||
//! startup and not just when those values are used.
|
||||
|
||||
mod report;
|
||||
mod util;
|
||||
|
||||
use crate::argparse::{Condition, Filter};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use taskchampion::Status;
|
||||
use toml::value::Table;
|
||||
use util::table_with_keys;
|
||||
|
||||
pub(crate) use report::{Column, Property, Report, Sort, SortBy};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Settings {
|
||||
// replica
|
||||
pub(crate) data_dir: PathBuf,
|
||||
|
||||
// remote sync server
|
||||
pub(crate) server_client_key: Option<String>,
|
||||
pub(crate) server_origin: Option<String>,
|
||||
pub(crate) encryption_secret: Option<String>,
|
||||
|
||||
// local sync server
|
||||
pub(crate) server_dir: PathBuf,
|
||||
|
||||
// reports
|
||||
pub(crate) reports: HashMap<String, Report>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub(crate) fn read() -> Result<Self> {
|
||||
if let Some(config_file) = env::var_os("TASKCHAMPION_CONFIG") {
|
||||
log::debug!("Loading configuration from {:?}", config_file);
|
||||
env::remove_var("TASKCHAMPION_CONFIG");
|
||||
Self::load_from_file(config_file.into(), true)
|
||||
} else if let Some(mut dir) = dirs_next::config_dir() {
|
||||
dir.push("taskchampion.toml");
|
||||
log::debug!("Loading configuration from {:?} (optional)", dir);
|
||||
Self::load_from_file(dir, false)
|
||||
} else {
|
||||
Ok(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
fn load_from_file(config_file: PathBuf, required: bool) -> Result<Self> {
|
||||
let mut settings = Self::default();
|
||||
|
||||
let config_toml = match fs::read_to_string(config_file.clone()) {
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
return if required {
|
||||
Err(e.into())
|
||||
} else {
|
||||
Ok(settings)
|
||||
};
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
Ok(s) => s,
|
||||
};
|
||||
|
||||
let config_toml = config_toml
|
||||
.parse::<toml::Value>()
|
||||
.with_context(|| format!("error while reading {:?}", config_file))?;
|
||||
settings
|
||||
.update_from_toml(&config_toml)
|
||||
.with_context(|| format!("error while parsing {:?}", config_file))?;
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
/// Update this object with configuration from the given config file. This is
|
||||
/// broken out mostly for convenience in error handling
|
||||
fn update_from_toml(&mut self, config_toml: &toml::Value) -> Result<()> {
|
||||
let table_keys = [
|
||||
"data_dir",
|
||||
"server_client_key",
|
||||
"server_origin",
|
||||
"encryption_secret",
|
||||
"server_dir",
|
||||
"reports",
|
||||
];
|
||||
let table = table_with_keys(&config_toml, &table_keys)?;
|
||||
|
||||
fn get_str_cfg<F: FnOnce(String)>(
|
||||
table: &Table,
|
||||
name: &'static str,
|
||||
setter: F,
|
||||
) -> Result<()> {
|
||||
if let Some(v) = table.get(name) {
|
||||
setter(
|
||||
v.as_str()
|
||||
.ok_or_else(|| anyhow!(".{}: not a string", name))?
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
get_str_cfg(table, "data_dir", |v| {
|
||||
self.data_dir = v.into();
|
||||
})?;
|
||||
|
||||
get_str_cfg(table, "server_client_key", |v| {
|
||||
self.server_client_key = Some(v);
|
||||
})?;
|
||||
|
||||
get_str_cfg(table, "server_origin", |v| {
|
||||
self.server_origin = Some(v);
|
||||
})?;
|
||||
|
||||
get_str_cfg(table, "encryption_secret", |v| {
|
||||
self.encryption_secret = Some(v);
|
||||
})?;
|
||||
|
||||
get_str_cfg(table, "server_dir", |v| {
|
||||
self.server_dir = v.into();
|
||||
})?;
|
||||
|
||||
if let Some(v) = table.get("reports") {
|
||||
let report_cfgs = v
|
||||
.as_table()
|
||||
.ok_or_else(|| anyhow!(".reports: not a table"))?;
|
||||
for (name, cfg) in report_cfgs {
|
||||
let report = Report::try_from(cfg).map_err(|e| anyhow!("reports.{}{}", name, e))?;
|
||||
self.reports.insert(name.clone(), report);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
let data_dir;
|
||||
let server_dir;
|
||||
|
||||
if let Some(dir) = dirs_next::data_local_dir() {
|
||||
data_dir = dir.join("taskchampion");
|
||||
server_dir = dir.join("taskchampion-sync-server");
|
||||
} else {
|
||||
// fallback
|
||||
data_dir = PathBuf::from(".");
|
||||
server_dir = PathBuf::from(".");
|
||||
}
|
||||
|
||||
// define the default reports
|
||||
let mut reports = HashMap::new();
|
||||
|
||||
reports.insert(
|
||||
"list".to_owned(),
|
||||
Report {
|
||||
sort: vec![Sort {
|
||||
ascending: true,
|
||||
sort_by: SortBy::Uuid,
|
||||
}],
|
||||
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,
|
||||
},
|
||||
],
|
||||
filter: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
reports.insert(
|
||||
"next".to_owned(),
|
||||
Report {
|
||||
sort: vec![Sort {
|
||||
ascending: true,
|
||||
sort_by: SortBy::Uuid,
|
||||
}],
|
||||
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,
|
||||
},
|
||||
],
|
||||
filter: Filter {
|
||||
conditions: vec![Condition::Status(Status::Pending)],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
data_dir,
|
||||
server_client_key: None,
|
||||
server_origin: None,
|
||||
encryption_secret: None,
|
||||
server_dir,
|
||||
reports,
|
||||
}
|
||||
}
|
||||
}
|
||||
535
cli/src/settings/report.rs
Normal file
535
cli/src/settings/report.rs
Normal file
@@ -0,0 +1,535 @@
|
||||
//! This module contains the data structures used to define reports.
|
||||
|
||||
use crate::argparse::{Condition, Filter};
|
||||
use crate::settings::util::table_with_keys;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
/// A report specifies a filter as well as a sort order and information about which
|
||||
/// task attributes to display
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub(crate) struct Report {
|
||||
/// Columns to display in this report
|
||||
pub columns: Vec<Column>,
|
||||
/// Sort order for this report
|
||||
pub sort: Vec<Sort>,
|
||||
/// Filter selecting tasks for this report
|
||||
pub filter: Filter,
|
||||
}
|
||||
|
||||
/// A column to display in a report
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Column {
|
||||
/// The label for this column
|
||||
pub label: String,
|
||||
|
||||
/// The property to display
|
||||
pub property: Property,
|
||||
}
|
||||
|
||||
/// Task property to display in a report
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum Property {
|
||||
/// The task's ID, either working-set index or Uuid if not in the working set
|
||||
Id,
|
||||
|
||||
/// The task's full UUID
|
||||
Uuid,
|
||||
|
||||
/// Whether the task is active or not
|
||||
Active,
|
||||
|
||||
/// The task's description
|
||||
Description,
|
||||
|
||||
/// The task's tags
|
||||
Tags,
|
||||
}
|
||||
|
||||
/// A sorting criterion for a sort operation.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Sort {
|
||||
/// True if the sort should be "ascending" (a -> z, 0 -> 9, etc.)
|
||||
pub ascending: bool,
|
||||
|
||||
/// The property to sort on
|
||||
pub sort_by: SortBy,
|
||||
}
|
||||
|
||||
/// Task property to sort by
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum SortBy {
|
||||
/// The task's ID, either working-set index or a UUID prefix; working
|
||||
/// set tasks sort before others.
|
||||
Id,
|
||||
|
||||
/// The task's full UUID
|
||||
Uuid,
|
||||
|
||||
/// The task's description
|
||||
Description,
|
||||
}
|
||||
|
||||
// Conversions from settings::Settings.
|
||||
|
||||
impl TryFrom<toml::Value> for Report {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(cfg: toml::Value) -> Result<Report> {
|
||||
Report::try_from(&cfg)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&toml::Value> for Report {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
/// Create a Report from a toml value. This should be the `report.<report_name>` value.
|
||||
/// The error message begins with any additional path information, e.g., `.sort[1].sort_by:
|
||||
/// ..`.
|
||||
fn try_from(cfg: &toml::Value) -> Result<Report> {
|
||||
let keys = ["sort", "columns", "filter"];
|
||||
let table = table_with_keys(cfg, &keys).map_err(|e| anyhow!(": {}", e))?;
|
||||
|
||||
let sort = match table.get("sort") {
|
||||
Some(v) => v
|
||||
.as_array()
|
||||
.ok_or_else(|| anyhow!(".sort: not an array"))?
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| v.try_into().map_err(|e| anyhow!(".sort[{}]{}", i, e)))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let columns = match table.get("columns") {
|
||||
Some(v) => v
|
||||
.as_array()
|
||||
.ok_or_else(|| anyhow!(".columns: not an array"))?
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| v.try_into().map_err(|e| anyhow!(".columns[{}]{}", i, e)))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
None => bail!(": `columns` property is required"),
|
||||
};
|
||||
|
||||
let conditions = match table.get("filter") {
|
||||
Some(v) => v
|
||||
.as_array()
|
||||
.ok_or_else(|| anyhow!(".filter: not an array"))?
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
v.as_str()
|
||||
.ok_or_else(|| anyhow!(".filter[{}]: not a string", i))
|
||||
.and_then(|s| Condition::parse_str(&s))
|
||||
.map_err(|e| anyhow!(".filter[{}]: {}", i, e))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
Ok(Report {
|
||||
columns,
|
||||
sort,
|
||||
filter: Filter { conditions },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&toml::Value> for Column {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(cfg: &toml::Value) -> Result<Column> {
|
||||
let keys = ["label", "property"];
|
||||
let table = table_with_keys(cfg, &keys).map_err(|e| anyhow!(": {}", e))?;
|
||||
|
||||
let label = match table.get("label") {
|
||||
Some(v) => v
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!(".label: not a string"))?
|
||||
.to_owned(),
|
||||
None => bail!(": `label` property is required"),
|
||||
};
|
||||
|
||||
let property = match table.get("property") {
|
||||
Some(v) => v.try_into().map_err(|e| anyhow!(".property{}", e))?,
|
||||
None => bail!(": `property` property is required"),
|
||||
};
|
||||
|
||||
Ok(Column { label, property })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&toml::Value> for Property {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(cfg: &toml::Value) -> Result<Property> {
|
||||
let s = cfg.as_str().ok_or_else(|| anyhow!(": not a string"))?;
|
||||
Ok(match s {
|
||||
"id" => Property::Id,
|
||||
"uuid" => Property::Uuid,
|
||||
"active" => Property::Active,
|
||||
"description" => Property::Description,
|
||||
"tags" => Property::Tags,
|
||||
_ => bail!(": unknown property {}", s),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&toml::Value> for Sort {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(cfg: &toml::Value) -> Result<Sort> {
|
||||
let keys = ["ascending", "sort_by"];
|
||||
let table = table_with_keys(cfg, &keys).map_err(|e| anyhow!(": {}", e))?;
|
||||
let ascending = match table.get("ascending") {
|
||||
Some(v) => v
|
||||
.as_bool()
|
||||
.ok_or_else(|| anyhow!(".ascending: not a boolean value"))?,
|
||||
None => true, // default
|
||||
};
|
||||
|
||||
let sort_by = match table.get("sort_by") {
|
||||
Some(v) => v.try_into().map_err(|e| anyhow!(".sort_by{}", e))?,
|
||||
None => bail!(": `sort_by` property is required"),
|
||||
};
|
||||
|
||||
Ok(Sort { ascending, sort_by })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&toml::Value> for SortBy {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(cfg: &toml::Value) -> Result<SortBy> {
|
||||
let s = cfg.as_str().ok_or_else(|| anyhow!(": not a string"))?;
|
||||
Ok(match s {
|
||||
"id" => SortBy::Id,
|
||||
"uuid" => SortBy::Uuid,
|
||||
"description" => SortBy::Description,
|
||||
_ => bail!(": unknown sort_by value `{}`", s),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use taskchampion::Status;
|
||||
use toml::toml;
|
||||
|
||||
#[test]
|
||||
fn test_report_ok() {
|
||||
let val = toml! {
|
||||
sort = []
|
||||
columns = []
|
||||
filter = ["status:pending"]
|
||||
};
|
||||
let report: Report = TryInto::try_into(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 = toml! {
|
||||
filter = []
|
||||
columns = []
|
||||
};
|
||||
let report = Report::try_from(val).unwrap();
|
||||
assert_eq!(report.sort, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_sort_not_array() {
|
||||
let val = toml! {
|
||||
filter = []
|
||||
sort = true
|
||||
columns = []
|
||||
};
|
||||
let err = Report::try_from(val).unwrap_err().to_string();
|
||||
assert_eq!(&err, ".sort: not an array");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_sort_error() {
|
||||
let val = toml! {
|
||||
filter = []
|
||||
sort = [ { sort_by = "id" }, true ]
|
||||
columns = []
|
||||
};
|
||||
let err = Report::try_from(val).unwrap_err().to_string();
|
||||
assert!(err.starts_with(".sort[1]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_unknown_prop() {
|
||||
let val = toml! {
|
||||
columns = []
|
||||
filter = []
|
||||
sort = []
|
||||
nosuch = true
|
||||
};
|
||||
let err = Report::try_from(val).unwrap_err().to_string();
|
||||
assert_eq!(&err, ": unknown table key `nosuch`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_no_columns() {
|
||||
let val = toml! {
|
||||
filter = []
|
||||
sort = []
|
||||
};
|
||||
let err = Report::try_from(val).unwrap_err().to_string();
|
||||
assert_eq!(&err, ": `columns` property is required");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_columns_not_array() {
|
||||
let val = toml! {
|
||||
filter = []
|
||||
sort = []
|
||||
columns = true
|
||||
};
|
||||
let err = Report::try_from(val).unwrap_err().to_string();
|
||||
assert_eq!(&err, ".columns: not an array");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_column_error() {
|
||||
let val = toml! {
|
||||
filter = []
|
||||
sort = []
|
||||
|
||||
[[columns]]
|
||||
label = "ID"
|
||||
property = "id"
|
||||
|
||||
[[columns]]
|
||||
foo = 10
|
||||
};
|
||||
let err = Report::try_from(val).unwrap_err().to_string();
|
||||
assert_eq!(&err, ".columns[1]: unknown table key `foo`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_filter_not_array() {
|
||||
let val = toml! {
|
||||
filter = "foo"
|
||||
sort = []
|
||||
columns = []
|
||||
};
|
||||
let err = Report::try_from(val).unwrap_err().to_string();
|
||||
assert_eq!(&err, ".filter: not an array");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_filter_error() {
|
||||
let val = toml! {
|
||||
sort = []
|
||||
columns = []
|
||||
filter = [ "nosuchfilter" ]
|
||||
};
|
||||
let err = Report::try_from(val).unwrap_err().to_string();
|
||||
assert!(err.starts_with(".filter[0]: invalid filter condition:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_column() {
|
||||
let val = toml! {
|
||||
label = "ID"
|
||||
property = "id"
|
||||
};
|
||||
let column = Column::try_from(&val).unwrap();
|
||||
assert_eq!(
|
||||
column,
|
||||
Column {
|
||||
label: "ID".to_owned(),
|
||||
property: Property::Id,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_column_unknown_prop() {
|
||||
let val = toml! {
|
||||
label = "ID"
|
||||
property = "id"
|
||||
nosuch = "foo"
|
||||
};
|
||||
assert_eq!(
|
||||
&Column::try_from(&val).unwrap_err().to_string(),
|
||||
": unknown table key `nosuch`"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_column_no_label() {
|
||||
let val = toml! {
|
||||
property = "id"
|
||||
};
|
||||
assert_eq!(
|
||||
&Column::try_from(&val).unwrap_err().to_string(),
|
||||
": `label` property is required"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_column_invalid_label() {
|
||||
let val = toml! {
|
||||
label = []
|
||||
property = "id"
|
||||
};
|
||||
assert_eq!(
|
||||
&Column::try_from(&val).unwrap_err().to_string(),
|
||||
".label: not a string"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_column_no_property() {
|
||||
let val = toml! {
|
||||
label = "ID"
|
||||
};
|
||||
assert_eq!(
|
||||
&Column::try_from(&val).unwrap_err().to_string(),
|
||||
": `property` property is required"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_column_invalid_property() {
|
||||
let val = toml! {
|
||||
label = "ID"
|
||||
property = []
|
||||
};
|
||||
assert_eq!(
|
||||
&Column::try_from(&val).unwrap_err().to_string(),
|
||||
".property: not a string"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_property() {
|
||||
let val = toml::Value::String("uuid".to_owned());
|
||||
let prop = Property::try_from(&val).unwrap();
|
||||
assert_eq!(prop, Property::Uuid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_property_invalid_type() {
|
||||
let val = toml::Value::Array(vec![]);
|
||||
assert_eq!(
|
||||
&Property::try_from(&val).unwrap_err().to_string(),
|
||||
": not a string"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort() {
|
||||
let val = toml! {
|
||||
ascending = false
|
||||
sort_by = "id"
|
||||
};
|
||||
let sort = Sort::try_from(&val).unwrap();
|
||||
assert_eq!(
|
||||
sort,
|
||||
Sort {
|
||||
ascending: false,
|
||||
sort_by: SortBy::Id,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_no_ascending() {
|
||||
let val = toml! {
|
||||
sort_by = "id"
|
||||
};
|
||||
let sort = Sort::try_from(&val).unwrap();
|
||||
assert_eq!(
|
||||
sort,
|
||||
Sort {
|
||||
ascending: true,
|
||||
sort_by: SortBy::Id,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_unknown_prop() {
|
||||
let val = toml! {
|
||||
sort_by = "id"
|
||||
nosuch = true
|
||||
};
|
||||
assert_eq!(
|
||||
&Sort::try_from(&val).unwrap_err().to_string(),
|
||||
": unknown table key `nosuch`"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_no_sort_by() {
|
||||
let val = toml! {
|
||||
ascending = true
|
||||
};
|
||||
assert_eq!(
|
||||
&Sort::try_from(&val).unwrap_err().to_string(),
|
||||
": `sort_by` property is required"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_invalid_ascending() {
|
||||
let val = toml! {
|
||||
sort_by = "id"
|
||||
ascending = {}
|
||||
};
|
||||
assert_eq!(
|
||||
&Sort::try_from(&val).unwrap_err().to_string(),
|
||||
".ascending: not a boolean value"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_invalid_sort_by() {
|
||||
let val = toml! {
|
||||
sort_by = {}
|
||||
};
|
||||
assert_eq!(
|
||||
&Sort::try_from(&val).unwrap_err().to_string(),
|
||||
".sort_by: not a string"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_by() {
|
||||
let val = toml::Value::String("uuid".to_string());
|
||||
let prop = SortBy::try_from(&val).unwrap();
|
||||
assert_eq!(prop, SortBy::Uuid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_by_unknown() {
|
||||
let val = toml::Value::String("nosuch".to_string());
|
||||
assert_eq!(
|
||||
&SortBy::try_from(&val).unwrap_err().to_string(),
|
||||
": unknown sort_by value `nosuch`"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_by_invalid_type() {
|
||||
let val = toml::Value::Array(vec![]);
|
||||
assert_eq!(
|
||||
&SortBy::try_from(&val).unwrap_err().to_string(),
|
||||
": not a string"
|
||||
);
|
||||
}
|
||||
}
|
||||
41
cli/src/settings/util.rs
Normal file
41
cli/src/settings/util.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use toml::value::Table;
|
||||
|
||||
/// Check that the input is a table and contains no keys not in the given list, returning
|
||||
/// the table.
|
||||
pub(super) fn table_with_keys<'a>(cfg: &'a toml::Value, keys: &[&str]) -> Result<&'a Table> {
|
||||
let table = cfg.as_table().ok_or_else(|| anyhow!("not a table"))?;
|
||||
|
||||
for tk in table.keys() {
|
||||
if !keys.iter().any(|k| k == tk) {
|
||||
bail!("unknown table key `{}`", tk);
|
||||
}
|
||||
}
|
||||
Ok(table)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use toml::toml;
|
||||
|
||||
#[test]
|
||||
fn test_dissect_table_missing() {
|
||||
let val = toml! { bar = true };
|
||||
let diss = table_with_keys(&val, &["foo", "bar"]).unwrap();
|
||||
assert_eq!(diss.get("bar"), Some(&toml::Value::Boolean(true)));
|
||||
assert_eq!(diss.get("foo"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dissect_table_extra() {
|
||||
let val = toml! { nosuch = 10 };
|
||||
assert!(table_with_keys(&val, &["foo", "bar"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dissect_table_not_a_table() {
|
||||
let val = toml::Value::Array(vec![]);
|
||||
assert!(table_with_keys(&val, &["foo", "bar"]).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user