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}