ecr-server 0.1.1

The ecr mail server: REST, SSE, bearer auth and a maildir watcher over ecr-store
Documentation
use ecr_core::revision::Revision;
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;

const CHANNEL_CAPACITY: usize = 256;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerEvent {
    MailChanged {
        revision: Revision,
    },
    TagsChanged {
        revision: Revision,
        ids: Vec<String>,
    },
    SyncStarted {
        accounts: Vec<String>,
    },
    SyncProgress {
        line: String,
    },
    SyncFinished {
        new_messages: usize,
        revision: Revision,
    },
    Error {
        detail: String,
    },
}

impl ServerEvent {
    pub fn name(&self) -> &'static str {
        match self {
            ServerEvent::MailChanged { .. } => "mail:changed",
            ServerEvent::TagsChanged { .. } => "tags:changed",
            ServerEvent::SyncStarted { .. } => "sync:started",
            ServerEvent::SyncProgress { .. } => "sync:progress",
            ServerEvent::SyncFinished { .. } => "sync:finished",
            ServerEvent::Error { .. } => "error",
        }
    }
}

#[derive(Clone)]
pub struct EventBus {
    sender: broadcast::Sender<ServerEvent>,
}

impl Default for EventBus {
    fn default() -> Self {
        Self::new()
    }
}

impl EventBus {
    pub fn new() -> Self {
        let (sender, _) = broadcast::channel(CHANNEL_CAPACITY);
        Self { sender }
    }

    pub fn publish(&self, event: ServerEvent) {
        let _ = self.sender.send(event);
    }

    pub fn subscribe(&self) -> broadcast::Receiver<ServerEvent> {
        self.sender.subscribe()
    }

    pub fn subscriber_count(&self) -> usize {
        self.sender.receiver_count()
    }
}

pub struct SyncProgress {
    bus: EventBus,
}

impl SyncProgress {
    pub fn new(bus: EventBus) -> Self {
        Self { bus }
    }
}

impl ecr_store::ProgressSink for SyncProgress {
    fn line(&self, text: &str) {
        self.bus.publish(ServerEvent::SyncProgress {
            line: text.to_string(),
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn revision() -> Revision {
        Revision::new("uuid", 1)
    }

    #[test]
    fn events_carry_stable_wire_names() {
        assert_eq!(
            ServerEvent::MailChanged {
                revision: revision()
            }
            .name(),
            "mail:changed"
        );
        assert_eq!(
            ServerEvent::SyncFinished {
                new_messages: 0,
                revision: revision()
            }
            .name(),
            "sync:finished"
        );
    }

    #[test]
    fn events_serialize_with_a_discriminating_type_field() {
        let json = serde_json::to_string(&ServerEvent::MailChanged {
            revision: revision(),
        })
        .unwrap();

        assert!(json.contains(r#""type":"mail_changed""#), "{json}");
        assert!(json.contains(r#""lastmod":1"#), "{json}");
    }

    #[tokio::test]
    async fn subscribers_receive_published_events() {
        let bus = EventBus::new();
        let mut rx = bus.subscribe();

        bus.publish(ServerEvent::MailChanged {
            revision: revision(),
        });

        let received = rx.recv().await.unwrap();
        assert_eq!(
            received,
            ServerEvent::MailChanged {
                revision: revision()
            }
        );
    }

    #[tokio::test]
    async fn publishing_with_no_subscribers_does_not_fail() {
        let bus = EventBus::new();
        bus.publish(ServerEvent::Error {
            detail: "nobody listening".to_string(),
        });
        assert_eq!(bus.subscriber_count(), 0);
    }

    #[tokio::test]
    async fn every_subscriber_sees_the_same_event() {
        let bus = EventBus::new();
        let mut a = bus.subscribe();
        let mut b = bus.subscribe();

        bus.publish(ServerEvent::SyncStarted {
            accounts: vec!["main".to_string()],
        });

        assert_eq!(a.recv().await.unwrap(), b.recv().await.unwrap());
    }

    #[tokio::test]
    async fn the_progress_sink_publishes_each_line() {
        use ecr_store::ProgressSink;

        let bus = EventBus::new();
        let mut rx = bus.subscribe();
        let sink = SyncProgress::new(bus.clone());

        sink.line("C: 1/2");

        assert_eq!(
            rx.recv().await.unwrap(),
            ServerEvent::SyncProgress {
                line: "C: 1/2".to_string()
            }
        );
    }
}