use std::sync::Arc;
use async_trait::async_trait;
use uuid::Uuid;
#[derive(Debug, thiserror::Error)]
pub enum PhoneSourceError {
#[error("db: {0}")]
Db(#[from] sqlx::Error),
#[error("phone book not composed: {detail}")]
NotComposed { detail: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PhoneCandidate {
pub raw: String,
pub country_hint: Option<String>,
}
impl PhoneCandidate {
pub fn international(raw: impl Into<String>) -> Self {
Self { raw: raw.into(), country_hint: None }
}
pub fn national(raw: impl Into<String>, country_hint: impl Into<String>) -> Self {
Self { raw: raw.into(), country_hint: Some(country_hint.into()) }
}
}
#[async_trait]
pub trait PhoneBookPort: Send + Sync {
async fn candidates(
&self,
model: &str,
res_id: Uuid,
) -> Result<Vec<PhoneCandidate>, PhoneSourceError>;
}
pub struct RefusingPhoneBook;
#[async_trait]
impl PhoneBookPort for RefusingPhoneBook {
async fn candidates(
&self,
model: &str,
res_id: Uuid,
) -> Result<Vec<PhoneCandidate>, PhoneSourceError> {
Err(PhoneSourceError::NotComposed {
detail: format!(
"no PhoneBookPort is installed; refusing candidate lookup for \
{model}/{res_id} — compose one via MessagingModule::set_phone_book"
),
})
}
}
pub struct NoopPhoneBook {
candidates: Vec<PhoneCandidate>,
}
impl NoopPhoneBook {
pub fn canned(candidates: Vec<PhoneCandidate>) -> Self {
Self { candidates }
}
pub fn empty() -> Self {
Self { candidates: Vec::new() }
}
}
#[async_trait]
impl PhoneBookPort for NoopPhoneBook {
async fn candidates(
&self,
_model: &str,
_res_id: Uuid,
) -> Result<Vec<PhoneCandidate>, PhoneSourceError> {
Ok(self.candidates.clone())
}
}
#[derive(Clone)]
pub struct PhoneBookSlot {
inner: Arc<std::sync::RwLock<Arc<dyn PhoneBookPort>>>,
}
impl PhoneBookSlot {
pub fn install(&self, book: Arc<dyn PhoneBookPort>) {
*self.inner.write().unwrap_or_else(|e| e.into_inner()) = book;
}
pub fn current(&self) -> Arc<dyn PhoneBookPort> {
self.inner.read().unwrap_or_else(|e| e.into_inner()).clone()
}
}
impl Default for PhoneBookSlot {
fn default() -> Self {
Self { inner: Arc::new(std::sync::RwLock::new(Arc::new(RefusingPhoneBook))) }
}
}
#[async_trait]
impl PhoneBookPort for PhoneBookSlot {
async fn candidates(
&self,
model: &str,
res_id: Uuid,
) -> Result<Vec<PhoneCandidate>, PhoneSourceError> {
self.current().candidates(model, res_id).await
}
}