morethantext-web/src/morethantext/mod-3.rs

641 lines
16 KiB
Rust

use async_std::{
channel::{unbounded, Receiver, Sender},
path::PathBuf,
task::spawn,
};
use std::{collections::HashMap, error::Error, fmt};
const ENTRY: &str = "EntryPoint";
#[derive(Debug)]
enum ErrorCode {
// General
Undefined(String),
// Cache
EntryNotFound(String),
InvalidCommitData,
// Store
DatabaseAlreadyExists(String),
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorCode::Undefined(msg) => write!(f, "{}", msg),
ErrorCode::EntryNotFound(id) => write!(f, "entry '{}' was not found", id),
ErrorCode::InvalidCommitData => write!(f, "commit data was not a database store"),
ErrorCode::DatabaseAlreadyExists(name) => {
write!(f, "database '{}' already exists", name)
}
}
}
}
mod errorcodes {
use super::*;
const ITEMS: [&str; 2] = ["one", "two"];
#[test]
fn undefined_display() {
for item in ITEMS {
let err = ErrorCode::Undefined(item.to_string());
assert_eq!(err.to_string(), item);
}
}
#[test]
fn bad_entry() {
for item in ITEMS {
let err = ErrorCode::EntryNotFound(item.to_string());
assert_eq!(err.to_string(), format!("entry '{}' was not found", item));
}
}
#[test]
fn invalid_commit_data() {
let err = ErrorCode::InvalidCommitData;
assert_eq!(err.to_string(), "commit data was not a database store");
}
#[test]
fn database_already_exists() {
for item in ITEMS {
let err = ErrorCode::DatabaseAlreadyExists(item.to_string());
assert_eq!(
err.to_string(),
format!("database '{}' already exists", item)
);
}
}
}
#[derive(Debug)]
pub struct MTTError {
code: ErrorCode,
}
impl MTTError {
fn new<S>(msg: S) -> Self
where
S: Into<String>,
{
let text = msg.into();
Self {
code: ErrorCode::Undefined(text),
}
}
fn from_code(code: ErrorCode) -> Self {
Self { code: code }
}
}
impl Error for MTTError {}
impl fmt::Display for MTTError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.code)
}
}
#[cfg(test)]
mod errors {
use super::*;
#[test]
fn create_with_str() {
let msgs = ["one", "two"];
for msg in msgs {
let err = MTTError::new(msg);
assert_eq!(err.to_string(), msg);
}
}
#[test]
fn create_with_string() {
let msg = "three";
let err = MTTError::new(msg.to_string());
assert_eq!(err.to_string(), msg);
}
#[test]
fn create_from_code() {
let code = ErrorCode::Undefined("oops".to_string());
let err = MTTError::from_code(code);
match err.code {
ErrorCode::Undefined(_) => (),
_ => assert!(false, "{:?} is not undefined", err.code),
}
}
#[test]
fn create_missing_entry() {
let code = ErrorCode::EntryNotFound("an_id".to_string());
let err = MTTError::from_code(code);
match err.code {
ErrorCode::EntryNotFound(_) => (),
_ => assert!(false, "{:?} is not undefined", err.code),
}
}
}
#[derive(Clone, Debug)]
struct Storage<D> {
id: Option<String>,
data: Option<D>,
// delete: bool,
}
impl Storage<D> {
fn from_id<S>(id: S) -> Self
where
S: Into<String>,
{
Self {
id: Some(id.into()),
data: None,
}
}
fn from_datatype(dt: DataType) -> Self {
Self {
id: None,
data: Some(dt),
}
}
}
#[cfg(test)]
mod storage {
use super::*;
#[test]
fn from_id_with_str() {
let ids = ["first", "second"];
for id in ids {
let output = Storage::from_id(id);
assert_eq!(output.id, Some(id.to_string()));
assert!(
output.data.is_none(),
"The storage data should have been Non."
);
}
}
#[test]
fn from_id_with_string() {
let id = "my_id".to_string();
let output = Storage::from_id(id.clone());
assert_eq!(output.id, Some(id));
}
#[test]
fn from_store() {
let output = Storage::from_datatype(DataType::new("store"));
assert!(output.id.is_none(), "id should be None.");
assert!(output.data.is_some(), "There should be data");
let result = output.data.unwrap();
match result {
DataType::DBMap(_) => (),
_ => assert!(false, "{:?} should have been DataType::DBMap.", result),
}
}
#[test]
fn from_database() {
let output = Storage::from_datatype(DataType::new("database"));
let result = output.data.unwrap();
match result {
DataType::TableMap(_) => (),
_ => assert!(false, "{:?} should have been DataType::TableMap.", result),
}
}
}
#[derive(Clone, Debug)]
struct Store {
data: HashMap<String, Storage<Database>>,
}
impl Store {
fn new() -> Self {
Self {
data: HashMap::new(),
}
}
fn add_new<S>(&mut self, name: S) -> Result<(), MTTError>
where
S: Into<String>,
{
let dbname = name.into();
match self.get(&dbname) {
Some(_) => Err(MTTError::from_code(ErrorCode::DatabaseAlreadyExists(
dbname,
))),
None => {
self.data
.insert(dbname, Storage::from_datatype(DataType::new("database")));
Ok(())
}
}
}
fn get(&self, name: &str) -> Option<&Storage<Database>> {
self.data.get(name)
}
}
#[cfg(test)]
mod stores {
use super::*;
#[test]
fn get_no_database() -> Result<(), MTTError> {
let store = Store::new();
match store.get("missing_name") {
Some(_) => Err(MTTError::new("should have returned None")),
None => Ok(()),
}
}
#[test]
fn add_database_str() {
let mut store = Store::new();
let names = ["first", "second"];
for name in names {
store.add_new(name).unwrap();
let output = store.get(name).unwrap();
assert!(output.data.is_some(), "There should be a data type.");
match output.data.clone().unwrap() {
DataType::TableMap(_) => (),
_ => assert!(
false,
"{:?} should have been DataType::TableMap.",
output.data
),
}
assert!(output.id.is_none(), "Should not have an id.");
}
}
#[test]
fn add_database_string() {
let mut store = Store::new();
let name = "third".to_string();
store.add_new(name.clone()).unwrap();
let output = store.get(&name).unwrap();
match output.data.clone().unwrap() {
DataType::TableMap(_) => (),
_ => assert!(
false,
"{:?} should have been DataType::TableMap.",
output.data
),
}
}
#[test]
fn no_duplicate_database_names() -> Result<(), MTTError> {
let mut store = Store::new();
let name = "duplicate";
store.add_new(name).unwrap();
match store.add_new(name) {
Ok(_) => Err(MTTError::new("should have been an error")),
Err(err) => match err.code {
ErrorCode::DatabaseAlreadyExists(dbname) => {
assert_eq!(dbname, name);
Ok(())
}
_ => Err(MTTError::new(format!(
"{:?} should have been DatabaseAlreadyExists.",
err.code
))),
},
}
}
}
#[derive(Clone, Debug)]
struct Database;
#[cfg(test)]
mod databases {
use super::*;
#[test]
fn create() {
Database::new();
}
}
impl Database {
fn new() -> Self {
Self {}
}
}
#[derive(Clone, Debug)]
enum DataType {
DBMap(Store),
TableMap(Database),
}
impl DataType {
fn new(dtype: &str) -> DataType {
match dtype {
"store" => Self::DBMap(Store::new()),
"database" => Self::TableMap(Database::new()),
_ => unreachable!(),
}
}
}
#[cfg(test)]
mod datatypes {
use super::*;
#[test]
fn create_store() {
let dtype = DataType::new("store");
match dtype {
DataType::DBMap(_) => (),
_ => assert!(false, "{:?} is not incorrect data type", dtype),
}
}
#[test]
fn create_database() {
let dtype = DataType::new("database");
match dtype {
DataType::TableMap(_) => (),
_ => assert!(false, "{:?} is not incorrect data type", dtype),
}
}
}
#[derive(Debug)]
enum FromCache {
Ok,
Data(HashMap<String, DataType>),
Error(MTTError),
}
struct CacheQuery {
ids: Vec<String>,
reply: Sender<FromCache>,
}
struct CacheCommit {
reply: Sender<FromCache>,
data: DataType,
}
impl CacheCommit {
fn new(data: DataType, channel: Sender<FromCache>) -> Result<Self, MTTError> {
match data {
DataType::DBMap(_) => (),
_ => return Err(MTTError::from_code(ErrorCode::InvalidCommitData)),
}
Ok(Self {
data: data,
reply: channel,
})
}
}
mod commits {
use super::*;
#[test]
fn create() -> Result<(), MTTError> {
let (s, _) = unbounded();
match CacheCommit::new(DataType::new("store"), s) {
Ok(output) => match output.data {
DataType::DBMap(_) => Ok(()),
_ => Err(MTTError::new(format!(
"{:?} should have been DBMap",
output.data
))),
},
Err(err) => Err(err),
}
}
#[test]
fn bad_data_type() -> Result<(), MTTError> {
let (s, _) = unbounded();
match CacheCommit::new(DataType::new("database"), s) {
Ok(_) => Err(MTTError::new("CacheCommit::new did not return error")),
Err(err) => match err.code {
ErrorCode::InvalidCommitData => Ok(()),
_ => Err(MTTError::new(format!(
"{:?} is not the correct error",
err.code
))),
},
}
}
}
enum ToCache {
Query(CacheQuery),
Commit(CacheCommit),
}
#[derive(Clone)]
pub struct MoreThanText {
session: Vec<String>,
cache: Sender<Vec<String>>,
}
impl MoreThanText {
async fn new(cache: Sender<Vec<String>>) -> Result<Self, MTTError> {
Ok(Self {
session: [ENTRY.to_string()].to_vec(),
cache: cache,
})
}
}
#[cfg(test)]
mod mtt {
use super::*;
#[async_std::test]
async fn create() {
let (s, _) = unbounded();
let mtt = MoreThanText::new(s).await.unwrap();
assert_eq!(mtt.session, [ENTRY]);
}
}
struct Cache;
impl Cache {
async fn new<P>(_dir: P) -> Result<Self, MTTError>
where
P: Into<PathBuf>,
{
Ok(Self {})
}
async fn query(&self, qry: &Vec<String>) -> Result<HashMap<String, DataType>, MTTError> {
let mut output = HashMap::new();
for id in qry {
if id == ENTRY {
output.insert(ENTRY.to_string(), DataType::new("store"));
} else {
return Err(MTTError::from_code(ErrorCode::EntryNotFound(
id.to_string(),
)));
}
}
Ok(output)
}
async fn commit(&self) -> Result<(), MTTError> {
Ok(())
}
async fn start(&self, listener: Receiver<ToCache>) {
loop {
match listener.recv().await.unwrap() {
ToCache::Query(qry) => match self.query(&qry.ids).await {
Ok(data) => qry.reply.send(FromCache::Data(data)).await.unwrap(),
Err(error) => qry.reply.send(FromCache::Error(error)).await.unwrap(),
},
ToCache::Commit(commit) => match self.commit().await {
Ok(_) => commit.reply.send(FromCache::Ok).await.unwrap(),
Err(error) => commit.reply.send(FromCache::Error(error)).await.unwrap(),
},
}
}
}
}
#[cfg(test)]
mod caches {
use super::*;
use tempfile::tempdir;
async fn start_cache<P>(dir: P) -> Sender<ToCache>
where
P: Into<PathBuf>,
{
let (s, r) = unbounded();
let datadir = dir.into();
spawn(async move {
let cache = Cache::new(datadir).await.unwrap();
cache.start(r).await;
});
s
}
async fn send_request(data: Vec<&str>, channel: Sender<ToCache>) -> FromCache {
let mut ids = Vec::new();
for id in data.iter() {
ids.push(id.to_string());
}
let (s, r) = unbounded();
let msg = ToCache::Query(CacheQuery { ids: ids, reply: s });
channel.send(msg).await.unwrap();
r.recv().await.unwrap()
}
#[async_std::test]
async fn create() {
let dir = tempdir().unwrap();
let s_cache = start_cache(dir.path()).await;
let result = send_request(vec![ENTRY], s_cache).await;
match result {
FromCache::Data(data) => match data.get(ENTRY) {
Some(output) => match output {
DataType::DBMap(_) => (),
_ => assert!(false, "{:?} is not a database store.", output),
},
None => assert!(false, "Should contain entry point."),
},
_ => assert!(false, "{:?} should have been a store.", result),
}
}
#[async_std::test]
async fn bad_entry() {
let dir = tempdir().unwrap();
let s_cache = start_cache(dir.path()).await;
let result = send_request(vec!["bad_id"], s_cache).await;
match result {
FromCache::Error(_) => (),
_ => assert!(false, "{:?} should have been an error.", result),
}
}
#[async_std::test]
async fn empty_commit() {
let dir = tempdir().unwrap();
let s_cache = start_cache(dir.path()).await;
let (s, r) = unbounded();
let msg = ToCache::Commit(CacheCommit::new(DataType::new("store"), s).unwrap());
s_cache.send(msg).await.unwrap();
let result = r.recv().await.unwrap();
match result {
FromCache::Ok => (),
_ => assert!(false, "{:?} should have been an Ok.", result),
}
}
#[async_std::test]
async fn get_store() {
let dir = tempdir().unwrap();
let cache = Cache::new(dir.path()).await.unwrap();
let output = cache.query(&[ENTRY.to_string()].to_vec()).await.unwrap();
let result = output.get(ENTRY).unwrap();
match result {
DataType::DBMap(_) => (),
_ => assert!(false, "{:?} should have been an Ok.", result),
}
}
#[async_std::test]
async fn bad_get() {
let dir = tempdir().unwrap();
let cache = Cache::new(dir.path()).await.unwrap();
let bad_id = "really_bad_id";
match cache.query(&[bad_id.to_string()].to_vec()).await {
Ok(_) => assert!(false, "Should have produced an error."),
Err(err) => match err.code {
ErrorCode::EntryNotFound(id) => assert_eq!(id, bad_id),
_ => assert!(false, "{:?} should have been EntryNotFound.", err.code),
},
}
}
}
pub async fn start_db<P>(_dir: P) -> Result<MoreThanText, MTTError>
where
P: Into<PathBuf>,
{
let (s, r) = unbounded();
spawn(async move {
loop {
r.recv().await.unwrap();
}
});
Ok(MoreThanText::new(s).await.unwrap())
}
#[cfg(test)]
mod db_start_up {
use super::*;
use tempfile::tempdir;
#[async_std::test]
async fn initial_session() {
let dir = tempdir().unwrap();
let mtt = start_db(dir.path()).await.unwrap();
assert_eq!(mtt.session, [ENTRY]);
}
}