msy 0.4.2

Modern musl rsync alternative - Fast, parallel file synchronization
Documentation
use serde::Serialize;
use std::path::PathBuf;

/// JSON output mode for machine-readable sync events
/// Uses NDJSON format (newline-delimited JSON)
#[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SyncEvent {
    Start {
        source: PathBuf,
        destination: PathBuf,
        total_files: usize,
    },
    Create {
        path: PathBuf,
        size: u64,
        bytes_transferred: u64,
    },
    Update {
        path: PathBuf,
        size: u64,
        bytes_transferred: u64,
        delta_used: bool,
    },
    Skip {
        path: PathBuf,
        reason: String,
    },
    Delete {
        path: PathBuf,
    },
    #[allow(dead_code)] // Event for error reporting
    Error {
        path: PathBuf,
        error: String,
    },
    Summary {
        files_created: usize,
        files_updated: usize,
        files_skipped: usize,
        files_deleted: usize,
        bytes_transferred: u64,
        duration_secs: f64,
        files_verified: usize,
        verification_failures: usize,
    },
    #[allow(dead_code)] // Event for verify-only mode (Phase 5c)
    VerificationResult {
        files_matched: usize,
        files_mismatched: Vec<PathBuf>,
        files_only_in_source: Vec<PathBuf>,
        files_only_in_dest: Vec<PathBuf>,
        errors: Vec<VerificationError>,
        duration_secs: f64,
        exit_code: i32,
    },
    Performance {
        total_duration_secs: f64,
        scan_duration_secs: f64,
        plan_duration_secs: f64,
        transfer_duration_secs: f64,
        bytes_transferred: u64,
        bytes_read: u64,
        files_processed: u64,
        files_created: u64,
        files_updated: u64,
        files_deleted: u64,
        directories_created: u64,
        avg_transfer_speed: f64,
        peak_transfer_speed: f64,
        files_per_second: f64,
        bandwidth_utilization: Option<f64>,
    },
}

#[derive(Debug, Serialize)]
pub struct VerificationError {
    pub path: PathBuf,
    pub error: String,
    pub action: String,
}

impl SyncEvent {
    /// Emit this event as JSON to stdout
    pub fn emit(&self) {
        if let Ok(json) = serde_json::to_string(self) {
            println!("{}", json);
        }
    }
}

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

    #[test]
    fn test_serialize_start_event() {
        let event = SyncEvent::Start {
            source: PathBuf::from("/src"),
            destination: PathBuf::from("/dst"),
            total_files: 100,
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""type":"start"#));
        assert!(json.contains(r#""total_files":100"#));
    }

    #[test]
    fn test_serialize_create_event() {
        let event = SyncEvent::Create {
            path: PathBuf::from("file.txt"),
            size: 1234,
            bytes_transferred: 1234,
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""type":"create"#));
        assert!(json.contains(r#""size":1234"#));
    }

    #[test]
    fn test_serialize_update_event() {
        let event = SyncEvent::Update {
            path: PathBuf::from("file.txt"),
            size: 5678,
            bytes_transferred: 234,
            delta_used: true,
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""type":"update"#));
        assert!(json.contains(r#""delta_used":true"#));
    }

    #[test]
    fn test_serialize_summary_event() {
        let event = SyncEvent::Summary {
            files_created: 10,
            files_updated: 5,
            files_skipped: 20,
            files_deleted: 2,
            bytes_transferred: 123456,
            duration_secs: 12.5,
            files_verified: 15,
            verification_failures: 0,
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""type":"summary"#));
        assert!(json.contains(r#""files_created":10"#));
        assert!(json.contains(r#""duration_secs":12.5"#));
        assert!(json.contains(r#""files_verified":15"#));
        assert!(json.contains(r#""verification_failures":0"#));
    }

    #[test]
    fn test_serialize_verification_result() {
        let event = SyncEvent::VerificationResult {
            files_matched: 10,
            files_mismatched: vec![PathBuf::from("file1.txt"), PathBuf::from("file2.txt")],
            files_only_in_source: vec![PathBuf::from("src_only.txt")],
            files_only_in_dest: vec![PathBuf::from("dst_only.txt")],
            errors: vec![VerificationError {
                path: PathBuf::from("error_file.txt"),
                error: "Permission denied".to_string(),
                action: "verify".to_string(),
            }],
            duration_secs: 1.5,
            exit_code: 1,
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""type":"verification_result"#));
        assert!(json.contains(r#""files_matched":10"#));
        assert!(json.contains(r#""files_mismatched"#));
        assert!(json.contains(r#""file1.txt"#));
        assert!(json.contains(r#""files_only_in_source"#));
        assert!(json.contains(r#""src_only.txt"#));
        assert!(json.contains(r#""files_only_in_dest"#));
        assert!(json.contains(r#""dst_only.txt"#));
        assert!(json.contains(r#""errors"#));
        assert!(json.contains(r#""error_file.txt"#));
        assert!(json.contains(r#""duration_secs":1.5"#));
        assert!(json.contains(r#""exit_code":1"#));
    }

    #[test]
    fn test_serialize_performance_event() {
        let event = SyncEvent::Performance {
            total_duration_secs: 10.5,
            scan_duration_secs: 1.2,
            plan_duration_secs: 0.8,
            transfer_duration_secs: 8.5,
            bytes_transferred: 1_000_000,
            bytes_read: 1_200_000,
            files_processed: 100,
            files_created: 50,
            files_updated: 30,
            files_deleted: 10,
            directories_created: 5,
            avg_transfer_speed: 117_647.0,
            peak_transfer_speed: 200_000.0,
            files_per_second: 9.52,
            bandwidth_utilization: Some(87.5),
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""type":"performance"#));
        assert!(json.contains(r#""bytes_transferred":1000000"#));
        assert!(json.contains(r#""avg_transfer_speed":117647"#));
        assert!(json.contains(r#""bandwidth_utilization":87.5"#));
    }
}