diff --git a/cli/src/cmd/mod.rs b/cli/src/cmd/mod.rs index e7d27255b..3e55d0475 100644 --- a/cli/src/cmd/mod.rs +++ b/cli/src/cmd/mod.rs @@ -1,7 +1,5 @@ use clap::{App, ArgMatches}; use failure::{Error, Fallible}; -use std::path::Path; -use taskchampion::{taskstorage, Replica}; #[macro_use] mod macros; @@ -12,6 +10,7 @@ mod gc; mod info; mod list; mod pending; +mod sync; /// Get a list of all subcommands in this crate pub(crate) fn subcommands() -> Vec> { @@ -21,6 +20,7 @@ pub(crate) fn subcommands() -> Vec> { list::cmd(), pending::cmd(), info::cmd(), + sync::cmd(), ] } @@ -54,24 +54,4 @@ pub(crate) trait SubCommandInvocation: std::fmt::Debug { fn as_any(&self) -> &dyn std::any::Any; } -/// A command invocation contains all of the necessary regarding a single invocation of the CLI. -#[derive(Debug)] -pub struct CommandInvocation { - pub(crate) subcommand: Box, -} - -impl CommandInvocation { - pub(crate) fn new(subcommand: Box) -> Self { - Self { subcommand } - } - - pub fn run(self) -> Fallible<()> { - self.subcommand.run(&self) - } - - fn get_replica(&self) -> Replica { - Replica::new(Box::new( - taskstorage::KVStorage::new(Path::new("/tmp/tasks")).unwrap(), - )) - } -} +pub use shared::CommandInvocation; diff --git a/cli/src/cmd/shared.rs b/cli/src/cmd/shared.rs index 4e24bfcc9..411c85f25 100644 --- a/cli/src/cmd/shared.rs +++ b/cli/src/cmd/shared.rs @@ -1,6 +1,7 @@ use clap::Arg; use failure::{format_err, Fallible}; -use taskchampion::{Replica, Task, Uuid}; +use std::path::Path; +use taskchampion::{server, taskstorage, Replica, Task, Uuid}; pub(super) fn task_arg<'a>() -> Arg<'a, 'a> { Arg::with_name("task") @@ -26,3 +27,31 @@ pub(super) fn get_task>(replica: &mut Replica, task_arg: S) -> Fal Err(format_err!("Cannot interpret {:?} as a task", task_arg)) } + +/// A command invocation contains all of the necessary regarding a single invocation of the CLI. +#[derive(Debug)] +pub struct CommandInvocation { + pub(crate) subcommand: Box, +} + +impl CommandInvocation { + pub(crate) fn new(subcommand: Box) -> Self { + Self { subcommand } + } + + pub fn run(self) -> Fallible<()> { + self.subcommand.run(&self) + } + + // -- utilities for command invocations + + pub(super) fn get_replica(&self) -> Replica { + Replica::new(Box::new( + taskstorage::KVStorage::new(Path::new("/tmp/tasks")).unwrap(), + )) + } + + pub(super) fn get_server(&self) -> impl server::Server { + server::LocalServer::new() + } +} diff --git a/cli/src/cmd/sync.rs b/cli/src/cmd/sync.rs new file mode 100644 index 000000000..f968ecf39 --- /dev/null +++ b/cli/src/cmd/sync.rs @@ -0,0 +1,39 @@ +use clap::{App, ArgMatches, SubCommand as ClapSubCommand}; +use failure::Fallible; + +use crate::cmd::{ArgMatchResult, CommandInvocation}; + +#[derive(Debug)] +struct Invocation {} + +define_subcommand! { + fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> { + app.subcommand(ClapSubCommand::with_name("sync").about("sync with the server")) + } + + fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult { + match matches.subcommand() { + ("sync", _) => ArgMatchResult::Ok(Box::new(Invocation {})), + _ => ArgMatchResult::None, + } + } +} + +subcommand_invocation! { + fn run(&self, command: &CommandInvocation) -> Fallible<()> { + let mut replica = command.get_replica(); + let mut server = command.get_server(); + replica.sync(&mut server)?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_command() { + with_subcommand_invocation!(vec!["task", "sync"], |_inv| {}); + } +} diff --git a/taskchampion/src/server/local.rs b/taskchampion/src/server/local.rs new file mode 100644 index 000000000..30999496a --- /dev/null +++ b/taskchampion/src/server/local.rs @@ -0,0 +1,79 @@ +use crate::server::{ + AddVersionResult, GetVersionResult, HistorySegment, Server, VersionId, NO_VERSION_ID, +}; +use failure::Fallible; +use std::collections::HashMap; +use uuid::Uuid; + +struct Version { + version_id: VersionId, + parent_version_id: VersionId, + history_segment: HistorySegment, +} + +pub struct LocalServer { + latest_version_id: VersionId, + // NOTE: indexed by parent_version_id! + versions: HashMap, +} + +impl LocalServer { + /// A test server has no notion of clients, signatures, encryption, etc. + pub fn new() -> LocalServer { + LocalServer { + latest_version_id: NO_VERSION_ID, + versions: HashMap::new(), + } + } +} + +impl Server for LocalServer { + /// Add a new version. If the given version number is incorrect, this responds with the + /// appropriate version and expects the caller to try again. + fn add_version( + &mut self, + parent_version_id: VersionId, + history_segment: HistorySegment, + ) -> Fallible { + // no client lookup + // no signature validation + + // check the parent_version_id for linearity + if self.latest_version_id != NO_VERSION_ID { + if parent_version_id != self.latest_version_id { + return Ok(AddVersionResult::ExpectedParentVersion( + self.latest_version_id, + )); + } + } + + // invent a new ID for this version + let version_id = Uuid::new_v4(); + + self.versions.insert( + parent_version_id, + Version { + version_id, + parent_version_id, + history_segment, + }, + ); + self.latest_version_id = version_id; + + Ok(AddVersionResult::Ok(version_id)) + } + + /// Get a vector of all versions after `since_version` + fn get_child_version(&self, parent_version_id: VersionId) -> Fallible { + if let Some(version) = self.versions.get(&parent_version_id) { + Ok(GetVersionResult::Version { + version_id: version.version_id, + parent_version_id: version.parent_version_id, + history_segment: version.history_segment.clone(), + }) + } else { + Ok(GetVersionResult::NoSuchVersion) + } + } +} + diff --git a/taskchampion/src/server/mod.rs b/taskchampion/src/server/mod.rs index 06009b127..0331bb63c 100644 --- a/taskchampion/src/server/mod.rs +++ b/taskchampion/src/server/mod.rs @@ -1,7 +1,9 @@ #[cfg(test)] pub(crate) mod test; +mod local; mod signing; mod types; +pub use local::LocalServer; pub use types::*;