Merge pull request #70 from djmitche/issue9

Support configuration of the cli
This commit is contained in:
Dustin J. Mitchell 2020-11-28 22:02:07 -05:00 committed by GitHub
commit 7a2857e814
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 228 additions and 36 deletions

75
Cargo.lock generated
View file

@ -580,6 +580,18 @@ 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",
"serde",
"yaml-rust",
]
[[package]]
name = "const_fn"
version = "0.4.3"
@ -710,6 +722,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"
@ -1149,6 +1181,19 @@ 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"
@ -1298,6 +1343,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"
@ -2041,6 +2097,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"
@ -2141,6 +2203,8 @@ version = "0.1.0"
dependencies = [
"assert_cmd",
"clap",
"config",
"dirs 3.0.1",
"failure",
"predicates",
"prettytable-rs",
@ -2189,7 +2253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42"
dependencies = [
"byteorder",
"dirs",
"dirs 1.0.5",
"winapi 0.3.9",
]
@ -2687,3 +2751,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",
]

View file

@ -9,6 +9,8 @@ clap = "^2.33.0"
taskchampion = { path = "../taskchampion" }
failure = "^0.1.8"
prettytable-rs = "^0.8.0"
config = { version="^0.10.1", default-features=false, features=["yaml"] }
dirs = "^3.0.1"
[dev-dependencies]
assert_cmd = "^1.0.1"

View file

