From 0e926df578812e57049daa5850b75a30ec942960 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 28 Nov 2020 18:18:43 -0500 Subject: [PATCH] Add configuration-file support to the 'task' command --- Cargo.lock | 178 ++++++++++++++++++++++++++++++++++++------ cli/Cargo.toml | 2 + cli/src/cmd/shared.rs | 42 ++++++---- cli/src/lib.rs | 1 + cli/src/settings.rs | 35 +++++++++ docs/src/usage.md | 23 ++++-- 6 files changed, 236 insertions(+), 45 deletions(-) create mode 100644 cli/src/settings.rs diff --git a/Cargo.lock b/Cargo.lock index 4147eb7c9..9a683e918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,7 +74,7 @@ dependencies = [ "pin-project 1.0.2", "rand 0.7.3", "regex", - "serde", + "serde 1.0.117", "serde_json", "serde_urlencoded", "sha-1", @@ -102,7 +102,7 @@ dependencies = [ "http", "log", "regex", - "serde", + "serde 1.0.117", ] [[package]] @@ -241,7 +241,7 @@ dependencies = [ "mime", "pin-project 1.0.2", "regex", - "serde", + "serde 1.0.117", "serde_json", "serde_urlencoded", "socket2", @@ -372,7 +372,7 @@ dependencies = [ "mime", "percent-encoding", "rand 0.7.3", - "serde", + "serde 1.0.117", "serde_json", "serde_urlencoded", ] @@ -479,7 +479,7 @@ dependencies = [ "lazy_static", "memchr", "regex-automata", - "serde", + "serde 1.0.117", ] [[package]] @@ -535,8 +535,8 @@ checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ "libc", "num-integer", - "num-traits", - "serde", + "num-traits 0.2.14", + "serde 1.0.117", "time 0.1.44", "winapi 0.3.9", ] @@ -580,6 +580,22 @@ dependencies = [ "bitflags", ] +[[package]] +name = "config" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +dependencies = [ + "lazy_static", + "nom", + "rust-ini", + "serde 1.0.117", + "serde-hjson", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "const_fn" version = "0.4.3" @@ -613,7 +629,7 @@ dependencies = [ "idna", "log", "publicsuffix", - "serde", + "serde 1.0.117", "serde_json", "time 0.2.23", "url", @@ -661,7 +677,7 @@ dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde 1.0.117", ] [[package]] @@ -710,6 +726,26 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "dirs" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "discard" version = "1.0.4" @@ -804,7 +840,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" dependencies = [ - "num-traits", + "num-traits 0.2.14", ] [[package]] @@ -1132,7 +1168,7 @@ checksum = "cb79e59d356a5ae85b13990bbb3649a293d64df1ca6e7890822076186527a9f7" dependencies = [ "lmdb-rkv", "rmp-serde", - "serde", + "serde 1.0.117", "thiserror", "toml", ] @@ -1149,12 +1185,35 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexical-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 0.1.10", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +[[package]] +name = "linked-hash-map" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" +dependencies = [ + "serde 0.8.23", + "serde_test", +] + [[package]] name = "linked-hash-map" version = "0.5.3" @@ -1208,7 +1267,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" dependencies = [ - "linked-hash-map", + "linked-hash-map 0.5.3", ] [[package]] @@ -1298,6 +1357,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1311,7 +1381,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ "autocfg 1.0.1", - "num-traits", + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.14", ] [[package]] @@ -1527,7 +1606,7 @@ dependencies = [ "bitflags", "byteorder", "lazy_static", - "num-traits", + "num-traits 0.2.14", "quick-error", "rand 0.6.5", "rand_chacha 0.1.1", @@ -1828,7 +1907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f10b46df14cf1ee1ac7baa4d2fbc2c52c0622a4b82fa8740e37bc452ac0184f" dependencies = [ "byteorder", - "num-traits", + "num-traits 0.2.14", ] [[package]] @@ -1839,7 +1918,7 @@ checksum = "4ce7d70c926fe472aed493b902010bccc17fa9f7284145cb8772fd22fdb052d8" dependencies = [ "byteorder", "rmp", - "serde", + "serde 1.0.117", ] [[package]] @@ -1854,6 +1933,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rust-ini" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" + [[package]] name = "rustc-demangle" version = "0.1.18" @@ -1931,6 +2016,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "serde" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" + [[package]] name = "serde" version = "1.0.117" @@ -1940,6 +2031,19 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-hjson" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8" +dependencies = [ + "lazy_static", + "linked-hash-map 0.3.0", + "num-traits 0.1.43", + "regex", + "serde 0.8.23", +] + [[package]] name = "serde_derive" version = "1.0.117" @@ -1959,7 +2063,16 @@ checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" dependencies = [ "itoa", "ryu", - "serde", + "serde 1.0.117", +] + +[[package]] +name = "serde_test" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5" +dependencies = [ + "serde 0.8.23", ] [[package]] @@ -1971,7 +2084,7 @@ dependencies = [ "form_urlencoded", "itoa", "ryu", - "serde", + "serde 1.0.117", ] [[package]] @@ -2041,6 +2154,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stdweb" version = "0.4.20" @@ -2063,7 +2182,7 @@ checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" dependencies = [ "proc-macro2", "quote", - "serde", + "serde 1.0.117", "serde_derive", "syn", ] @@ -2077,7 +2196,7 @@ dependencies = [ "base-x", "proc-macro2", "quote", - "serde", + "serde 1.0.117", "serde_derive", "serde_json", "sha1", @@ -2128,7 +2247,7 @@ dependencies = [ "kv", "lmdb-rkv", "proptest", - "serde", + "serde 1.0.117", "serde_json", "tempdir", "ureq", @@ -2141,6 +2260,8 @@ version = "0.1.0" dependencies = [ "assert_cmd", "clap", + "config", + "dirs 3.0.1", "failure", "predicates", "prettytable-rs", @@ -2189,7 +2310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" dependencies = [ "byteorder", - "dirs", + "dirs 1.0.5", "winapi 0.3.9", ] @@ -2344,7 +2465,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" dependencies = [ - "serde", + "serde 1.0.117", ] [[package]] @@ -2510,7 +2631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" dependencies = [ "rand 0.7.3", - "serde", + "serde 1.0.117", ] [[package]] @@ -2687,3 +2808,12 @@ dependencies = [ "winapi 0.2.8", "winapi-build", ] + +[[package]] +name = "yaml-rust" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" +dependencies = [ + "linked-hash-map 0.5.3", +] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 89b1b9ed9..2f8ff3c83 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -9,6 +9,8 @@ clap = "^2.33.0" taskchampion = { path = "../taskchampion" } failure = "^0.1.8" prettytable-rs = "^0.8.0" +config = "^0.10.1" +dirs = "^3.0.1" [dev-dependencies] assert_cmd = "^1.0.1" diff --git a/cli/src/cmd/shared.rs b/cli/src/cmd/shared.rs index bcd6fbf0b..9744ea6f3 100644 --- a/cli/src/cmd/shared.rs +++ b/cli/src/cmd/shared.rs @@ -1,7 +1,8 @@ +use crate::settings; use clap::Arg; +use config::{Config, ConfigError}; use failure::{format_err, Fallible}; -use std::env; -use std::ffi::OsString; +use std::cell::{Ref, RefCell}; use taskchampion::{server, Replica, ReplicaConfig, ServerConfig, Task, Uuid}; pub(super) fn task_arg<'a>() -> Arg<'a, 'a> { @@ -33,11 +34,15 @@ pub(super) fn get_task>(replica: &mut Replica, task_arg: S) -> Fal #[derive(Debug)] pub struct CommandInvocation { pub(crate) subcommand: Box, + settings: RefCell, } impl CommandInvocation { pub(crate) fn new(subcommand: Box) -> Self { - Self { subcommand } + Self { + subcommand, + settings: RefCell::new(Config::default()), + } } pub fn run(self) -> Fallible<()> { @@ -46,28 +51,33 @@ impl CommandInvocation { // -- utilities for command invocations + pub(super) fn get_settings(&self) -> Fallible> { + { + // use the special `_loaded" config value to detect whether we have + // loaded the configuration yet + let mut settings = self.settings.borrow_mut(); + if let Err(ConfigError::NotFound(_)) = settings.get_bool("_loaded") { + settings.merge(settings::read_settings()?)?; + settings.set("_loaded", true)?; + } + } + Ok(self.settings.borrow()) + } + pub(super) fn get_replica(&self) -> Fallible { - // temporarily use $TASK_DB to locate the taskdb - let taskdb_dir = env::var_os("TASK_DB").unwrap_or_else(|| OsString::from("/tmp/tasks")); + let settings = self.get_settings()?; let replica_config = ReplicaConfig { - taskdb_dir: taskdb_dir.into(), + taskdb_dir: settings.get_str("data_dir")?.into(), }; Ok(Replica::from_config(replica_config)?) } pub(super) fn get_server(&self) -> Fallible> { - // temporarily use $SYNC_SERVER_ORIGIN for the sync server - let origin = env::var_os("SYNC_SERVER_ORIGIN") - .map(|osstr| osstr.into_string().unwrap()) - .unwrap_or_else(|| String::from("http://localhost:8080")); - let client_id = env::var_os("SYNC_SERVER_CLIENT_ID") - .ok_or_else(|| format_err!("SYNC_SERVER_CLIENT_ID not set"))?; - let client_id = client_id - .into_string() - .map_err(|_| format_err!("SYNC_SERVER_CLIENT_ID is not a valid UUID"))?; + let settings = self.get_settings()?; + let client_id = settings.get_str("server_client_id")?; let client_id = Uuid::parse_str(&client_id)?; Ok(server::from_config(ServerConfig::Remote { - origin, + origin: settings.get_str("server_origin")?, client_id, })?) } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 48421f25a..4474af8f3 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -3,6 +3,7 @@ use failure::Fallible; use std::ffi::OsString; mod cmd; +pub(crate) mod settings; mod table; use cmd::ArgMatchResult; diff --git a/cli/src/settings.rs b/cli/src/settings.rs new file mode 100644 index 000000000..f8a92582e --- /dev/null +++ b/cli/src/settings.rs @@ -0,0 +1,35 @@ +use config::{Config, Environment, File, FileSourceFile}; +use failure::Fallible; +use std::env; +use std::path::PathBuf; + +pub(crate) fn read_settings() -> Fallible { + let mut settings = Config::default(); + + // set up defaults + if let Some(mut dir) = dirs::data_local_dir() { + dir.push("taskchampion"); + settings.set_default( + "data_dir", + // the config crate does not support non-string paths + dir.to_str().expect("data_local_dir is not utf-8"), + )?; + } + + // load either from the path in TASKCHAMPION_CONFIG, or from CONFIG_DIR/taskchampion + if let Some(config_file) = env::var_os("TASKCHAMPION_CONFIG") { + let config_file: PathBuf = config_file.into(); + let config_file: File = config_file.into(); + settings.merge(config_file.required(true))?; + env::remove_var("TASKCHAMPION_CONFIG"); + } else if let Some(mut dir) = dirs::config_dir() { + dir.push("taskchampion"); + let config_file: File = dir.into(); + settings.merge(config_file.required(false))?; + } + + // merge environment variables + settings.merge(Environment::with_prefix("TASKCHAMPION"))?; + + Ok(settings) +} diff --git a/docs/src/usage.md b/docs/src/usage.md index 47ab9388b..329cdf1eb 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -9,13 +9,26 @@ Note that the `task` interface does not match that of TaskWarrior. ### Configuration -Temporarily, configuration is by environment variables. -The directory containing the replica's task data is given by `TASK_DB`, defaulting to `/tmp/tasks`. -the origin of the sync server is given by `SYNC_SERVER_ORIGIN`, defaulting to `http://localhost:8080`. -The client ID to use with the sync server is givne by `SYNC_SERVER_CLIENT_ID` (with no default). +The `task` command will work out-of-the-box with no configuration file, using default values. + +Configuration is read from `taskchampion.yaml` (or `taskchampion.toml` or `taskchmapion.json` if you prefer) in your config directory. +On Linux systems, that directory is `~/.config`. +On OS X, it's `~/Library/Preferences`. +On Windows, it's `AppData/Roaming` in your home directory. +The path can be overridden by setting `$TASKCHAMPION_CONFIG`. + +Individual configuration parameters can be overridden by environemnt variables, converted to upper-case and prefixed with `TASKCHAMPION_`, e.g., `TASKCHAMPION_DATA_DIR`. +Nested configuration parameters cannot be overridden by environment variables. + +The following configuration parameters are available: + +* `data_dir` - path to a directory containing the replica's task data (which will be created if necessary). + Default: `taskchampion` in the local data directory +* `server_origin` - Origin of the taskchampion sync server, e.g., `https://taskchampion.example.com` +* `server_client_id` - Client ID to identify this replica to the sync server (a UUID) ## `taskchampion-sync-server` Run `taskchampion-sync-server` to start the sync server. It serves on port 8080 on all interfaces, using an in-memory database (meaning that all data is lost when the process exits). -Requests for previously-unknown clients are automatically added. +Requests for previously-unknown clients automatically create the client.