takproto 0.4.2

Rust library for TAK (Team Awareness Kit) Protocol - send CoT messages to TAK servers with mTLS support
Documentation
//! Builder pattern for creating CotEvent messages
//!
//! The `CotEventBuilder` provides an ergonomic way to construct CoT events
//! with sensible defaults and a fluent API.

use crate::proto::{CotEvent, Detail, Contact, Track, Group, Status, Takv, PrecisionLocation};
use std::time::{SystemTime, UNIX_EPOCH};

/// Builder for creating CotEvent messages
///
/// Provides a fluent API for constructing CoT events with automatic timestamp
/// management and sensible defaults.
///
/// # Example
///
/// ```
/// use takproto::builder::CotEventBuilder;
/// use takproto::helpers::{contact, track};
///
/// let event = CotEventBuilder::new()
///     .uid("ALPHA-1")
///     .cot_type("a-f-G-U-C")
///     .lat_lon(37.7749, -122.4194)
///     .hae(10.0)
///     .ce_le(9.9, 9.9)
///     .stale_minutes(5)
///     .how("m-g")
///     .with_contact(contact("ALPHA-1", Some("192.168.1.100:4242")))
///     .with_track(track(15.0, 270.0))
///     .build()
///     .unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct CotEventBuilder {
    event: CotEvent,
    detail: Detail,
    stale_offset_ms: u64,
}

impl CotEventBuilder {
    /// Create a new builder with default values
    ///
    /// Defaults:
    /// - Current time for send_time and start_time
    /// - 1 minute stale time
    /// - Empty strings for required fields
    pub fn new() -> Self {
        let now_ms = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_millis() as u64;

        Self {
            event: CotEvent {
                r#type: String::new(),
                access: String::new(),
                qos: String::new(),
                opex: String::new(),
                uid: String::new(),
                send_time: now_ms,
                start_time: now_ms,
                stale_time: now_ms + 60_000, // 1 minute default
                how: String::new(),
                lat: 0.0,
                lon: 0.0,
                hae: 0.0,
                ce: 9999999.0,
                le: 9999999.0,
                detail: None,
            },
            detail: Detail {
                xml_detail: String::new(),
                contact: None,
                group: None,
                precision_location: None,
                status: None,
                takv: None,
                track: None,
            },
            stale_offset_ms: 60_000,
        }
    }

    /// Set the CoT type (e.g., "a-f-G-U-C" for friendly ground unit)
    pub fn cot_type(mut self, cot_type: &str) -> Self {
        self.event.r#type = cot_type.to_string();
        self
    }

    /// Set the unique identifier
    pub fn uid(mut self, uid: &str) -> Self {
        self.event.uid = uid.to_string();
        self
    }

    /// Set latitude and longitude
    pub fn lat_lon(mut self, lat: f64, lon: f64) -> Self {
        self.event.lat = lat;
        self.event.lon = lon;
        self
    }

    /// Set Height Above Ellipsoid (HAE) in meters
    pub fn hae(mut self, hae: f64) -> Self {
        self.event.hae = hae;
        self
    }

    /// Set Circular Error (CE) and Linear Error (LE) in meters
    ///
    /// Use 9999999.0 for unknown values.
    pub fn ce_le(mut self, ce: f64, le: f64) -> Self {
        self.event.ce = ce;
        self.event.le = le;
        self
    }

    /// Set the "how" field (method of position determination)
    ///
    /// Common values:
    /// - "m-g" - GPS/GNSS
    /// - "h-e" - Human entered
    /// - "h-g-i-g-o" - Human GPS observation
    pub fn how(mut self, how: &str) -> Self {
        self.event.how = how.to_string();
        self
    }

    /// Set stale time as an offset from current time in minutes
    pub fn stale_minutes(mut self, minutes: u64) -> Self {
        self.stale_offset_ms = minutes * 60_000;
        self.event.stale_time = self.event.start_time + self.stale_offset_ms;
        self
    }

    /// Set stale time as an offset from current time in seconds
    pub fn stale_seconds(mut self, seconds: u64) -> Self {
        self.stale_offset_ms = seconds * 1000;
        self.event.stale_time = self.event.start_time + self.stale_offset_ms;
        self
    }

