Skip to main content

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