Skip to main content

llmtxt_core/
patch.rs

1use diffy::{Patch, apply as diffy_apply, create_patch as diffy_create_patch};
2
3#[cfg(feature = "wasm")]
4use wasm_bindgen::prelude::*;
5
6/// Apply a unified diff patch to an original string.
7/// Returns the updated string on success, or an error if the patch is invalid
8/// or fails to apply cleanly.
9#[cfg_attr(feature = "wasm", wasm_bindgen)]
10pub fn apply_patch(original: &str, patch_text: &str) -> Result<String, String> {
11    let patch = Patch::from_str(patch_text).map_err(|err| format!("Invalid patch text: {err}"))?;
12    diffy_apply(original, &patch).map_err(|err| format!("Patch application failed: {err}"))
13}
14
15/// Create a unified diff patch representing the difference between `original`
16/// and `modified`.
17#[cfg_attr(feature = "wasm", wasm_bindgen)]
18pub fn create_patch(original: &str, modified: &str) -> String {
19    diffy_create_patch(original, modified).to_string()
20}
21
22/// Apply a sequence of patches to base content, returning the content at the
23/// target version. This avoids N WASM boundary crossings by performing all
24/// patch applications in a single Rust call.
25///
26/// `patches_json` is a JSON array of patch strings: `["patch1", "patch2", ...]`.
27/// `target` is the 1-based version to reconstruct (0 returns `base` unchanged).
28/// If `target` exceeds the number of patches, all patches are applied.
29#[cfg_attr(feature = "wasm", wasm_bindgen)]
30pub fn reconstruct_version(base: &str, patches_json: &str, target: u32) -> Result<String, String> {
31    if target == 0 {
32        return Ok(base.to_string());
33    }
34
35    let patches: Vec<String> =
36        serde_json::from_str(patches_json).map_err(|e| format!("Invalid patches JSON: {e}"))?;
37
38    let limit = (target as usize).min(patches.len());
39    let mut content = base.to_string();
40
41    for (i, patch_text) in patches.iter().take(limit).enumerate() {
42        content = apply_patch(&content, patch_text)
43            .map_err(|e| format!("Patch {} failed: {e}", i + 1))?;
44    }
45
46    Ok(content)
47}
48
49/// Native-friendly version of [`reconstruct_version`] that accepts a slice
50/// directly instead of JSON. Use this from Rust consumers; the JSON variant
51/// is for WASM callers.
52pub fn reconstruct_version_native(
53    base: &str,
54    patches: &[String],
55    target: usize,
56) -> Result<String, String> {
57    if target == 0 {
58        return Ok(base.to_string());
59    }
60    let limit = target.min(patches.len());
61    let mut content = base.to_string();
62    for (i, patch_text) in patches.iter().take(limit).enumerate() {
63        content = apply_patch(&content, patch_text)
64            .map_err(|e| format!("Patch {} failed: {e}", i + 1))?;
65    }
66    Ok(content)
67}
68
69/// Native-friendly version of [`squash_patches`] that accepts a slice directly.
70pub fn squash_patches_native(base: &str, patches: &[String]) -> Result<String, String> {
71    let final_content = reconstruct_version_native(base, patches, patches.len())?;
72    Ok(create_patch(base, &final_content))
73}
74
75/// Apply all patches sequentially to base content, then produce a single
76/// unified diff from the original base to the final state.
77///
78/// `patches_json` is a JSON array of patch strings: `["patch1", "patch2", ...]`.
79#[cfg_attr(feature = "wasm", wasm_bindgen)]
80pub fn squash_patches(base: &str, patches_json: &str) -> Result<String, String> {
81    let patches: Vec<String> =
82        serde_json::from_str(patches_json).map_err(|e| format!("Invalid patches JSON: {e}"))?;
83
84    let mut content = base.to_string();
85    for (i, patch_text) in patches.iter().enumerate() {
86        content = apply_patch(&content, patch_text)
87            .map_err(|e| format!("Patch {} failed: {e}", i + 1))?;
88    }
89
90    Ok(create_patch(base, &content))
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_create_and_apply_patch() {
99        let original = "Hello world. This is a test.\n";
100        let modified = "Hello beautiful world. This is an awesome test.\n";
101
102        let patch = create_patch(original, modified);
103        assert!(patch.contains("@@"));
104
105        let applied = apply_patch(original, &patch).expect("patch should apply");
106        assert_eq!(applied, modified);
107    }
108
109    #[test]
110    fn test_apply_invalid_patch() {
111        let result = apply_patch("Hello world\n", "@@ invalid patch format @@");
112        assert!(result.is_err());
113    }
114
115    #[test]
116    fn test_reconstruct_version_zero_returns_base() {
117        let base = "Hello world\n";
118        let result = reconstruct_version(base, "[]", 0).unwrap();
119        assert_eq!(result, base);
120    }
121
122    #[test]
123    fn test_reconstruct_version_applies_patches() {
124        let v0 = "line 1\n";
125        let v1 = "line 1\nline 2\n";
126        let v2 = "line 1\nline 2\nline 3\n";
127
128        let p1 = create_patch(v0, v1);
129        let p2 = create_patch(v1, v2);
130        let patches_json = serde_json::to_string(&vec![p1, p2]).unwrap();
131
132        let at_v1 = reconstruct_version(v0, &patches_json, 1).unwrap();
133        assert_eq!(at_v1, v1);
134
135        let at_v2 = reconstruct_version(v0, &patches_json, 2).unwrap();
136        assert_eq!(at_v2, v2);
137    }
138
139    #[test]
140    fn test_squash_patches_produces_single_diff() {
141        let v0 = "line 1\n";
142        let v1 = "line 1\nline 2\n";
143        let v2 = "line 1\nline 2\nline 3\n";
144
145        let p1 = create_patch(v0, v1);
146        let p2 = create_patch(v1, v2);
147        let patches_json = serde_json::to_string(&vec![p1, p2]).unwrap();
148
149        let squashed = squash_patches(v0, &patches_json).unwrap();
150        let result = apply_patch(v0, &squashed).unwrap();
151        assert_eq!(result, v2);
152    }
153
154    #[test]
155    fn test_reconstruct_version_native() {
156        let v0 = "line 1\n";
157        let v1 = "line 1\nline 2\n";
158        let v2 = "line 1\nline 2\nline 3\n";
159
160        let patches = vec![create_patch(v0, v1), create_patch(v1, v2)];
161        let at_v2 = reconstruct_version_native(v0, &patches, 2).unwrap();
162        assert_eq!(at_v2, v2);
163    }
164
165    #[test]
166    fn test_squash_patches_native() {
167        let v0 = "line 1\n";
168        let v1 = "line 1\nline 2\n";
169        let v2 = "line 1\nline 2\nline 3\n";
170
171        let patches = vec![create_patch(v0, v1), create_patch(v1, v2)];
172        let squashed = squash_patches_native(v0, &patches).unwrap();
173        let result = apply_patch(v0, &squashed).unwrap();
174        assert_eq!(result, v2);
175    }
176
177    #[test]
178    fn test_apply_conflicting_patch() {
179        let original = "Hello world. This is a test.\n";
180        let modified = "Hello beautiful world. This is an awesome test.\n";
181        let patch = create_patch(original, modified);
182
183        let result = apply_patch("Completely different text\n", &patch);
184        assert!(result.is_err());
185    }
186}