faultline_diff 0.1.0

Diff and change classification contracts for FaultLine.
Documentation
use chrono::{DateTime, Utc};
use faultline_core::{ChangeEvent, DatasetSnapshot};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DiffRequest {
    pub left: String,
    pub right: String,
    pub layer: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DiffSummary {
    pub diff_id: Uuid,
    pub added: u64,
    pub removed: u64,
    pub changed: u64,
}

impl DiffSummary {
    pub fn empty() -> Self {
        Self {
            diff_id: Uuid::new_v4(),
            added: 0,
            removed: 0,
            changed: 0,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DiffClassification {
    Deterministic,
    NoChanges,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SnapshotState {
    pub snapshot: DatasetSnapshot,
    pub features: Vec<faultline_core::FeatureRef>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DiffArtifact {
    pub artifact_id: Uuid,
    pub generated_at: DateTime<Utc>,
    pub classification: DiffClassification,
    pub left_snapshot_id: Uuid,
    pub right_snapshot_id: Uuid,
    pub summary: DiffSummary,
    pub change_events: Vec<ChangeEvent>,
}

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

    #[test]
    fn summary_empty_starts_at_zero() {
        let summary = DiffSummary::empty();

        assert_eq!(summary.added, 0);
        assert_eq!(summary.removed, 0);
        assert_eq!(summary.changed, 0);
    }

    #[test]
    fn diff_artifact_round_trip_serialization() {
        let snapshot = DatasetSnapshot::new("roads");
        let summary = DiffSummary::empty();
        let artifact = DiffArtifact {
            artifact_id: Uuid::new_v4(),
            generated_at: Utc::now(),
            classification: DiffClassification::NoChanges,
            left_snapshot_id: snapshot.snapshot_id,
            right_snapshot_id: snapshot.snapshot_id,
            summary,
            change_events: Vec::new(),
        };

        let json = serde_json::to_string(&artifact).expect("serialize artifact");
        let restored: DiffArtifact = serde_json::from_str(&json).expect("deserialize artifact");

        assert_eq!(restored.classification, DiffClassification::NoChanges);
        assert!(restored.change_events.is_empty());
    }
}