use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
pub struct PublishError(pub String);
impl std::fmt::Display for PublishError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for PublishError {}
impl From<String> for PublishError {
fn from(s: String) -> Self {
PublishError(s)
}
}
pub trait Publisher: Send + Sync {
fn publish(&self, channel: &str, payload: &serde_json::Value) -> Result<(), PublishError>;
}
#[derive(Debug, Default, Clone)]
pub struct NoopPublisher;
impl Publisher for NoopPublisher {
fn publish(&self, channel: &str, payload: &serde_json::Value) -> Result<(), PublishError> {
eprintln!(
"[etdl.publisher] noop: channel={} payload={}",
channel, payload
);
Ok(())
}
}
#[derive(Debug, Default, Clone)]
pub struct ChannelCapturingPublisher {
sent: Arc<Mutex<Vec<(String, serde_json::Value)>>>,
}
impl ChannelCapturingPublisher {
pub fn new() -> Self {
Self::default()
}
pub fn sent(&self) -> Vec<(String, serde_json::Value)> {
self.sent.lock().map(|g| g.clone()).unwrap_or_default()
}
pub fn published_to(&self, channel: &str) -> bool {
self.sent().iter().any(|(c, _)| c == channel)
}
}
impl Publisher for ChannelCapturingPublisher {
fn publish(&self, channel: &str, payload: &serde_json::Value) -> Result<(), PublishError> {
if let Ok(mut g) = self.sent.lock() {
g.push((channel.to_string(), payload.clone()));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noop_publisher_accepts_all() {
let p = NoopPublisher;
assert!(p.publish("ch", &serde_json::json!({"a": 1})).is_ok());
}
#[test]
fn capturing_publisher_records_ordered() {
let p = ChannelCapturingPublisher::new();
p.publish("a", &serde_json::json!(1)).unwrap();
p.publish("b", &serde_json::json!({"k": "v"})).unwrap();
let sent = p.sent();
assert_eq!(sent.len(), 2);
assert_eq!(sent[0].0, "a");
assert_eq!(sent[1].0, "b");
assert!(p.published_to("b"));
assert!(!p.published_to("c"));
}
}