howler-core 0.1.0

Core library for the Howler wolf-tracking platform: database, data ingestion, ML, web server, and real-time streaming
Documentation
use crate::models::Sighting;
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum StreamingEvent {
    SightingCreated(Sighting),
    SightingUpdated(Sighting),
    AnalysisComplete(String),
}

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

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

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

    pub fn publish(
        &self,
        event: StreamingEvent,
    ) -> Result<(), Box<broadcast::error::SendError<StreamingEvent>>> {
        self.sender.send(event).map(|_| ()).map_err(Box::new)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::models::Source;
    use chrono::Utc;

    fn test_sighting() -> Sighting {
        Sighting {
            id: Some(1),
            species: "Canis lupus".to_string(),
            scientific_name: Some("Canis lupus".to_string()),
            latitude: 45.0,
            longitude: -122.0,
            observed_on: Utc::now(),
            source: Source::GBIF,
            source_id: "test_1".to_string(),
            details: Some("Test sighting".to_string()),
        }
    }

    #[tokio::test]
    async fn test_broadcast_publish_subscribe() {
        let broadcast = Broadcast::new(32);
        let mut rx = broadcast.subscribe();

        let sighting = test_sighting();
        let event = StreamingEvent::SightingCreated(sighting.clone());
        broadcast.publish(event).unwrap();

        let received = rx.recv().await.unwrap();
        match received {
            StreamingEvent::SightingCreated(s) => {
                assert_eq!(s.species, "Canis lupus");
                assert_eq!(s.id, Some(1));
            }
            _ => panic!("Expected SightingCreated"),
        }
    }

    #[tokio::test]
    async fn test_broadcast_multiple_subscribers() {
        let broadcast = Broadcast::new(32);
        let mut rx1 = broadcast.subscribe();
        let mut rx2 = broadcast.subscribe();

        let event = StreamingEvent::AnalysisComplete("done".to_string());
        broadcast.publish(event).unwrap();

        let received1 = rx1.recv().await.unwrap();
        let received2 = rx2.recv().await.unwrap();

        match (&received1, &received2) {
            (StreamingEvent::AnalysisComplete(m1), StreamingEvent::AnalysisComplete(m2)) => {
                assert_eq!(m1, "done");
                assert_eq!(m2, "done");
            }
            _ => panic!("Expected AnalysisComplete"),
        }
    }
}