Skip to main content

suture_driver/
lib.rs

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