@ -38,7 +38,7 @@ define_subcommand! {
subcommand_invocation! {
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
let t = command
.get_replica()
.get_replica()?
.new_task(Status::Pending, self.description.clone())
.unwrap();
println!("added task {}", t.get_uuid());

View file

@ -20,7 +20,7 @@ define_subcommand! {
subcommand_invocation! {
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
command.get_replica().gc()?;
command.get_replica()?.gc()?;
println!("garbage collected.");
Ok(())
}

View file

@ -30,7 +30,7 @@ define_subcommand! {
subcommand_invocation! {
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
let mut replica = command.get_replica();
let mut replica = command.get_replica()?;
let task = shared::get_task(&mut replica, &self.task)?;
let uuid = task.get_uuid();

View file

@ -23,7 +23,7 @@ define_subcommand! {
subcommand_invocation! {
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
let mut replica = command.get_replica();
let mut replica = command.get_replica()?;
let mut t = Table::new();
t.set_format(table::format());
t.set_titles(row![b->"id", b->"description"]);

View file

@ -36,7 +36,7 @@ define_subcommand! {
subcommand_invocation! {
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
let mut replica = command.get_replica();
let mut replica = command.get_replica()?;
let task = shared::get_task(&mut replica, &self.task)?;
let mut task = task.into_mut(&mut replica);

View file

@ -25,7 +25,7 @@ define_subcommand! {
subcommand_invocation! {
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
let working_set = command.get_replica().working_set().unwrap();
let working_set = command.get_replica()?.working_set().unwrap();
let mut t = Table::new();
t.set_format(table::format());
t.set_titles(row![b->"id", b->"description"]);

View file

@ -1,8 +1,9 @@
use crate::settings;
use clap::Arg;
use config::{Config, ConfigError};
use failure::{format_err, Fallible};
use std::env;
use std::ffi::OsString;
use taskchampion::{server, taskstorage, Replica, Task, Uuid};
use std::cell::{Ref, RefCell};
use taskchampion::{server, Replica, ReplicaConfig, ServerConfig, Task, Uuid};
pub(super) fn task_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("task")
@ -33,11 +34,15 @@ pub(super) fn get_task<S: AsRef<str>>(replica: &mut Replica, task_arg: S) -> Fal
#[derive(Debug)]
pub struct CommandInvocation {
pub(crate) subcommand: Box<dyn super::SubCommandInvocation>,
settings: RefCell<Config>,
}
impl CommandInvocation {
pub(crate) fn new(subcommand: Box<dyn super::SubCommandInvocation>) -> Self {
Self { subcommand }
Self {
subcommand,
settings: RefCell::new(Config::default()),
}
}
pub fn run(self) -> Fallible<()> {
@ -46,20 +51,34 @@ impl CommandInvocation {
// -- utilities for command invocations
pub(super) fn get_replica(&self) -> Replica {
// temporarily use $TASK_DB to locate the taskdb
let taskdb_dir = env::var_os("TASK_DB").unwrap_or_else(|| OsString::from("/tmp/tasks"));
Replica::new(Box::new(taskstorage::KVStorage::new(taskdb_dir).unwrap()))
pub(super) fn get_settings(&self) -> Fallible<Ref<Config>> {
{
// 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_server(&self) -> Fallible<impl server::Server> {
// temporarily use $SYNC_SERVER_ORIGIN for the sync server
let sync_server_origin = env::var_os("SYNC_SERVER_ORIGIN")
.map(|osstr| osstr.into_string().unwrap())
.unwrap_or_else(|| String::from("http://localhost:8080"));
Ok(server::RemoteServer::new(
sync_server_origin,
Uuid::parse_str("d5b55cbd-9a82-4860-9a39-41b67893b22f").unwrap(),
))
pub(super) fn get_replica(&self) -> Fallible<Replica> {
let settings = self.get_settings()?;
let replica_config = ReplicaConfig {
taskdb_dir: settings.get_str("data_dir")?.into(),
};
Ok(Replica::from_config(replica_config)?)
}
pub(super) fn get_server(&self) -> Fallible<Box<dyn server::Server>> {
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: settings.get_str("server_origin")?,
client_id,
})?)
}
}

View file

@ -21,7 +21,7 @@ define_subcommand! {
subcommand_invocation! {
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
let mut replica = command.get_replica();
let mut replica = command.get_replica()?;
let mut server = command.get_server()?;
replica.sync(&mut server)?;
Ok(())

View file

@ -3,6 +3,7 @@ use failure::Fallible;
use std::ffi::OsString;
mod cmd;
pub(crate) mod settings;
mod table;
use cmd::ArgMatchResult;

35
cli/src/settings.rs Normal file
View file

@ -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<Config> {
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<FileSourceFile> = 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<FileSourceFile> = dir.into();
settings.merge(config_file.required(false))?;
}
// merge environment variables
settings.merge(Environment::with_prefix("TASKCHAMPION"))?;
Ok(settings)
}

View file

@ -9,12 +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 `task` command will work out-of-the-box with no configuration file, using default values.
Configuration is read from `taskchampion.yaml` 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.

View file

@ -0,0 +1,26 @@
use std::path::PathBuf;
use uuid::Uuid;
/// The configuration required for a replica. Use with [`crate::Replica::from_config`].
pub struct ReplicaConfig {
/// Path containing the task DB.
pub taskdb_dir: PathBuf,
}
/// The configuration for a replica's access to a sync server. Use with
/// [`crate::server::from_config`].
pub enum ServerConfig {
/// A local task database, for situations with a single replica.
Local {
/// Path containing the server's DB
server_dir: PathBuf,
},
/// A remote taskchampion-sync-server instance
Remote {
/// Sync server "origin"; a URL with schema and hostname but no path or trailing `/`
origin: String,
/// Client ID to identify this replica to the server
client_id: Uuid,
},
}

View file

@ -23,6 +23,7 @@ for more information about the design and usage of the tool.
*/
mod config;
mod errors;
mod replica;
pub mod server;
@ -31,6 +32,7 @@ mod taskdb;
pub mod taskstorage;
mod utils;
pub use config::{ReplicaConfig, ServerConfig};
pub use replica::Replica;
pub use task::Priority;
pub use task::Status;

View file

@ -1,8 +1,9 @@
use crate::config::ReplicaConfig;
use crate::errors::Error;
use crate::server::Server;
use crate::task::{Status, Task};
use crate::taskdb::TaskDB;
use crate::taskstorage::{Operation, TaskMap, TaskStorage};
use crate::taskstorage::{KVStorage, Operation, TaskMap, TaskStorage};
use chrono::Utc;
use failure::Fallible;
use std::collections::HashMap;
@ -21,6 +22,11 @@ impl Replica {
}
}
pub fn from_config(config: ReplicaConfig) -> Fallible<Replica> {
let storage = Box::new(KVStorage::new(config.taskdb_dir)?);
Ok(Replica::new(storage))
}
#[cfg(test)]
pub fn new_inmemory() -> Replica {
Replica::new(Box::new(crate::taskstorage::InMemoryStorage::new()))
@ -140,7 +146,7 @@ impl Replica {
}
/// Synchronize this replica against the given server.
pub fn sync(&mut self, server: &mut dyn Server) -> Fallible<()> {
pub fn sync(&mut self, server: &mut Box<dyn Server>) -> Fallible<()> {
self.taskdb.sync(server)
}

View file

@ -25,7 +25,7 @@ pub struct LocalServer<'t> {
impl<'t> LocalServer<'t> {
/// A test server has no notion of clients, signatures, encryption, etc.
pub fn new(directory: &Path) -> Fallible<LocalServer> {
pub fn new<P: AsRef<Path>>(directory: P) -> Fallible<LocalServer<'t>> {
let mut config = Config::default(directory);
config.bucket("versions", None);
config.bucket("numbers", None);

View file

@ -1,3 +1,6 @@
use crate::ServerConfig;
use failure::Fallible;
#[cfg(test)]
pub(crate) mod test;
@ -8,3 +11,12 @@ mod types;
pub use local::LocalServer;
pub use remote::RemoteServer;
pub use types::*;
pub fn from_config(config: ServerConfig) -> Fallible<Box<dyn Server>> {
Ok(match config {
ServerConfig::Local { server_dir } => Box::new(LocalServer::new(server_dir)?),
ServerConfig::Remote { origin, client_id } => {
Box::new(RemoteServer::new(origin, client_id))
}
})
}

View file

@ -164,7 +164,7 @@ impl TaskDB {
}
/// Sync to the given server, pulling remote changes and pushing local changes.
pub fn sync(&mut self, server: &mut dyn Server) -> Fallible<()> {
pub fn sync(&mut self, server: &mut Box<dyn Server>) -> Fallible<()> {
let mut txn = self.storage.txn()?;
// retry synchronizing until the server accepts our version (this allows for races between
@ -542,7 +542,7 @@ mod tests {
#[test]
fn test_sync() {
let mut server = TestServer::new();
let mut server: Box<dyn Server> = Box::new(TestServer::new());
let mut db1 = newdb();
db1.sync(&mut server).unwrap();
@ -602,7 +602,7 @@ mod tests {
#[test]
fn test_sync_create_delete() {
let mut server = TestServer::new();
let mut server: Box<dyn Server> = Box::new(TestServer::new());
let mut db1 = newdb();
db1.sync(&mut server).unwrap();
@ -692,7 +692,7 @@ mod tests {
// and delete operations that results in a task existing in one TaskDB but not existing in
// another. So, the generated sequences focus on a single task UUID.
fn transform_sequences_of_operations(action_sequence in action_sequence_strategy()) {
let mut server = TestServer::new();
let mut server: Box<dyn Server> = Box::new(TestServer::new());
let mut dbs = [newdb(), newdb(), newdb()];
for (action, db) in action_sequence {

View file

@ -2,11 +2,13 @@ use failure::Fallible;
use std::collections::HashMap;
use uuid::Uuid;
#[cfg(test)]
mod inmemory;
mod kv;
mod operation;
pub use self::kv::KVStorage;
#[cfg(test)]
pub use inmemory::InMemoryStorage;
pub use operation::Operation;