Skip to main content

suture_driver/
lib.rs

1#![allow(clippy::collapsible_match)]
2//! SutureDriver trait and registry for format-specific drivers.
3//!
4//! Drivers translate between file formats and semantic patches,
5//! enabling Suture to understand *what changed* rather than just
6//! *which bytes changed*.
7
8pub mod error;
9pub mod plugin;
10pub mod registry;
11pub mod types;
12
13pub use error::DriverError;
14pub use plugin::{BuiltinDriverPlugin, DriverPlugin, PluginError, PluginRegistry};
15pub use registry::DriverRegistry;
16pub use types::{DiffHunk, DiffHunkType, DiffSummary, VisualDiff};
17
18/// Format-specific driver for translating between file formats and Suture patches.
19///
20/// Implementations must be `Send + Sync` for concurrent use across threads.
21/// A driver understands the *semantics* of a file format — it knows that
22/// changing a key in a JSON object is a different operation than appending
23/// to an array.
24pub trait SutureDriver: Send + Sync {
25    /// Human-readable driver name (e.g., "JSON", "OpenTimelineIO", "CSV").
26    fn name(&self) -> &str;
27
28    /// File extensions this driver handles (e.g., `[".json", ".jsonl"]`).
29    fn supported_extensions(&self) -> &[&str];
30
31    /// Parse a file and produce a semantic diff between it and an optional base.
32    ///
33    /// If `base_content` is `None`, this is a new file — produce creation patches.
34    /// If `base_content` is `Some`, produce patches representing the differences.
35    ///
36    /// Each returned `SemanticChange` describes a meaningful semantic operation
37    /// (e.g., "key `users.2.email` changed from `old@example.com` to `new@example.com`").
38    fn diff(
39        &self,
40        base_content: Option<&str>,
41        new_content: &str,
42    ) -> Result<Vec<SemanticChange>, DriverError>;
43
44    /// Produce a human-readable diff string between two versions of a file.
45    ///
46    /// This is used by `suture diff` when a driver is available for the file type.
47    /// The output should be more meaningful than raw line diffs — showing
48    /// semantic operations like key changes, array insertions, etc.
49    fn format_diff(
50        &self,
51        base_content: Option<&str>,
52        new_content: &str,
53    ) -> Result<String, DriverError>;
54
55    /// Perform a semantic three-way merge.
56    ///
57    /// Given base, ours, and theirs content, produce a merged result.
58    /// Returns `None` if the merge cannot be resolved automatically (conflict).
59    /// Returns `Some(merged_content)` if the merge is clean.
60    fn merge(
61        &self,
62        _base: &str,
63        _ours: &str,
64        _theirs: &str,
65    ) -> Result<Option<String>, DriverError> {
66        Ok(None)
67    }
68
69    /// Byte-level three-way merge for binary formats.
70    ///
71    /// Like `merge()` but operates on raw bytes instead of `&str`.
72    /// Binary drivers (DOCX, XLSX, PPTX, PDF, images) should override this
73    /// to avoid `unsafe { String::from_utf8_unchecked }`.
74    /// The default implementation converts to/from UTF-8 lossy and delegates
75    /// to `merge()` — text drivers do not need to override this.
76    fn merge_raw(
77        &self,
78        base: &[u8],
79        ours: &[u8],
80        theirs: &[u8],
81    ) -> Result<Option<Vec<u8>>, DriverError> {
82        let base_str = String::from_utf8_lossy(base);
83        let ours_str = String::from_utf8_lossy(ours);
84        let theirs_str = String::from_utf8_lossy(theirs);
85        match self.merge(&base_str, &ours_str, &theirs_str)? {
86            Some(s) => Ok(Some(s.into_bytes())),
87            None => Ok(None),
88        }
89    }
90
91    /// Byte-level semantic diff for binary formats.
92    ///
93    /// Like `diff()` but operates on raw bytes instead of `&str`.
94    /// Binary drivers should override this to avoid `unsafe` conversions.
95    fn diff_raw(
96        &self,
97        base: Option<&[u8]>,
98        new_content: &[u8],
99    ) -> Result<Vec<SemanticChange>, DriverError> {
100        let base_str = base.map(|b| String::from_utf8_lossy(b));
101        let new_str = String::from_utf8_lossy(new_content);
102        self.diff(base_str.as_deref(), &new_str)
103    }
104}
105
106/// A single semantic change detected by a driver.
107#[derive(Debug, Clone, PartialEq)]
108pub enum SemanticChange {
109    /// A value was added at a path (e.g., new key in JSON object).
110    Added { path: String, value: String },
111    /// A value was removed at a path.
112    Removed { path: String, old_value: String },
113    /// A value was modified at a path.
114    Modified {
115        path: String,
116        old_value: String,
117        new_value: String,
118    },
119    /// A value was moved/renamed from one path to another.
120    Moved {
121        old_path: String,
122        new_path: String,
123        value: String,
124    },
125}