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}