Merge pull request #147 from djmitche/issue144

Reorganize the 'taskchampion' crate
This commit is contained in:
Dustin J. Mitchell 2021-01-10 22:31:40 -05:00 committed by GitHub
commit 9cfbe01271
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 118 additions and 80 deletions

View file

@ -3,7 +3,7 @@
use crate::argparse::{Command, Subcommand}; use crate::argparse::{Command, Subcommand};
use config::Config; use config::Config;
use failure::{format_err, Fallible}; use failure::{format_err, Fallible};
use taskchampion::{server, Replica, ReplicaConfig, ServerConfig, Uuid}; use taskchampion::{Replica, Server, ServerConfig, StorageConfig, Uuid};
use termcolor::{ColorChoice, StandardStream}; use termcolor::{ColorChoice, StandardStream};
mod cmd; mod cmd;
@ -104,15 +104,15 @@ pub(crate) fn invoke(command: Command, settings: Config) -> Fallible<()> {
fn get_replica(settings: &Config) -> Fallible<Replica> { fn get_replica(settings: &Config) -> Fallible<Replica> {
let taskdb_dir = settings.get_str("data_dir")?.into(); let taskdb_dir = settings.get_str("data_dir")?.into();
log::debug!("Replica data_dir: {:?}", taskdb_dir); log::debug!("Replica data_dir: {:?}", taskdb_dir);
let replica_config = ReplicaConfig { taskdb_dir }; let storage_config = StorageConfig::OnDisk { taskdb_dir };
Ok(Replica::from_config(replica_config)?) Ok(Replica::new(storage_config.into_storage()?))
} }
/// Get the server for this invocation /// Get the server for this invocation
fn get_server(settings: &Config) -> Fallible<Box<dyn server::Server>> { fn get_server(settings: &Config) -> Fallible<Box<dyn Server>> {
// if server_client_key and server_origin are both set, use // if server_client_key and server_origin are both set, use
// the remote server // the remote server
if let (Ok(client_key), Ok(origin)) = ( let config = if let (Ok(client_key), Ok(origin)) = (
settings.get_str("server_client_key"), settings.get_str("server_client_key"),
settings.get_str("server_origin"), settings.get_str("server_origin"),
) { ) {
@ -123,16 +123,17 @@ fn get_server(settings: &Config) -> Fallible<Box<dyn server::Server>> {
log::debug!("Using sync-server with origin {}", origin); log::debug!("Using sync-server with origin {}", origin);
log::debug!("Sync client ID: {}", client_key); log::debug!("Sync client ID: {}", client_key);
Ok(server::from_config(ServerConfig::Remote { ServerConfig::Remote {
origin, origin,
client_key, client_key,
encryption_secret: encryption_secret.as_bytes().to_vec(), encryption_secret: encryption_secret.as_bytes().to_vec(),
})?) }
} else { } else {
let server_dir = settings.get_str("server_dir")?.into(); let server_dir = settings.get_str("server_dir")?.into();
log::debug!("Using local sync-server at `{:?}`", server_dir); log::debug!("Using local sync-server at `{:?}`", server_dir);
Ok(server::from_config(ServerConfig::Local { server_dir })?) ServerConfig::Local { server_dir }
} };
Ok(config.into_server()?)
} }
/// Get a WriteColor implementation based on whether the output is a tty. /// Get a WriteColor implementation based on whether the output is a tty.

View file

@ -1,16 +1,17 @@
use std::io; use std::io;
use taskchampion::{server, taskstorage, Replica, ServerConfig}; use taskchampion::{storage, Replica, Server, ServerConfig};
use tempdir::TempDir; use tempdir::TempDir;
pub(super) fn test_replica() -> Replica { pub(super) fn test_replica() -> Replica {
let storage = taskstorage::InMemoryStorage::new(); let storage = storage::InMemoryStorage::new();
Replica::new(Box::new(storage)) Replica::new(Box::new(storage))
} }
pub(super) fn test_server(dir: &TempDir) -> Box<dyn server::Server> { pub(super) fn test_server(dir: &TempDir) -> Box<dyn Server> {
server::from_config(ServerConfig::Local { ServerConfig::Local {
server_dir: dir.path().to_path_buf(), server_dir: dir.path().to_path_buf(),
}) }
.into_server()
.unwrap() .unwrap()
} }

View file

@ -1,7 +1,7 @@
# Replica Storage # Replica Storage
Each replica has a storage backend. Each replica has a storage backend.
The interface for this backend is given in `crate::taskstorage::TaskStorage` and `TaskStorageTxn`. The interface for this backend is given in `crate::taskstorage::Storage` and `StorageTxn`.
The storage is transaction-protected, with the expectation of a serializable isolation level. The storage is transaction-protected, with the expectation of a serializable isolation level.
The storage contains the following information: The storage contains the following information:

View file

@ -3,19 +3,26 @@
This crate implements the core of TaskChampion, the [replica](crate::Replica). This crate implements the core of TaskChampion, the [replica](crate::Replica).
# Replica
A TaskChampion replica is a local copy of a user's task data. As the name suggests, several A TaskChampion replica is a local copy of a user's task data. As the name suggests, several
replicas of the same data can exist (such as on a user's laptop and on their phone) and can replicas of the same data can exist (such as on a user's laptop and on their phone) and can
synchronize with one another. synchronize with one another.
Replicas are accessed using the [`Replica`](crate::replica) type.
# Task Storage # Task Storage
The [`taskstorage`](crate::taskstorage) module supports pluggable storage for a replica's data. The [`storage`](crate::storage) module supports pluggable storage for a replica's data.
An implementation is provided, but users of this crate can provide their own implementation as well. An implementation is provided, but users of this crate can provide their own implementation as well.
# Server # Server
Replica synchronization takes place against a server. Replica synchronization takes place against a server.
Create a server with [`ServerConfig`](crate::ServerConfig).
The [`server`](crate::server) module defines the interface a server must meet. The [`server`](crate::server) module defines the interface a server must meet.
Users can define their own server impelementations.
# See Also # See Also
@ -24,18 +31,18 @@ for more information about the design and usage of the tool.
*/ */
mod config;
mod errors; mod errors;
mod replica; mod replica;
pub mod server; pub mod server;
pub mod storage;
mod task; mod task;
mod taskdb; mod taskdb;
pub mod taskstorage;
mod utils; mod utils;
mod workingset; mod workingset;
pub use config::{ReplicaConfig, ServerConfig};
pub use replica::Replica; pub use replica::Replica;
pub use server::{Server, ServerConfig};
pub use storage::StorageConfig;
pub use task::{Priority, Status, Tag, Task, TaskMut}; pub use task::{Priority, Status, Tag, Task, TaskMut};
pub use workingset::WorkingSet; pub use workingset::WorkingSet;

View file

@ -1,9 +1,8 @@
use crate::config::ReplicaConfig;
use crate::errors::Error; use crate::errors::Error;
use crate::server::Server; use crate::server::Server;
use crate::storage::{Operation, Storage, TaskMap};
use crate::task::{Status, Task}; use crate::task::{Status, Task};
use crate::taskdb::TaskDB; use crate::taskdb::TaskDB;
use crate::taskstorage::{KVStorage, Operation, TaskMap, TaskStorage};
use crate::workingset::WorkingSet; use crate::workingset::WorkingSet;
use chrono::Utc; use chrono::Utc;
use failure::Fallible; use failure::Fallible;
@ -30,22 +29,15 @@ pub struct Replica {
} }
impl Replica { impl Replica {
pub fn new(storage: Box<dyn TaskStorage>) -> Replica { pub fn new(storage: Box<dyn Storage>) -> Replica {
Replica { Replica {
taskdb: TaskDB::new(storage), taskdb: TaskDB::new(storage),
} }
} }
/// Construct a new replica from a configuration object. This is the common way
/// to create a new object.
pub fn from_config(config: ReplicaConfig) -> Fallible<Replica> {
let storage = Box::new(KVStorage::new(config.taskdb_dir)?);
Ok(Replica::new(storage))
}
#[cfg(test)] #[cfg(test)]
pub fn new_inmemory() -> Replica { pub fn new_inmemory() -> Replica {
Replica::new(Box::new(crate::taskstorage::InMemoryStorage::new())) Replica::new(Box::new(crate::storage::InMemoryStorage::new()))
} }
/// Update an existing task. If the value is Some, the property is added or updated. If the /// Update an existing task. If the value is Some, the property is added or updated. If the

View file

@ -1,14 +1,10 @@
use super::types::Server;
use super::{LocalServer, RemoteServer};
use failure::Fallible;
use std::path::PathBuf; use std::path::PathBuf;
use uuid::Uuid; use uuid::Uuid;
/// The configuration required for a replica. Use with [`crate::Replica::from_config`]. /// The configuration for a replica's access to a sync server.
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 { pub enum ServerConfig {
/// A local task database, for situations with a single replica. /// A local task database, for situations with a single replica.
Local { Local {
@ -28,3 +24,17 @@ pub enum ServerConfig {
encryption_secret: Vec<u8>, encryption_secret: Vec<u8>,
}, },
} }
impl ServerConfig {
/// Get a server based on this configuration
pub fn into_server(self) -> Fallible<Box<dyn Server>> {
Ok(match self {
ServerConfig::Local { server_dir } => Box::new(LocalServer::new(server_dir)?),
ServerConfig::Remote {
origin,
client_key,
encryption_secret,
} => Box::new(RemoteServer::new(origin, client_key, encryption_secret)),
})
}
}

View file

@ -1,25 +1,22 @@
use crate::ServerConfig; /**
use failure::Fallible;
This module defines the client interface to TaskChampion sync servers.
It defines a [trait](crate::server::Server) for servers, and implements both local and remote servers.
Typical uses of this crate do not interact directly with this module; [`ServerConfig`](crate::ServerConfig) is sufficient.
However, users who wish to implement their own server interfaces can implement the traits defined here and pass the result to [`Replica`](crate::Replica).
*/
#[cfg(test)] #[cfg(test)]
pub(crate) mod test; pub(crate) mod test;
mod config;
mod local; mod local;
mod remote; mod remote;
mod types; mod types;
pub use config::ServerConfig;
pub use local::LocalServer; pub use local::LocalServer;
pub use remote::RemoteServer; pub use remote::RemoteServer;
pub use types::*; pub use types::*;
/// Create a new server based on the given configuration.
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_key,
encryption_secret,
} => Box::new(RemoteServer::new(origin, client_key, encryption_secret)),
})
}

