smart_patcher/
patch.rs

1use difference_rs::Changeset;
2use serde::{Deserialize, Serialize};
3use std::fs::{self, File};
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use walkdir::WalkDir;
7
8use crate::regex::RegexIR;
9
10#[cfg(feature = "lua")]
11use crate::lua_exec;
12#[cfg(feature = "rhai")]
13use crate::rhai_exec;
14use crate::sh_exec;
15
16#[derive(Deserialize, Serialize, PartialEq, Default, Clone)]
17/// Patch file structure,
18///
19/// Each `*.patch` file may contain several individual patches.
20pub struct PatchFile {
21  /// List of individual patches.
22  pub patches: Vec<Patch>,
23}
24
25#[derive(Deserialize, Serialize, PartialEq, Default, Clone)]
26/// Patch definition.
27pub struct Patch {
28  /// Rules to define patchable files.
29  pub files: Vec<FilePath>,
30  #[serde(skip_serializing_if = "Option::is_none")]
31  /// Decode file content by decoder before patching.
32  ///
33  /// Useful on patching non-plain text or even non-text files.
34  pub decoder: Option<DecodeBy>,
35  #[serde(skip_serializing_if = "Option::is_none")]
36  /// Encode file content by encoder after patching.
37  ///
38  /// Useful on patching non-plain text or even non-text files.
39  pub encoder: Option<EncodeBy>,
40  /// List of rules to define patch area and cursor position.
41  ///
42  /// Allows to patch only defined content area.
43  pub patch_area: Vec<AreaRule>,
44  #[serde(skip_serializing_if = "Option::is_none")]
45  /// Replaces some content inside patch area.
46  pub replace: Option<Replacer>,
47  #[serde(skip_serializing_if = "Option::is_none")]
48  /// Inserts some content at cursor position.
49  pub insert: Option<String>,
50}
51
52#[derive(Deserialize, Serialize, PartialEq, Clone)]
53#[serde(rename_all = "snake_case")]
54/// Rule for patch area and cursor position definition.
55pub enum AreaRule {
56  /// Select given file to patch only if it contains some expression.
57  Contains(RegexIR),
58  /// Select given file to patch only if it doesn't contain some expression.
59  NotContains(RegexIR),
60  /// Crops patch area to the beginning of some expression and sets cursor to the end.
61  Before(RegexIR),
62  /// Crops patch area to the ending of some expression and sets cursor to the beginning.
63  After(RegexIR),
64  /// Explicitly sets cursor to the beginning.
65  CursorAtBegin,
66  /// Explicitly sets cursor to the end.
67  CursorAtEnd,
68  /// Find patch area by Lua script.
69  FindByLua(PathBuf),
70  /// Find patch area by Rhai script.
71  FindByRhai(PathBuf),
72  /// Find patch area by shell script.
73  FindBySh(PathBuf),
74}
75
76/// Decoder definition.
77#[derive(Deserialize, Serialize, PartialEq, Clone)]
78#[serde(rename_all = "snake_case")]
79pub enum DecodeBy {
80  /// Decode file content by Lua script.
81  Lua(PathBuf),
82  /// Decode file content by Rhai script.
83  Rhai(PathBuf),
84  /// Decode file content by shell script.
85  Sh(PathBuf),
86}
87
88/// Encoder definition.
89#[derive(Deserialize, Serialize, PartialEq, Clone)]
90#[serde(rename_all = "snake_case")]
91pub enum EncodeBy {
92  /// Encode file content by Lua script.
93  Lua(PathBuf),
94  /// Encode file content by Rhai script.
95  Rhai(PathBuf),
96  /// Encode file content by shell script.
97  Sh(PathBuf),
98}
99
100/// Replacer definition.
101#[derive(Deserialize, Serialize, PartialEq, Clone)]
102#[serde(rename_all = "snake_case")]
103pub enum Replacer {
104  /// Replaces one text by another.
105  FromTo(String, String),
106  /// Replaces all non-overlapping matches in the haystack with the replacement provided.
107  ///
108  /// See [Regex::replace_all](https://docs.rs/regex/latest/regex/struct.Regex.html#method.replace_all).
109  RegexTo(RegexIR, String),
110  /// Transforms patch area by Lua script.
111  ByLua(PathBuf),
112  /// Transforms patch area by Rhai script.
113  ByRhai(PathBuf),
114  /// Transforms patch area by shell script.
115  BySh(PathBuf),
116}
117
118#[derive(Deserialize, Serialize, PartialEq, Clone)]
119#[serde(rename_all = "snake_case")]
120/// File path definition.
121pub enum FilePath {
122  /// Just given path (relative or absolute).
123  Just(PathBuf),
124  /// Regular expression rule to find files.
125  ///
126  /// See [Regex::is_match](https://docs.rs/regex/latest/regex/struct.Regex.html#method.is_match).
127  Re(RegexIR),
128}
129
130enum CursorType {
131  Start,
132  End,
133}
134
135impl PatchFile {
136  /// Runs all patches and returns number of patches applications.
137  pub fn patch(&self, root: &Path, patch_dir: &Path) -> anyhow::Result<usize> {
138    let mut cntr = 0;
139
140    if root.is_dir() {
141      for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
142        let path = entry.path();
143        if !path.is_file() {
144          continue;
145        }
146        for patch in &self.patches {
147          if self.should_process_file(path, &patch.files) {
148            let content = PatchFile::read(path, patch_dir, &patch.decoder)?;
149            if let Some(res) = PatchFile::apply_patch(content, patch_dir, patch, &mut cntr)? {
150              PatchFile::write(path, patch_dir, &patch.encoder, &res)?;
151            }
152          }
153        }
154      }
155    } else {
156      for patch in &self.patches {
157        if self.should_process_file(root, &patch.files) {
158          let content = PatchFile::read(root, patch_dir, &patch.decoder)?;
159          if let Some(res) = PatchFile::apply_patch(content, patch_dir, patch, &mut cntr)? {
160            PatchFile::write(root, patch_dir, &patch.encoder, &res)?;
161          }
162        }
163      }
164    }
165
166    Ok(cntr)
167  }
168
169  /// Tests patch file and returns number of possible patches applications.
170  pub fn test(&self, root: &Path, patch_dir: &Path) -> anyhow::Result<usize> {
171    let mut cntr = 0;
172
173    if root.is_dir() {
174      for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
175        let path = entry.path();
176        if !path.is_file() {
177          continue;
178        }
179        for patch in &self.patches {
180          if self.should_process_file(path, &patch.files) {
181            let content = PatchFile::read(path, patch_dir, &patch.decoder)?;
182            if let Some(res) = PatchFile::apply_patch(content.clone(), patch_dir, patch, &mut cntr)? {
183              let diffs = Changeset::new(&content, &res, "");
184              println!("In file {path:?}:");
185              println!("{diffs}");
186              println!();
187            }
188          }
189        }
190      }
191    } else {
192      let path = root;
193      for patch in &self.patches {
194        if self.should_process_file(path, &patch.files) {
195          let content = PatchFile::read(path, patch_dir, &patch.decoder)?;
196          if let Some(res) = PatchFile::apply_patch(content.clone(), patch_dir, patch, &mut cntr)? {
197            let diffs = Changeset::new(&content, &res, "");
198            println!("In file {path:?}:");
199            println!("{diffs}");
200            println!();
201          }
202        }
203      }
204    }
205
206    Ok(cntr)
207  }
208
209  fn should_process_file(&self, path: &Path, file_patterns: &[FilePath]) -> bool {
210    if file_patterns.is_empty() {
211      return true;
212    }
213
214    let path_str = path.to_string_lossy();
215
216    file_patterns.iter().any(|pattern| match pattern {
217      FilePath::Just(exact_path) => path.ends_with(exact_path),
218      FilePath::Re(regex) => regex.inner().is_match(&path_str),
219    })
220  }
221
222  fn read(path: &Path, _patch_dir: &Path, decoder: &Option<DecodeBy>) -> anyhow::Result<String> {
223    #[allow(unreachable_patterns)]
224    match decoder {
225      None => {
226        let mut content = String::new();
227        File::open(path)?.read_to_string(&mut content)?;
228        Ok(content)
229      }
230      Some(DecodeBy::Sh(script)) => Ok(sh_exec::decode_by(_patch_dir, script, path.canonicalize()?)?),
231      #[cfg(feature = "lua")]
232      Some(DecodeBy::Lua(script)) => {
233        let mut content = vec![];
234        File::open(path)?.read_to_end(&mut content)?;
235        Ok(lua_exec::decode_by(_patch_dir, script, &content)?)
236      }
237      #[cfg(feature = "rhai")]
238      Some(DecodeBy::Rhai(script)) => {
239        let mut content = vec![];
240        File::open(path)?.read_to_end(&mut content)?;
241        Ok(rhai_exec::decode_by(_patch_dir, script, &content)?)
242      }
243      Some(_) => {
244        anyhow::bail!("Given decoder variant is unsupported. Build `smart-patcher` with `lua` or `rhai` feature.")
245      }
246    }
247  }
248
249  fn write(path: &Path, _patch_dir: &Path, encoder: &Option<EncodeBy>, content: &str) -> anyhow::Result<()> {
250    #[allow(unreachable_patterns)]
251    match encoder {
252      None => {
253        fs::write(path, content)?;
254        Ok(())
255      }
256      Some(EncodeBy::Sh(script)) => {
257        sh_exec::encode_by(_patch_dir, script, content, path.canonicalize()?)?;
258        Ok(())
259      }
260      #[cfg(feature = "lua")]
261      Some(EncodeBy::Lua(script)) => {
262        let content = lua_exec::encode_by(_patch_dir, script, content)?;
263        fs::write(path, content)?;
264        Ok(())
265      }
266      #[cfg(feature = "rhai")]
267      Some(EncodeBy::Rhai(script)) => {
268        let content = rhai_exec::encode_by(_patch_dir, script, content)?;
269        fs::write(path, content)?;
270        Ok(())
271      }
272      Some(_) => {
273        anyhow::bail!("Given encoder variant is unsupported. Build `smart-patcher` with `lua` or `rhai` feature.")
274      }
275    }
276  }
277
278  fn apply_patch(content: String, patch_dir: &Path, patch: &Patch, cntr: &mut usize) -> anyhow::Result<Option<String>> {
279    if let Some((range, cursor)) = PatchFile::find_section(&content, patch_dir, &patch.patch_area)? {
280      let before = &content[..range.start];
281      let after = &content[range.end..];
282
283      let mut middle = content[range.start..range.end].to_owned();
284      if !middle.is_empty()
285        && let Some(replacer) = &patch.replace
286      {
287        #[allow(unreachable_patterns)]
288        match replacer {
289          Replacer::FromTo(from, to) => {
290            middle = middle.replace(from, to);
291          }
292          Replacer::RegexTo(from_re, to) => {
293            middle = from_re.replace_all(&middle, to.as_str()).to_string();
294          }
295          Replacer::BySh(path) => {
296            middle = sh_exec::replace_by(patch_dir, path, &middle)?;
297          }
298          #[cfg(feature = "lua")]
299          Replacer::ByLua(path) => {
300            middle = lua_exec::replace_by(patch_dir, path, &middle)?;
301          }
302          #[cfg(feature = "rhai")]
303          Replacer::ByRhai(path) => {
304            middle = rhai_exec::replace_by(patch_dir, path, &middle)?;
305          }
306          _ => {
307            anyhow::bail!("Given replacer variant is unsupported. Build `smart-patcher` with `lua` or `rhai` feature.")
308          }
309        }
310      }
311      if let Some(insert) = &patch.insert {
312        if middle.is_empty() {
313          middle = insert.to_owned();
314        } else {
315          middle = match cursor {
316            CursorType::Start => insert.to_owned() + middle.as_str(),
317            CursorType::End => middle + insert.as_str(),
318          }
319        }
320      }
321
322      let res = format!("{before}{middle}{after}");
323
324      #[cfg(test)]
325      {
326        println!("{}", content);
327        println!("{}", res);
328      }
329
330      if !res.as_str().eq(content.as_str()) {
331        *cntr += 1;
332      }
333      return Ok(Some(res));
334    }
335
336    Ok(None)
337  }
338
339  fn find_section(
340    content: &str,
341    _patch_dir: &Path,
342    rules: &[AreaRule],
343  ) -> anyhow::Result<Option<(std::ops::Range<usize>, CursorType)>> {
344    let mut current_pos = 0;
345    let mut start_pos = None;
346    let mut end_pos = None;
347    let mut cursor = CursorType::Start;
348
349    for rule in rules {
350      #[allow(unreachable_patterns)]
351      match &rule {
352        AreaRule::Contains(regex) => {
353          if !regex.inner().is_match(&content[current_pos..]) {
354            return Ok(None);
355          }
356        }
357        AreaRule::NotContains(regex) => {
358          if regex.inner().is_match(&content[current_pos..]) {
359            return Ok(None);
360          }
361        }
362        AreaRule::Before(regex) => {
363          cursor = CursorType::End;
364
365          if let Some(mat) = regex.inner().find(&content[current_pos..]) {
366            end_pos = Some(current_pos + mat.start());
367          } else {
368            return Ok(None);
369          }
370        }
371        AreaRule::After(regex) => {
372          cursor = CursorType::Start;
373
374          if let Some(mat) = regex.inner().find(&content[current_pos..]) {
375            start_pos = Some(current_pos + mat.end());
376            current_pos += mat.end();
377          } else {
378            return Ok(None);
379          }
380        }
381        AreaRule::CursorAtBegin => {
382          cursor = CursorType::Start;
383        }
384        AreaRule::CursorAtEnd => {
385          cursor = CursorType::End;
386        }
387        AreaRule::FindBySh(path) => {
388          let (new_start, new_end, cursor_at_end) = sh_exec::find_by(_patch_dir, path, content, start_pos, end_pos)?;
389
390          if new_start.is_some() {
391            start_pos = new_start;
392          }
393          if new_end.is_some() {
394            end_pos = new_end;
395          }
396
397          if cursor_at_end {
398            cursor = CursorType::End;
399          } else {
400            cursor = CursorType::Start;
401          }
402        }
403        #[cfg(feature = "lua")]
404        AreaRule::FindByLua(path) => {
405          let (new_start, new_end, cursor_at_end) = lua_exec::find_by(_patch_dir, path, content, start_pos, end_pos)?;
406
407          if start_pos.is_none() && new_start != 0 {
408            start_pos = Some(new_start);
409          }
410          if end_pos.is_none() && new_end != content.len() {
411            end_pos = Some(new_end);
412          }
413
414          if cursor_at_end {
415            cursor = CursorType::End;
416          } else {
417            cursor = CursorType::Start;
418          }
419        }
420        #[cfg(feature = "rhai")]
421        AreaRule::FindByRhai(path) => {
422          let (new_start, new_end, cursor_at_end) = rhai_exec::find_by(_patch_dir, path, content, start_pos, end_pos)?;
423
424          if start_pos.is_none() && new_start != 0 {
425            start_pos = Some(new_start);
426          }
427          if end_pos.is_none() && new_end != content.len() {
428            end_pos = Some(new_end);
429          }
430
431          if cursor_at_end {
432            cursor = CursorType::End;
433          } else {
434            cursor = CursorType::Start;
435          }
436        }
437        _ => {
438          anyhow::bail!("Given find rule variant is unsupported. Build `smart-patcher` with `lua` or `rhai` feature.")
439        }
440      }
441    }
442
443    let range = match (start_pos, end_pos) {
444      (Some(start), Some(end)) if start <= end => Some(start..end),
445      (Some(start), None) => Some(start..content.len()),
446      (None, Some(end)) => Some(0..end),
447      _ => Some(0..content.len()),
448    };
449
450    if let Some(range) = range {
451      Ok(Some((range, cursor)))
452    } else {
453      Ok(None)
454    }
455  }
456}
457
458#[cfg(test)]
459mod tests {
460  use super::*;
461
462  #[test]
463  fn find_v1() -> anyhow::Result<()> {
464    let patch = Patch {
465      files: vec![],
466      patch_area: vec![AreaRule::After(RegexIR::new("media")?)],
467      replace: Some(Replacer::FromTo(String::from("ttt"), String::from("yyy"))),
468      insert: None,
469      decoder: None,
470      encoder: None,
471    };
472
473    let content = String::from("ttt is a new media: there is only ttt");
474    let mut cntr = 0;
475    let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
476    assert_eq!(res, Some(String::from("ttt is a new media: there is only yyy")));
477    assert_eq!(cntr, 1);
478
479    Ok(())
480  }
481
482  #[test]
483  fn find_v2() -> anyhow::Result<()> {
484    let patch = Patch {
485      files: vec![],
486      patch_area: vec![AreaRule::After(RegexIR::new("media")?)],
487      replace: None,
488      insert: Some(String::from(" v2")),
489      decoder: None,
490      encoder: None,
491    };
492
493    let content = String::from("ttt is a new media: there is only ttt");
494    let mut cntr = 0;
495    let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
496    assert_eq!(res, Some(String::from("ttt is a new media v2: there is only ttt")));
497    assert_eq!(cntr, 1);
498
499    Ok(())
500  }
501
502  #[test]
503  fn find_v3() -> anyhow::Result<()> {
504    let patch = Patch {
505      files: vec![],
506      patch_area: vec![AreaRule::Before(RegexIR::new("media")?)],
507      replace: None,
508      insert: Some(String::from("v2 ")),
509      decoder: None,
510      encoder: None,
511    };
512
513    let content = String::from("ttt is a new media: there is only ttt");
514    let mut cntr = 0;
515    let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
516    assert_eq!(res, Some(String::from("ttt is a new v2 media: there is only ttt")));
517    assert_eq!(cntr, 1);
518
519    Ok(())
520  }
521
522  #[test]
523  fn find_v4() -> anyhow::Result<()> {
524    let patch = Patch {
525      files: vec![],
526      patch_area: vec![
527        AreaRule::Before(RegexIR::new(": there")?),
528        AreaRule::After(RegexIR::new("new ")?),
529        AreaRule::FindBySh(PathBuf::from("tests/test_v4.py")),
530      ],
531      replace: None,
532      insert: Some(String::from(" v2")),
533      decoder: None,
534      encoder: None,
535    };
536
537    let content = String::from("ttt is a new media: there is only ttt");
538    let mut cntr = 0;
539    let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
540    assert_eq!(res, Some(String::from("ttt is a new media v2: there is only ttt")));
541    assert_eq!(cntr, 1);
542
543    Ok(())
544  }
545
546  #[test]
547  fn find_v5() -> anyhow::Result<()> {
548    let patch = Patch {
549      files: vec![FilePath::Just(PathBuf::from("test_v5.docx"))],
550      patch_area: vec![],
551      replace: Some(Replacer::FromTo("game".to_string(), "rock".to_string())),
552      insert: None,
553      decoder: Some(DecodeBy::Sh(PathBuf::from("tests/test_v5.py"))),
554      encoder: Some(EncodeBy::Sh(PathBuf::from("tests/test_v5.py"))),
555    };
556
557    let pf = PatchFile { patches: vec![patch] };
558    let cntr = pf.patch(&PathBuf::from("tests"), &PathBuf::from("."))?;
559
560    assert_eq!(cntr, 1);
561
562    Ok(())
563  }
564
565  #[test]
566  #[cfg(feature = "lua")]
567  fn find_v6() -> anyhow::Result<()> {
568    let patch = Patch {
569      files: vec![],
570      patch_area: vec![
571        AreaRule::Before(RegexIR::new(": there")?),
572        AreaRule::After(RegexIR::new("new ")?),
573        AreaRule::FindByLua(PathBuf::from("tests/test_v6.lua")),
574      ],
575      replace: None,
576      insert: Some(String::from(" v2")),
577      decoder: None,
578      encoder: None,
579    };
580
581    let content = String::from("ttt is a new media: there is only ttt");
582    let mut cntr = 0;
583    let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
584    assert_eq!(res, Some(String::from("ttt is a new media v2: there is only ttt")));
585    assert_eq!(cntr, 1);
586
587    Ok(())
588  }
589
590  #[test]
591  #[cfg(feature = "rhai")]
592  fn find_v7() -> anyhow::Result<()> {
593    let patch = Patch {
594      files: vec![],
595      patch_area: vec![
596        AreaRule::Before(RegexIR::new(": there")?),
597        AreaRule::After(RegexIR::new("new ")?),
598        AreaRule::FindByRhai(PathBuf::from("tests/test_v7.rhai")),
599      ],
600      replace: None,
601      insert: Some(String::from(" v2")),
602      decoder: None,
603      encoder: None,
604    };
605
606    let content = String::from("ttt is a new media: there is only ttt");
607    let mut cntr = 0;
608    let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
609    assert_eq!(res, Some(String::from("ttt is a new media v2: there is only ttt")));
610    assert_eq!(cntr, 1);
611
612    Ok(())
613  }
614
615  #[test]
616  fn find_v8() -> anyhow::Result<()> {
617    let patch = Patch {
618      files: vec![],
619      patch_area: vec![],
620      replace: Some(Replacer::BySh(PathBuf::from("tests/test_v8.py"))),
621      insert: None,
622      decoder: None,
623      encoder: None,
624    };
625
626    let content = String::from("This is my number: +18235123154");
627    let mut cntr = 0;
628    let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
629    assert_eq!(res, Some(String::from("This is my number: +28235223254")));
630    assert_eq!(cntr, 1);
631
632    Ok(())
633  }
634
635  #[test]
636  fn find_v9() -> anyhow::Result<()> {
637    let patch = Patch {
638      files: vec![],
639      patch_area: vec![],
640      replace: Some(Replacer::BySh(PathBuf::from("tests/test_v9.py"))),
641      insert: None,
642      decoder: None,
643      encoder: None,
644    };
645
646    let content = String::from("This is my project name: {1}");
647    let mut cntr = 0;
648    let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
649    assert_eq!(res, Some(String::from("This is my project name: smart-patcher")));
650    assert_eq!(cntr, 1);
651
652    Ok(())
653  }
654
655  #[test]
656  fn find_v10() -> anyhow::Result<()> {
657    let patch = Patch {
658      files: vec![],
659      patch_area: vec![
660        AreaRule::Before(RegexIR::new(": there")?),
661        AreaRule::After(RegexIR::new("new ")?),
662        AreaRule::CursorAtEnd,
663      ],
664      replace: None,
665      insert: Some(String::from(" v2")),
666      decoder: None,
667      encoder: None,
668    };
669
670    let content = String::from("ttt is a new media: there is only ttt");
671    let mut cntr = 0;
672    let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
673    assert_eq!(res, Some(String::from("ttt is a new media v2: there is only ttt")));
674    assert_eq!(cntr, 1);
675
676    Ok(())
677  }
678}
679
680#[cfg(feature = "generate-patches")]
681#[allow(dead_code)]
682pub(crate) fn generate_examples() -> anyhow::Result<()> {
683  use std::io::BufWriter;
684
685  if !PathBuf::from("examples").exists() {
686    fs::create_dir("examples")?;
687  }
688
689  let patch = Patch {
690    files: vec![],
691    patch_area: vec![AreaRule::After(RegexIR::new("media")?)],
692    replace: Some(Replacer::FromTo(String::from("ttt"), String::from("yyy"))),
693    insert: None,
694    decoder: None,
695    encoder: None,
696  };
697  let pf = PatchFile { patches: vec![patch] };
698  let f = File::create("examples/patch1.json")?;
699  let buf = BufWriter::new(f);
700  serde_json::to_writer_pretty(buf, &pf)?;
701
702  let patch = Patch {
703    files: vec![],
704    patch_area: vec![AreaRule::After(RegexIR::new("media")?)],
705    replace: None,
706    insert: Some(String::from(" v2")),
707    decoder: None,
708    encoder: None,
709  };
710  let pf = PatchFile { patches: vec![patch] };
711  let f = File::create("examples/patch2.json")?;
712  let buf = BufWriter::new(f);
713  serde_json::to_writer_pretty(buf, &pf)?;
714
715  let patch = Patch {
716    files: vec![],
717    patch_area: vec![AreaRule::Before(RegexIR::new("media")?)],
718    replace: None,
719    insert: Some(String::from("v2 ")),
720    decoder: None,
721    encoder: None,
722  };
723  let pf = PatchFile { patches: vec![patch] };
724  let f = File::create("examples/patch3.json")?;
725  let buf = BufWriter::new(f);
726  serde_json::to_writer_pretty(buf, &pf)?;
727
728  let patch = Patch {
729    files: vec![],
730    patch_area: vec![
731      AreaRule::Before(RegexIR::new(": there")?),
732      AreaRule::After(RegexIR::new("new ")?),
733      AreaRule::FindBySh(PathBuf::from("../tests/test_v4.py")),
734    ],
735    replace: None,
736    insert: Some(String::from(" v2")),
737    decoder: None,
738    encoder: None,
739  };
740  let pf = PatchFile { patches: vec![patch] };
741  let f = File::create("examples/patch4.json")?;
742  let buf = BufWriter::new(f);
743  serde_json::to_writer_pretty(buf, &pf)?;
744
745  let patch = Patch {
746    files: vec![FilePath::Just(PathBuf::from("test_v5.docx"))],
747    patch_area: vec![],
748    replace: Some(Replacer::FromTo("game".to_string(), "rock".to_string())),
749    insert: None,
750    decoder: Some(DecodeBy::Sh(PathBuf::from("../tests/test_v5.py"))),
751    encoder: Some(EncodeBy::Sh(PathBuf::from("../tests/test_v5.py"))),
752  };
753  let pf = PatchFile { patches: vec![patch] };
754  let f = File::create("examples/patch5.json")?;
755  let buf = BufWriter::new(f);
756  serde_json::to_writer_pretty(buf, &pf)?;
757
758  let patch = Patch {
759    files: vec![],
760    patch_area: vec![
761      AreaRule::Before(RegexIR::new(": there")?),
762      AreaRule::After(RegexIR::new("new ")?),
763      AreaRule::FindByLua(PathBuf::from("../tests/test_v6.lua")),
764    ],
765    replace: None,
766    insert: Some(String::from(" v2")),
767    decoder: None,
768    encoder: None,
769  };
770  let pf = PatchFile { patches: vec![patch] };
771  let f = File::create("examples/patch6.json")?;
772  let buf = BufWriter::new(f);
773  serde_json::to_writer_pretty(buf, &pf)?;
774
775  let patch = Patch {
776    files: vec![],
777    patch_area: vec![
778      AreaRule::Before(RegexIR::new(": there")?),
779      AreaRule::After(RegexIR::new("new ")?),
780      AreaRule::FindByRhai(PathBuf::from("../tests/test_v7.rhai")),
781    ],
782    replace: None,
783    insert: Some(String::from(" v2")),
784    decoder: None,
785    encoder: None,
786  };
787  let pf = PatchFile { patches: vec![patch] };
788  let f = File::create("examples/patch7.json")?;
789  let buf = BufWriter::new(f);
790  serde_json::to_writer_pretty(buf, &pf)?;
791
792  let patch = Patch {
793    files: vec![],
794    patch_area: vec![],
795    replace: Some(Replacer::BySh(PathBuf::from("../tests/test_v8.py"))),
796    insert: None,
797    decoder: None,
798    encoder: None,
799  };
800  let pf = PatchFile { patches: vec![patch] };
801  let f = File::create("examples/patch8.json")?;
802  let buf = BufWriter::new(f);
803  serde_json::to_writer_pretty(buf, &pf)?;
804
805  let patch = Patch {
806    files: vec![],
807    patch_area: vec![],
808    replace: Some(Replacer::BySh(PathBuf::from("../tests/test_v9.py"))),
809    insert: None,
810    decoder: None,
811    encoder: None,
812  };
813  let pf = PatchFile { patches: vec![patch] };
814  let f = File::create("examples/patch9.json")?;
815  let buf = BufWriter::new(f);
816  serde_json::to_writer_pretty(buf, &pf)?;
817
818  let patch = Patch {
819    files: vec![],
820    patch_area: vec![
821      AreaRule::Before(RegexIR::new(": there")?),
822      AreaRule::After(RegexIR::new("new ")?),
823      AreaRule::CursorAtEnd,
824    ],
825    replace: None,
826    insert: Some(String::from(" v2")),
827    decoder: None,
828    encoder: None,
829  };
830  let pf = PatchFile { patches: vec![patch] };
831  let f = File::create("examples/patch10.json")?;
832  let buf = BufWriter::new(f);
833  serde_json::to_writer_pretty(buf, &pf)?;
834
835  Ok(())
836}