use std::sync::Arc;
use async_trait::async_trait;
use uuid::Uuid;
#[derive(Debug, thiserror::Error)]
pub enum TraceClickError {
#[error("trace click port not composed: {detail}")]
NotComposed { detail: String },
#[error("unknown short code")]
UnknownCode,
#[error("code attribution does not match the trace's campaign")]
AttributionMismatch,
#[error("click backend: {0}")]
Backend(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CodeAttribution {
pub campaign_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedClick {
pub url: String,
pub campaign_id: Option<Uuid>,
}
#[async_trait]
pub trait TraceClickPort: Send + Sync {
async fn attribution(&self, code: &str) -> Result<CodeAttribution, TraceClickError>;
async fn resolve_click(
&self,
code: &str,
trace_campaign_id: Option<Uuid>,
ip: Option<&str>,
country_code: Option<&str>,
) -> Result<ResolvedClick, TraceClickError>;
}
pub struct RefusingTraceClick;
#[async_trait]
impl TraceClickPort for RefusingTraceClick {
async fn attribution(&self, code: &str) -> Result<CodeAttribution, TraceClickError> {
Err(TraceClickError::NotComposed {
detail: format!(
"no TraceClickPort is installed; refusing attribution lookup for code \
{code} — compose one via MailingModule::set_trace_click_port"
),
})
}
async fn resolve_click(
&self,
code: &str,
_trace_campaign_id: Option<Uuid>,
_ip: Option<&str>,
_country_code: Option<&str>,
) -> Result<ResolvedClick, TraceClickError> {
Err(TraceClickError::NotComposed {
detail: format!(
"no TraceClickPort is installed; refusing click resolution for code \
{code} — compose one via MailingModule::set_trace_click_port"
),
})
}
}
pub struct CannedTraceClick {
pub campaign_id: Option<Uuid>,
pub url: String,
pub unknown: bool,
}
#[async_trait]
impl TraceClickPort for CannedTraceClick {
async fn attribution(&self, code: &str) -> Result<CodeAttribution, TraceClickError> {
if self.unknown {
return Err(TraceClickError::UnknownCode);
}
let _ = code;
Ok(CodeAttribution { campaign_id: self.campaign_id })
}
async fn resolve_click(
&self,
code: &str,
trace_campaign_id: Option<Uuid>,
_ip: Option<&str>,
_country_code: Option<&str>,
) -> Result<ResolvedClick, TraceClickError> {
if self.unknown {
return Err(TraceClickError::UnknownCode);
}
if self.campaign_id != trace_campaign_id {
let _ = code;
return Err(TraceClickError::AttributionMismatch);
}
Ok(ResolvedClick { url: self.url.clone(), campaign_id: self.campaign_id })
}
}
#[derive(Clone)]
pub struct TraceClickSlot {
inner: Arc<std::sync::RwLock<Arc<dyn TraceClickPort>>>,
}
impl TraceClickSlot {
pub fn install(&self, port: Arc<dyn TraceClickPort>) {
*self.inner.write().unwrap_or_else(|e| e.into_inner()) = port;
}
pub fn current(&self) -> Arc<dyn TraceClickPort> {
self.inner.read().unwrap_or_else(|e| e.into_inner()).clone()
}
}
impl Default for TraceClickSlot {
fn default() -> Self {
Self { inner: Arc::new(std::sync::RwLock::new(Arc::new(RefusingTraceClick))) }
}
}
#[async_trait]
impl TraceClickPort for TraceClickSlot {
async fn attribution(&self, code: &str) -> Result<CodeAttribution, TraceClickError> {
self.current().attribution(code).await
}
async fn resolve_click(
&self,
code: &str,
trace_campaign_id: Option<Uuid>,
ip: Option<&str>,
country_code: Option<&str>,
) -> Result<ResolvedClick, TraceClickError> {
self.current()
.resolve_click(code, trace_campaign_id, ip, country_code)
.await
}
}