View file

@ -0,0 +1,23 @@
use super::{InMemoryStorage, KVStorage, Storage};
use failure::Fallible;
use std::path::PathBuf;
/// The configuration required for a replica's storage.
pub enum StorageConfig {
/// Store the data on disk. This is the common choice.
OnDisk {
/// Path containing the task DB.
taskdb_dir: PathBuf,
},
/// Store the data in memory. This is only useful for testing.
InMemory,
}
impl StorageConfig {
pub fn into_storage(self) -> Fallible<Box<dyn Storage>> {
Ok(match self {
StorageConfig::OnDisk { taskdb_dir } => Box::new(KVStorage::new(taskdb_dir)?),
StorageConfig::InMemory => Box::new(InMemoryStorage::new()),
})
}
}

View file

@ -1,8 +1,6 @@
#![allow(clippy::new_without_default)] #![allow(clippy::new_without_default)]
use crate::taskstorage::{ use crate::storage::{Operation, Storage, StorageTxn, TaskMap, VersionId, DEFAULT_BASE_VERSION};
Operation, TaskMap, TaskStorage, TaskStorageTxn, VersionId, DEFAULT_BASE_VERSION,
};
use failure::{bail, Fallible}; use failure::{bail, Fallible};
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::collections::HashMap; use std::collections::HashMap;
@ -42,7 +40,7 @@ impl<'t> Txn<'t> {
} }
} }
impl<'t> TaskStorageTxn for Txn<'t> { impl<'t> StorageTxn for Txn<'t> {
fn get_task(&mut self, uuid: Uuid) -> Fallible<Option<TaskMap>> { fn get_task(&mut self, uuid: Uuid) -> Fallible<Option<TaskMap>> {
match self.data_ref().tasks.get(&uuid) { match self.data_ref().tasks.get(&uuid) {
None => Ok(None), None => Ok(None),
@ -157,8 +155,8 @@ impl InMemoryStorage {
} }
} }
impl TaskStorage for InMemoryStorage { impl Storage for InMemoryStorage {
fn txn<'a>(&'a mut self) -> Fallible<Box<dyn TaskStorageTxn + 'a>> { fn txn<'a>(&'a mut self) -> Fallible<Box<dyn StorageTxn + 'a>> {
Ok(Box::new(Txn { Ok(Box::new(Txn {
storage: self, storage: self,
new_data: None, new_data: None,

View file

@ -1,6 +1,4 @@
use crate::taskstorage::{ use crate::storage::{Operation, Storage, StorageTxn, TaskMap, VersionId, DEFAULT_BASE_VERSION};
Operation, TaskMap, TaskStorage, TaskStorageTxn, VersionId, DEFAULT_BASE_VERSION,
};
use crate::utils::Key; use crate::utils::Key;
use failure::{bail, Fallible}; use failure::{bail, Fallible};
use kv::msgpack::Msgpack; use kv::msgpack::Msgpack;
@ -62,8 +60,8 @@ impl<'t> KVStorage<'t> {
} }
} }
impl<'t> TaskStorage for KVStorage<'t> { impl<'t> Storage for KVStorage<'t> {
fn txn<'a>(&'a mut self) -> Fallible<Box<dyn TaskStorageTxn + 'a>> { fn txn<'a>(&'a mut self) -> Fallible<Box<dyn StorageTxn + 'a>> {
Ok(Box::new(Txn { Ok(Box::new(Txn {
storage: self, storage: self,
txn: Some(self.store.write_txn()?), txn: Some(self.store.write_txn()?),
@ -104,7 +102,7 @@ impl<'t> Txn<'t> {
} }
} }
impl<'t> TaskStorageTxn for Txn<'t> { impl<'t> StorageTxn for Txn<'t> {
fn get_task(&mut self, uuid: Uuid) -> Fallible<Option<TaskMap>> { fn get_task(&mut self, uuid: Uuid) -> Fallible<Option<TaskMap>> {
let bucket = self.tasks_bucket(); let bucket = self.tasks_bucket();
let buf = match self.kvtxn().get(bucket, uuid.into()) { let buf = match self.kvtxn().get(bucket, uuid.into()) {
@ -356,7 +354,7 @@ impl<'t> TaskStorageTxn for Txn<'t> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::taskstorage::taskmap_with; use crate::storage::taskmap_with;
use failure::Fallible; use failure::Fallible;
use tempdir::TempDir; use tempdir::TempDir;

View file

@ -1,12 +1,23 @@
/**
This module defines the backend storage used by [`Replica`](crate::Replica).
It defines a [trait](crate::storage::Storage) for storage implementations, and provides a default on-disk implementation as well as an in-memory implementation for testing.
Typical uses of this crate do not interact directly with this module; [`StorageConfig`](crate::StorageConfig) is sufficient.
However, users who wish to implement their own storage backends can implement the traits defined here and pass the result to [`Replica`](crate::Replica).
*/
use failure::Fallible; use failure::Fallible;
use std::collections::HashMap; use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
mod config;
mod inmemory; mod inmemory;
mod kv; mod kv;
mod operation; mod operation;
pub use self::kv::KVStorage; pub use self::kv::KVStorage;
pub use config::StorageConfig;
pub use inmemory::InMemoryStorage; pub use inmemory::InMemoryStorage;
pub use operation::Operation; pub use operation::Operation;
@ -29,7 +40,7 @@ pub use crate::server::VersionId;
/// The default for base_version. /// The default for base_version.
pub(crate) const DEFAULT_BASE_VERSION: Uuid = crate::server::NO_VERSION_ID; pub(crate) const DEFAULT_BASE_VERSION: Uuid = crate::server::NO_VERSION_ID;
/// A TaskStorage transaction, in which storage operations are performed. /// A Storage transaction, in which storage operations are performed.
/// ///
/// # Concurrency /// # Concurrency
/// ///
@ -40,9 +51,9 @@ pub(crate) const DEFAULT_BASE_VERSION: Uuid = crate::server::NO_VERSION_ID;
/// # Commiting and Aborting /// # Commiting and Aborting
/// ///
/// A transaction is not visible to other readers until it is committed with /// A transaction is not visible to other readers until it is committed with
/// [`crate::taskstorage::TaskStorageTxn::commit`]. Transactions are aborted if they are dropped. /// [`crate::storage::StorageTxn::commit`]. Transactions are aborted if they are dropped.
/// It is safe and performant to drop transactions that did not modify any data without committing. /// It is safe and performant to drop transactions that did not modify any data without committing.
pub trait TaskStorageTxn { pub trait StorageTxn {
/// Get an (immutable) task, if it is in the storage /// Get an (immutable) task, if it is in the storage
fn get_task(&mut self, uuid: Uuid) -> Fallible<Option<TaskMap>>; fn get_task(&mut self, uuid: Uuid) -> Fallible<Option<TaskMap>>;
@ -102,8 +113,8 @@ pub trait TaskStorageTxn {
} }
/// A trait for objects able to act as task storage. Most of the interesting behavior is in the /// A trait for objects able to act as task storage. Most of the interesting behavior is in the
/// [`crate::taskstorage::TaskStorageTxn`] trait. /// [`crate::storage::StorageTxn`] trait.
pub trait TaskStorage { pub trait Storage {
/// Begin a transaction /// Begin a transaction
fn txn<'a>(&'a mut self) -> Fallible<Box<dyn TaskStorageTxn + 'a>>; fn txn<'a>(&'a mut self) -> Fallible<Box<dyn StorageTxn + 'a>>;
} }

View file

@ -125,8 +125,8 @@ impl Operation {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::storage::InMemoryStorage;
use crate::taskdb::TaskDB; use crate::taskdb::TaskDB;
use crate::taskstorage::InMemoryStorage;
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use proptest::prelude::*; use proptest::prelude::*;

View file

@ -1,5 +1,5 @@
use crate::replica::Replica; use crate::replica::Replica;
use crate::taskstorage::TaskMap; use crate::storage::TaskMap;
use chrono::prelude::*; use chrono::prelude::*;
use failure::{format_err, Fallible}; use failure::{format_err, Fallible};
use log::trace; use log::trace;

View file

@ -1,6 +1,6 @@
use crate::errors::Error; use crate::errors::Error;
use crate::server::{AddVersionResult, GetVersionResult, Server}; use crate::server::{AddVersionResult, GetVersionResult, Server};
use crate::taskstorage::{Operation, TaskMap, TaskStorage, TaskStorageTxn}; use crate::storage::{Operation, Storage, StorageTxn, TaskMap};
use failure::{format_err, Fallible}; use failure::{format_err, Fallible};
use log::{info, trace, warn}; use log::{info, trace, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -12,7 +12,7 @@ use uuid::Uuid;
/// and so on, and all the invariants that come with it. It leaves the meaning of particular task /// and so on, and all the invariants that come with it. It leaves the meaning of particular task
/// properties to the replica and task implementations. /// properties to the replica and task implementations.
pub struct TaskDB { pub struct TaskDB {
storage: Box<dyn TaskStorage>, storage: Box<dyn Storage>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -22,13 +22,13 @@ struct Version {
impl TaskDB { impl TaskDB {
/// Create a new TaskDB with the given backend storage /// Create a new TaskDB with the given backend storage
pub fn new(storage: Box<dyn TaskStorage>) -> TaskDB { pub fn new(storage: Box<dyn Storage>) -> TaskDB {
TaskDB { storage } TaskDB { storage }
} }
#[cfg(test)] #[cfg(test)]
pub fn new_inmemory() -> TaskDB { pub fn new_inmemory() -> TaskDB {
TaskDB::new(Box::new(crate::taskstorage::InMemoryStorage::new())) TaskDB::new(Box::new(crate::storage::InMemoryStorage::new()))
} }
/// Apply an operation to the TaskDB. Aside from synchronization operations, this is the only way /// Apply an operation to the TaskDB. Aside from synchronization operations, this is the only way
@ -45,7 +45,7 @@ impl TaskDB {
Ok(()) Ok(())
} }
fn apply_op(txn: &mut dyn TaskStorageTxn, op: &Operation) -> Fallible<()> { fn apply_op(txn: &mut dyn StorageTxn, op: &Operation) -> Fallible<()> {
match op { match op {
Operation::Create { uuid } => { Operation::Create { uuid } => {
// insert if the task does not already exist // insert if the task does not already exist
@ -261,7 +261,7 @@ impl TaskDB {
Ok(()) Ok(())
} }
fn apply_version(txn: &mut dyn TaskStorageTxn, mut version: Version) -> Fallible<()> { fn apply_version(txn: &mut dyn StorageTxn, mut version: Version) -> Fallible<()> {
// The situation here is that the server has already applied all server operations, and we // The situation here is that the server has already applied all server operations, and we
// have already applied all local operations, so states have diverged by several // have already applied all local operations, so states have diverged by several
// operations. We need to figure out what operations to apply locally and on the server in // operations. We need to figure out what operations to apply locally and on the server in
@ -358,7 +358,7 @@ impl TaskDB {
mod tests { mod tests {
use super::*; use super::*;
use crate::server::test::TestServer; use crate::server::test::TestServer;
use crate::taskstorage::InMemoryStorage; use crate::storage::InMemoryStorage;
use chrono::Utc; use chrono::Utc;
use proptest::prelude::*; use proptest::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;