use crate::contacts_service::error::ContactsServiceStorageError;
use log::*;
use std::{
fmt::{Display, Error, Formatter},
sync::Arc,
};
use tari_comms::types::CommsPublicKey;
const LOG_TARGET: &str = "wallet::contacts_service::database";
#[derive(Debug, Clone, PartialEq)]
pub struct Contact {
pub alias: String,
pub public_key: CommsPublicKey,
}
pub trait ContactsBackend: Send + Sync + Clone {
fn fetch(&self, key: &DbKey) -> Result<Option<DbValue>, ContactsServiceStorageError>;
fn write(&self, op: WriteOperation) -> Result<Option<DbValue>, ContactsServiceStorageError>;
}
#[derive(Debug, Clone, PartialEq)]
pub enum DbKey {
Contact(CommsPublicKey),
Contacts,
}
pub enum DbValue {
Contact(Box<Contact>),
Contacts(Vec<Contact>),
}
pub enum DbKeyValuePair {
Contact(CommsPublicKey, Contact),
}
pub enum WriteOperation {
Upsert(DbKeyValuePair),
Remove(DbKey),
}
macro_rules! fetch {
($db:ident, $key_val:expr, $key_var:ident) => {{
let key = DbKey::$key_var($key_val);
match $db.fetch(&key) {
Ok(None) => Err(ContactsServiceStorageError::ValueNotFound(key)),
Ok(Some(DbValue::$key_var(k))) => Ok(*k),
Ok(Some(other)) => unexpected_result(key, other),
Err(e) => log_error(key, e),
}
}};
}
pub struct ContactsDatabase<T>
where T: ContactsBackend
{
db: Arc<T>,
}
impl<T> ContactsDatabase<T>
where T: ContactsBackend + 'static
{
pub fn new(db: T) -> Self {
Self { db: Arc::new(db) }
}
pub async fn get_contact(&self, pub_key: CommsPublicKey) -> Result<Contact, ContactsServiceStorageError> {
let db_clone = self.db.clone();
tokio::task::spawn_blocking(move || fetch!(db_clone, pub_key.clone(), Contact))
.await
.map_err(|err| ContactsServiceStorageError::BlockingTaskSpawnError(err.to_string()))
.and_then(|inner_result| inner_result)
}
pub async fn get_contacts(&self) -> Result<Vec<Contact>, ContactsServiceStorageError> {
let db_clone = self.db.clone();
let c = tokio::task::spawn_blocking(move || match db_clone.fetch(&DbKey::Contacts) {
Ok(None) => log_error(
DbKey::Contacts,
ContactsServiceStorageError::UnexpectedResult("Could not retrieve contacts".to_string()),
),
Ok(Some(DbValue::Contacts(c))) => Ok(c),
Ok(Some(other)) => unexpected_result(DbKey::Contacts, other),
Err(e) => log_error(DbKey::Contacts, e),
})
.await
.map_err(|err| ContactsServiceStorageError::BlockingTaskSpawnError(err.to_string()))??;
Ok(c)
}
pub async fn upsert_contact(&self, contact: Contact) -> Result<(), ContactsServiceStorageError> {
let db_clone = self.db.clone();
tokio::task::spawn_blocking(move || {
db_clone.write(WriteOperation::Upsert(DbKeyValuePair::Contact(
contact.public_key.clone(),
contact,
)))
})
.await
.map_err(|err| ContactsServiceStorageError::BlockingTaskSpawnError(err.to_string()))??;
Ok(())
}
pub async fn remove_contact(&self, pub_key: CommsPublicKey) -> Result<Contact, ContactsServiceStorageError> {
let db_clone = self.db.clone();
let pub_key_clone = pub_key.clone();
let result =
tokio::task::spawn_blocking(move || db_clone.write(WriteOperation::Remove(DbKey::Contact(pub_key_clone))))
.await
.map_err(|err| ContactsServiceStorageError::BlockingTaskSpawnError(err.to_string()))
.and_then(|inner_result| inner_result)?
.ok_or_else(|| ContactsServiceStorageError::ValueNotFound(DbKey::Contact(pub_key.clone())))?;
match result {
DbValue::Contact(c) => Ok(*c),
DbValue::Contacts(_) => Err(ContactsServiceStorageError::UnexpectedResult(
"Incorrect response from backend.".to_string(),
)),
}
}
}
fn unexpected_result<T>(req: DbKey, res: DbValue) -> Result<T, ContactsServiceStorageError> {
let msg = format!("Unexpected result for database query {}. Response: {}", req, res);
error!(target: LOG_TARGET, "{}", msg);
Err(ContactsServiceStorageError::UnexpectedResult(msg))
}
impl Display for DbKey {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
match self {
DbKey::Contact(c) => f.write_str(&format!("Contact: {:?}", c)),
DbKey::Contacts => f.write_str(&"Contacts".to_string()),
}
}
}
impl Display for DbValue {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
match self {
DbValue::Contact(_) => f.write_str(&"Contact".to_string()),
DbValue::Contacts(_) => f.write_str(&"Contacts".to_string()),
}
}
}
fn log_error<T>(req: DbKey, err: ContactsServiceStorageError) -> Result<T, ContactsServiceStorageError> {
error!(
target: LOG_TARGET,
"Database access error on request: {}: {}",
req,
err.to_string()
);
Err(err)
}