From 0cd1f8a799dbfa50868034fe0b6ae04b587cbd25 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 23 Dec 2023 18:49:13 +0000 Subject: [PATCH] Make the sync server client an optional feature --- .github/workflows/rust-tests.yml | 38 ++++++++++++++++++- taskchampion/integration-tests/Cargo.toml | 2 +- taskchampion/taskchampion/Cargo.toml | 11 ++++++ taskchampion/taskchampion/src/errors.rs | 1 + taskchampion/taskchampion/src/lib.rs | 8 ++++ .../taskchampion/src/server/config.rs | 9 ++++- .../taskchampion/src/server/crypto.rs | 4 +- .../src/server/{local.rs => local/mod.rs} | 0 taskchampion/taskchampion/src/server/mod.rs | 10 +++-- .../src/server/{remote => sync}/mod.rs | 19 ++++------ taskchampion/taskchampion/src/server/types.rs | 7 +++- 11 files changed, 86 insertions(+), 23 deletions(-) rename taskchampion/taskchampion/src/server/{local.rs => local/mod.rs} (100%) rename taskchampion/taskchampion/src/server/{remote => sync}/mod.rs (93%) diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml index d7eb0b748..af821ab26 100644 --- a/.github/workflows/rust-tests.yml +++ b/.github/workflows/rust-tests.yml @@ -1,5 +1,3 @@ -## Run the TaskChampion tests, using both the minimum supported rust version -## and the latest stable Rust. name: tests - rust @@ -11,6 +9,42 @@ on: types: [opened, reopened, synchronize] jobs: + ## Run the `taskchampion` crate's tests with various combinations of features. + features: + strategy: + matrix: + features: + - "" + - "server-sync" + + name: "taskchampion ${{ matrix.features == '' && 'with no features' || format('with features {0}', matrix.features) }}" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Cache cargo registry + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: ubuntu-latest-stable-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v3 + with: + path: target + key: ubuntu-latest-stable-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: test + run: cargo test -p taskchampion --no-default-features --features "${{ matrix.features }}" + + ## Run all TaskChampion crate tests, using both the minimum supported rust version + ## and the latest stable Rust. test: strategy: matrix: diff --git a/taskchampion/integration-tests/Cargo.toml b/taskchampion/integration-tests/Cargo.toml index 9127faa54..867efcda5 100644 --- a/taskchampion/integration-tests/Cargo.toml +++ b/taskchampion/integration-tests/Cargo.toml @@ -7,7 +7,7 @@ publish = false build = "build.rs" [dependencies] -taskchampion = { path = "../taskchampion" } +taskchampion = { path = "../taskchampion", features = ["server-sync"] } taskchampion-lib = { path = "../lib" } taskchampion-sync-server = { path = "../sync-server" } diff --git a/taskchampion/taskchampion/Cargo.toml b/taskchampion/taskchampion/Cargo.toml index e96612a8c..eef7ff7ca 100644 --- a/taskchampion/taskchampion/Cargo.toml +++ b/taskchampion/taskchampion/Cargo.toml @@ -10,6 +10,14 @@ readme = "../README.md" license = "MIT" edition = "2018" +[features] +default = ["server-sync" ] +server-sync = ["crypto", "dep:ureq"] +crypto = ["dep:ring"] + +[package.metadata.docs.rs] +all-features = true + [dependencies] uuid.workspace = true serde.workspace = true @@ -26,6 +34,9 @@ flate2.workspace = true byteorder.workspace = true ring.workspace = true +ureq.optional = true +ring.optional = true + [dev-dependencies] proptest.workspace = true tempfile.workspace = true diff --git a/taskchampion/taskchampion/src/errors.rs b/taskchampion/taskchampion/src/errors.rs index 21e44e33a..eab0b71c9 100644 --- a/taskchampion/taskchampion/src/errors.rs +++ b/taskchampion/taskchampion/src/errors.rs @@ -34,6 +34,7 @@ macro_rules! other_error { } }; } +#[cfg(feature = "server-sync")] other_error!(ureq::Error); other_error!(io::Error); other_error!(serde_json::Error); diff --git a/taskchampion/taskchampion/src/lib.rs b/taskchampion/taskchampion/src/lib.rs index 9682747bb..7cc7c20fa 100644 --- a/taskchampion/taskchampion/src/lib.rs +++ b/taskchampion/taskchampion/src/lib.rs @@ -34,6 +34,14 @@ Create a server with [`ServerConfig`](crate::ServerConfig). The [`server`](crate::server) module defines the interface a server must meet. Users can define their own server impelementations. +# Feature Flags + +Support for some optional functionality is controlled by feature flags. + +Sync server client support: + + * `server-sync` - sync to the taskchampion-sync-server + # See Also See the [TaskChampion Book](http://taskchampion.github.com/taskchampion) diff --git a/taskchampion/taskchampion/src/server/config.rs b/taskchampion/taskchampion/src/server/config.rs index 5b15a6d63..0d2b6ab07 100644 --- a/taskchampion/taskchampion/src/server/config.rs +++ b/taskchampion/taskchampion/src/server/config.rs @@ -1,7 +1,10 @@ use super::types::Server; -use super::{LocalServer, RemoteServer}; use crate::errors::Result; +use crate::server::local::LocalServer; +#[cfg(feature = "server-sync")] +use crate::server::sync::SyncServer; use std::path::PathBuf; +#[cfg(feature = "server-sync")] use uuid::Uuid; /// The configuration for a replica's access to a sync server. @@ -12,6 +15,7 @@ pub enum ServerConfig { server_dir: PathBuf, }, /// A remote taskchampion-sync-server instance + #[cfg(feature = "server-sync")] Remote { /// Sync server "origin"; a URL with schema and hostname but no path or trailing `/` origin: String, @@ -30,11 +34,12 @@ impl ServerConfig { pub fn into_server(self) -> Result> { Ok(match self { ServerConfig::Local { server_dir } => Box::new(LocalServer::new(server_dir)?), + #[cfg(feature = "server-sync")] ServerConfig::Remote { origin, client_id, encryption_secret, - } => Box::new(RemoteServer::new(origin, client_id, encryption_secret)?), + } => Box::new(SyncServer::new(origin, client_id, encryption_secret)?), }) } } diff --git a/taskchampion/taskchampion/src/server/crypto.rs b/taskchampion/taskchampion/src/server/crypto.rs index 5d8a0e328..f5567b972 100644 --- a/taskchampion/taskchampion/src/server/crypto.rs +++ b/taskchampion/taskchampion/src/server/crypto.rs @@ -1,8 +1,8 @@ +#![allow(dead_code)] // temporary /// This module implements the encryption specified in the sync-protocol /// document. use crate::errors::{Error, Result}; use ring::{aead, digest, pbkdf2, rand, rand::SecureRandom}; -use std::io::Read; use uuid::Uuid; const PBKDF2_ITERATIONS: u32 = 100000; @@ -177,11 +177,13 @@ pub(super) struct Sealed { } impl Sealed { + #[cfg(feature = "server-sync")] pub(super) fn from_resp( resp: ureq::Response, version_id: Uuid, content_type: &str, ) -> Result { + use std::io::Read; if resp.header("Content-Type") == Some(content_type) { let mut reader = resp.into_reader(); let mut payload = vec![]; diff --git a/taskchampion/taskchampion/src/server/local.rs b/taskchampion/taskchampion/src/server/local/mod.rs similarity index 100% rename from taskchampion/taskchampion/src/server/local.rs rename to taskchampion/taskchampion/src/server/local/mod.rs diff --git a/taskchampion/taskchampion/src/server/mod.rs b/taskchampion/taskchampion/src/server/mod.rs index f97b9181b..a80675d4d 100644 --- a/taskchampion/taskchampion/src/server/mod.rs +++ b/taskchampion/taskchampion/src/server/mod.rs @@ -12,15 +12,17 @@ However, users who wish to implement their own server interfaces can implement t pub(crate) mod test; mod config; -mod crypto; mod local; mod op; -mod remote; mod types; +#[cfg(feature = "crypto")] +mod crypto; + +#[cfg(feature = "server-sync")] +mod sync; + pub use config::ServerConfig; -pub use local::LocalServer; -pub use remote::RemoteServer; pub use types::*; pub(crate) use op::SyncOp; diff --git a/taskchampion/taskchampion/src/server/remote/mod.rs b/taskchampion/taskchampion/src/server/sync/mod.rs similarity index 93% rename from taskchampion/taskchampion/src/server/remote/mod.rs rename to taskchampion/taskchampion/src/server/sync/mod.rs index 6b168ab32..6c08bfb9a 100644 --- a/taskchampion/taskchampion/src/server/remote/mod.rs +++ b/taskchampion/taskchampion/src/server/sync/mod.rs @@ -8,7 +8,7 @@ use uuid::Uuid; use super::crypto::{Cryptor, Sealed, Secret, Unsealed}; -pub struct RemoteServer { +pub struct SyncServer { origin: String, client_id: Uuid, cryptor: Cryptor, @@ -21,19 +21,14 @@ const HISTORY_SEGMENT_CONTENT_TYPE: &str = "application/vnd.taskchampion.history /// The content-type for snapshots (opaque blobs of bytes) const SNAPSHOT_CONTENT_TYPE: &str = "application/vnd.taskchampion.snapshot"; -/// A RemoeServer communicates with a remote server over HTTP (such as with -/// taskchampion-sync-server). -impl RemoteServer { - /// Construct a new RemoteServer. The `origin` is the sync server's protocol and hostname +/// A RemoeServer communicates with a taskchampion-sync-server over HTTP . +impl SyncServer { + /// Construct a new SyncServer. The `origin` is the sync server's protocol and hostname /// without a trailing slash, such as `https://tcsync.example.com`. Pass a client_id to /// identify this client to the server. Multiple replicas synchronizing the same task history /// should use the same client_id. - pub fn new( - origin: String, - client_id: Uuid, - encryption_secret: Vec, - ) -> Result { - Ok(RemoteServer { + pub fn new(origin: String, client_id: Uuid, encryption_secret: Vec) -> Result { + Ok(SyncServer { origin, client_id, cryptor: Cryptor::new(client_id, &Secret(encryption_secret.to_vec()))?, @@ -67,7 +62,7 @@ fn get_snapshot_urgency(resp: &ureq::Response) -> SnapshotUrgency { } } -impl Server for RemoteServer { +impl Server for SyncServer { fn add_version( &mut self, parent_version_id: VersionId, diff --git a/taskchampion/taskchampion/src/server/types.rs b/taskchampion/taskchampion/src/server/types.rs index 5588a720e..8929ae23b 100644 --- a/taskchampion/taskchampion/src/server/types.rs +++ b/taskchampion/taskchampion/src/server/types.rs @@ -1,7 +1,7 @@ use crate::errors::Result; use uuid::Uuid; -/// Versions are referred to with sha2 hashes. +/// Versions are referred to with UUIDs. pub type VersionId = Uuid; /// The distinguished value for "no version" @@ -52,6 +52,11 @@ pub enum GetVersionResult { /// A value implementing this trait can act as a server against which a replica can sync. pub trait Server { /// Add a new version. + /// + /// This must ensure that the new version is the only version with the given + /// `parent_version_id`, and that all versions form a single parent-child chain. Inductively, + /// this means that if there are any versions on the server, then `parent_version_id` must be + /// the only version that does not already have a child. fn add_version( &mut self, parent_version_id: VersionId,