refactor sync-server into a lib crate with a binary

This commit is contained in:
Dustin J. Mitchell
2021-09-07 02:44:38 +00:00
parent 4690cf7fc8
commit ebcf9527dc
8 changed files with 93 additions and 82 deletions

View File

@@ -77,9 +77,8 @@ pub(crate) async fn service(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::api::ServerState;
use crate::app_scope;
use crate::storage::{InMemoryStorage, Storage}; use crate::storage::{InMemoryStorage, Storage};
use crate::Server;
use actix_web::{http::StatusCode, test, App}; use actix_web::{http::StatusCode, test, App};
use uuid::Uuid; use uuid::Uuid;
@@ -88,16 +87,16 @@ mod test {
let client_key = Uuid::new_v4(); let client_key = Uuid::new_v4();
let version_id = Uuid::new_v4(); let version_id = Uuid::new_v4();
let parent_version_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4();
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new()); let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
// set up the storage contents.. // set up the storage contents..
{ {
let mut txn = server_box.txn().unwrap(); let mut txn = storage.txn().unwrap();
txn.new_client(client_key, Uuid::nil()).unwrap(); txn.new_client(client_key, Uuid::nil()).unwrap();
} }
let server_state = ServerState::new(server_box); let server = Server::new(storage);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await; let mut app = test::init_service(App::new().service(server.service())).await;
let uri = format!("/v1/client/add-version/{}", parent_version_id); let uri = format!("/v1/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post() let req = test::TestRequest::post()
@@ -125,16 +124,16 @@ mod test {
let client_key = Uuid::new_v4(); let client_key = Uuid::new_v4();
let version_id = Uuid::new_v4(); let version_id = Uuid::new_v4();
let parent_version_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4();
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new()); let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
// set up the storage contents.. // set up the storage contents..
{ {
let mut txn = server_box.txn().unwrap(); let mut txn = storage.txn().unwrap();
txn.new_client(client_key, version_id).unwrap(); txn.new_client(client_key, version_id).unwrap();
} }
let server_state = ServerState::new(server_box); let server = Server::new(storage);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await; let mut app = test::init_service(App::new().service(server.service())).await;
let uri = format!("/v1/client/add-version/{}", parent_version_id); let uri = format!("/v1/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post() let req = test::TestRequest::post()
@@ -159,9 +158,9 @@ mod test {
async fn test_bad_content_type() { async fn test_bad_content_type() {
let client_key = Uuid::new_v4(); let client_key = Uuid::new_v4();
let parent_version_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4();
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new()); let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
let server_state = ServerState::new(server_box); let server = Server::new(storage);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await; let mut app = test::init_service(App::new().service(server.service())).await;
let uri = format!("/v1/client/add-version/{}", parent_version_id); let uri = format!("/v1/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post() let req = test::TestRequest::post()
@@ -178,9 +177,9 @@ mod test {
async fn test_empty_body() { async fn test_empty_body() {
let client_key = Uuid::new_v4(); let client_key = Uuid::new_v4();
let parent_version_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4();
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new()); let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
let server_state = ServerState::new(server_box); let server = Server::new(storage);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await; let mut app = test::init_service(App::new().service(server.service())).await;
let uri = format!("/v1/client/add-version/{}", parent_version_id); let uri = format!("/v1/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post() let req = test::TestRequest::post()

View File

@@ -45,8 +45,8 @@ pub(crate) async fn service(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::api::ServerState; use crate::api::ServerState;
use crate::app_scope;
use crate::storage::{InMemoryStorage, Storage}; use crate::storage::{InMemoryStorage, Storage};
use crate::Server;
use actix_web::{http::StatusCode, test, App}; use actix_web::{http::StatusCode, test, App};
use uuid::Uuid; use uuid::Uuid;
@@ -55,18 +55,18 @@ mod test {
let client_key = Uuid::new_v4(); let client_key = Uuid::new_v4();
let version_id = Uuid::new_v4(); let version_id = Uuid::new_v4();
let parent_version_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4();
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new()); let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
// set up the storage contents.. // set up the storage contents..
{ {
let mut txn = server_box.txn().unwrap(); let mut txn = storage.txn().unwrap();
txn.new_client(client_key, Uuid::new_v4()).unwrap(); txn.new_client(client_key, Uuid::new_v4()).unwrap();
txn.add_version(client_key, version_id, parent_version_id, b"abcd".to_vec()) txn.add_version(client_key, version_id, parent_version_id, b"abcd".to_vec())
.unwrap(); .unwrap();
} }
let server_state = ServerState::new(server_box); let server = Server::new(storage);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await; let mut app = test::init_service(App::new().service(server.service())).await;
let uri = format!("/v1/client/get-child-version/{}", parent_version_id); let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
let req = test::TestRequest::get() let req = test::TestRequest::get()
@@ -97,9 +97,9 @@ mod test {
async fn test_client_not_found() { async fn test_client_not_found() {
let client_key = Uuid::new_v4(); let client_key = Uuid::new_v4();
let parent_version_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4();
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new()); let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
let server_state = ServerState::new(server_box); let server = Server::new(storage);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await; let mut app = test::init_service(App::new().service(server.service())).await;
let uri = format!("/v1/client/get-child-version/{}", parent_version_id); let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
let req = test::TestRequest::get() let req = test::TestRequest::get()
@@ -116,15 +116,15 @@ mod test {
async fn test_version_not_found() { async fn test_version_not_found() {
let client_key = Uuid::new_v4(); let client_key = Uuid::new_v4();
let parent_version_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4();
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new()); let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
// create the client, but not the version // create the client, but not the version
{ {
let mut txn = server_box.txn().unwrap(); let mut txn = storage.txn().unwrap();
txn.new_client(client_key, Uuid::new_v4()).unwrap(); txn.new_client(client_key, Uuid::new_v4()).unwrap();
} }
let server_state = ServerState::new(server_box); let server = Server::new(storage);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await; let mut app = test::init_service(App::new().service(server.service())).await;
let uri = format!("/v1/client/get-child-version/{}", parent_version_id); let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
let req = test::TestRequest::get() let req = test::TestRequest::get()

View File

@@ -20,7 +20,7 @@ pub(crate) const CLIENT_KEY_HEADER: &str = "X-Client-Key";
pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id"; pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id";
/// The type containing a reference to the Storage object in the Actix state. /// The type containing a reference to the Storage object in the Actix state.
pub(crate) type ServerState = Arc<Box<dyn Storage>>; pub(crate) type ServerState = Arc<dyn Storage>;
pub(crate) fn api_scope() -> Scope { pub(crate) fn api_scope() -> Scope {
web::scope("") web::scope("")

View File

@@ -1,29 +1,9 @@
#![deny(clippy::all)] #![deny(clippy::all)]
use crate::storage::{SqliteStorage, Storage}; use actix_web::{middleware::Logger, App, HttpServer};
use actix_web::{get, middleware::Logger, web, App, HttpServer, Responder, Scope};
use api::{api_scope, ServerState};
use clap::Arg; use clap::Arg;
use taskchampion_sync_server::storage::SqliteStorage;
mod api; use taskchampion_sync_server::Server;
mod server;
mod storage;
// TODO: use hawk to sign requests
#[get("/")]
async fn index() -> impl Responder {
format!("TaskChampion sync server v{}", env!("CARGO_PKG_VERSION"))
}
/// Return a scope defining the URL rules for this server, with access to
/// the given ServerState.
pub(crate) fn app_scope(server_state: ServerState) -> Scope {
web::scope("")
.data(server_state)
.service(index)
.service(api_scope())
}
#[actix_web::main] #[actix_web::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
@@ -56,33 +36,26 @@ async fn main() -> anyhow::Result<()> {
let data_dir = matches.value_of("data-dir").unwrap(); let data_dir = matches.value_of("data-dir").unwrap();
let port = matches.value_of("port").unwrap(); let port = matches.value_of("port").unwrap();
let server_box: Box<dyn Storage> = Box::new(SqliteStorage::new(data_dir)?); let server = Server::new(Box::new(SqliteStorage::new(data_dir)?));
let server_state = ServerState::new(server_box);
log::warn!("Serving on port {}", port); log::warn!("Serving on port {}", port);
HttpServer::new(move || { HttpServer::new(move || App::new().wrap(Logger::default()).service(server.service()))
App::new() .bind(format!("0.0.0.0:{}", port))?
.wrap(Logger::default()) .run()
.service(app_scope(server_state.clone())) .await?;
})
.bind(format!("0.0.0.0:{}", port))?
.run()
.await?;
Ok(()) Ok(())
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::api::ServerState;
use crate::storage::{InMemoryStorage, Storage};
use actix_web::{test, App}; use actix_web::{test, App};
use taskchampion_sync_server::storage::{InMemoryStorage, Storage};
#[actix_rt::test] #[actix_rt::test]
async fn test_index_get() { async fn test_index_get() {
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new()); let server = Server::new(Box::new(InMemoryStorage::new()));
let server_state = ServerState::new(server_box); let mut app = test::init_service(App::new().service(server.service())).await;
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
let req = test::TestRequest::get().uri("/").to_request(); let req = test::TestRequest::get().uri("/").to_request();
let resp = test::call_service(&mut app, req).await; let resp = test::call_service(&mut app, req).await;

37
sync-server/src/lib.rs Normal file
View File

@@ -0,0 +1,37 @@
#![deny(clippy::all)]
mod api;
mod server;
pub mod storage;
use crate::storage::Storage;
use actix_web::{get, web, Responder, Scope};
use api::{api_scope, ServerState};
#[get("/")]
async fn index() -> impl Responder {
format!("TaskChampion sync server v{}", env!("CARGO_PKG_VERSION"))
}
/// A Server represents a sync server.
#[derive(Clone)]
pub struct Server {
storage: ServerState,
}
impl Server {
/// Create a new sync server with the given storage implementation.
pub fn new(storage: Box<dyn Storage>) -> Self {
Self {
storage: storage.into(),
}
}
/// Get an Actix-web service for this server.
pub fn service(&self) -> Scope {
web::scope("")
.data(self.storage.clone())
.service(index)
.service(api_scope())
}
}

View File

@@ -10,10 +10,11 @@ struct Inner {
versions: HashMap<(Uuid, Uuid), Version>, versions: HashMap<(Uuid, Uuid), Version>,
} }
pub(crate) struct InMemoryStorage(Mutex<Inner>); pub struct InMemoryStorage(Mutex<Inner>);
impl InMemoryStorage { impl InMemoryStorage {
pub(crate) fn new() -> Self { #[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self(Mutex::new(Inner { Self(Mutex::new(Inner {
clients: HashMap::new(), clients: HashMap::new(),
versions: HashMap::new(), versions: HashMap::new(),

View File

@@ -1,27 +1,28 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
#[cfg(test)] #[cfg(debug_assertions)]
mod inmemory; mod inmemory;
#[cfg(test)]
pub(crate) use inmemory::InMemoryStorage; #[cfg(debug_assertions)]
pub use inmemory::InMemoryStorage;
mod sqlite; mod sqlite;
pub(crate) use self::sqlite::SqliteStorage; pub use self::sqlite::SqliteStorage;
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub(crate) struct Client { pub struct Client {
pub(crate) latest_version_id: Uuid, pub latest_version_id: Uuid,
} }
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub(crate) struct Version { pub struct Version {
pub(crate) version_id: Uuid, pub version_id: Uuid,
pub(crate) parent_version_id: Uuid, pub parent_version_id: Uuid,
pub(crate) history_segment: Vec<u8>, pub history_segment: Vec<u8>,
} }
pub(crate) trait StorageTxn { pub trait StorageTxn {
/// Get information about the given client /// Get information about the given client
fn get_client(&mut self, client_key: Uuid) -> anyhow::Result<Option<Client>>; fn get_client(&mut self, client_key: Uuid) -> anyhow::Result<Option<Client>>;
@@ -58,7 +59,7 @@ pub(crate) trait StorageTxn {
/// A trait for objects able to act as storage. Most of the interesting behavior is in the /// A trait for objects able to act as storage. Most of the interesting behavior is in the
/// [`crate::storage::StorageTxn`] trait. /// [`crate::storage::StorageTxn`] trait.
pub(crate) trait Storage: Send + Sync { pub trait Storage: Send + Sync {
/// Begin a transaction /// Begin a transaction
fn txn<'a>(&'a self) -> anyhow::Result<Box<dyn StorageTxn + 'a>>; fn txn<'a>(&'a self) -> anyhow::Result<Box<dyn StorageTxn + 'a>>;
} }

View File

@@ -49,7 +49,7 @@ impl ToSql for Client {
} }
/// An on-disk storage backend which uses SQLite /// An on-disk storage backend which uses SQLite
pub(crate) struct SqliteStorage { pub struct SqliteStorage {
db_file: std::path::PathBuf, db_file: std::path::PathBuf,
} }