    /// Set explicit timestamps in milliseconds since UNIX epoch
    pub fn timestamps(mut self, send_time: u64, start_time: u64, stale_time: u64) -> Self {
        self.event.send_time = send_time;
        self.event.start_time = start_time;
        self.event.stale_time = stale_time;
        self
    }

    /// Add contact information
    pub fn with_contact(mut self, contact: Contact) -> Self {
        self.detail.contact = Some(contact);
        self
    }

    /// Add track information (speed and course)
    pub fn with_track(mut self, track: Track) -> Self {
        self.detail.track = Some(track);
        self
    }

    /// Add group information
    pub fn with_group(mut self, group: Group) -> Self {
        self.detail.group = Some(group);
        self
    }

    /// Add status information
    pub fn with_status(mut self, status: Status) -> Self {
        self.detail.status = Some(status);
        self
    }

    /// Add TAK version information
    pub fn with_takv(mut self, takv: Takv) -> Self {
        self.detail.takv = Some(takv);
        self
    }

    /// Add precision location information
    pub fn with_precision_location(mut self, precision_location: PrecisionLocation) -> Self {
        self.detail.precision_location = Some(precision_location);
        self
    }

    /// Add XML detail string
    ///
    /// This should contain XML elements that go under `<detail>`, but without
    /// the `<detail>` wrapper itself.
    ///
    /// # Example
    ///
    /// ```
    /// use takproto::builder::CotEventBuilder;
    /// use takproto::helpers::{remarks, color, colors};
    ///
    /// let xml = format!("{}\n{}", remarks("Test"), color(colors::RED));
    /// let event = CotEventBuilder::new()
    ///     .uid("TEST-1")
    ///     .cot_type("b-m-p-s-m")
    ///     .lat_lon(37.0, -122.0)
    ///     .with_xml_detail(&xml)
    ///     .build()
    ///     .unwrap();
    /// ```
    pub fn with_xml_detail(mut self, xml_detail: &str) -> Self {
        self.detail.xml_detail = xml_detail.to_string();
        self
    }

    /// Build the CotEvent
    ///
    /// Returns an error if required fields (type, uid) are empty.
    pub fn build(mut self) -> Result<CotEvent, String> {
        if self.event.r#type.is_empty() {
            return Err("CoT type is required".to_string());
        }
        if self.event.uid.is_empty() {
            return Err("UID is required".to_string());
        }

        // Only add detail if there's actually something in it
        if self.detail.contact.is_some()
            || self.detail.group.is_some()
            || self.detail.precision_location.is_some()
            || self.detail.status.is_some()
            || self.detail.takv.is_some()
            || self.detail.track.is_some()
            || !self.detail.xml_detail.is_empty()
        {
            self.event.detail = Some(self.detail);
        }

        Ok(self.event)
    }
}

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

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

    #[test]
    fn test_builder_basic() {
        let event = CotEventBuilder::new()
            .uid("TEST-1")
            .cot_type("a-f-G-U-C")
            .lat_lon(37.7749, -122.4194)
            .build()
            .unwrap();

        assert_eq!(event.uid, "TEST-1");
        assert_eq!(event.r#type, "a-f-G-U-C");
        assert_eq!(event.lat, 37.7749);
        assert_eq!(event.lon, -122.4194);
    }

    #[test]
    fn test_builder_with_details() {
        use crate::helpers::{contact, track};

        let event = CotEventBuilder::new()
            .uid("TEST-2")
            .cot_type("a-f-G-U-C")
            .lat_lon(37.0, -122.0)
            .with_contact(contact("ALPHA-1", None))
            .with_track(track(15.0, 270.0))
            .build()
            .unwrap();

        assert!(event.detail.is_some());
        let detail = event.detail.unwrap();
        assert!(detail.contact.is_some());
        assert!(detail.track.is_some());
    }

    #[test]
    fn test_builder_missing_required() {
        let result = CotEventBuilder::new()
            .uid("TEST-3")
            .build();

        assert!(result.is_err());
        assert!(result.unwrap_err().contains("type"));
    }

    #[test]
    fn test_stale_time() {
        let event = CotEventBuilder::new()
            .uid("TEST-4")
            .cot_type("a-f-G-U-C")
            .lat_lon(0.0, 0.0)
            .stale_minutes(5)
            .build()
            .unwrap();

        // Stale time should be ~5 minutes after start time
        let diff = event.stale_time - event.start_time;
        assert_eq!(diff, 5 * 60 * 1000);
    }
}