fixture_manager/
context.rs1use anyhow::{Result, bail};
5use camino::{Utf8Path, Utf8PathBuf};
6use fixtures::json::JsonFixture;
7
8pub trait ContextImpl<'g> {
9 type IterArgs;
10 type IterItem;
11 type Existing;
12
13 fn dir_name(fixture: &'g JsonFixture) -> Utf8PathBuf;
14 fn file_name(fixture: &'g JsonFixture, item: &Self::IterItem) -> String;
15
16 fn iter(
17 fixture: &'g JsonFixture,
18 args: &Self::IterArgs,
19 ) -> Box<dyn Iterator<Item = Self::IterItem> + 'g>;
20
21 fn parse_existing(path: &Utf8Path, contents: String) -> Result<Self::Existing>;
22 fn is_changed(item: &Self::IterItem, existing: &Self::Existing) -> bool;
23 fn diff(
24 fixture: &'g JsonFixture,
25 item: &Self::IterItem,
26 existing: Option<&Self::Existing>,
27 ) -> String;
28
29 fn write_to_string(
30 fixture: &'g JsonFixture,
31 item: &Self::IterItem,
32 out: &mut String,
33 ) -> Result<()>;
34}
35
36pub trait ContextDiff<'a> {}
37
38pub struct GenerateContext<'g, T: ContextImpl<'g>> {
39 fixture: &'g JsonFixture,
40 skip_existing: bool,
41 file_template: Utf8PathBuf,
42 iter: Box<dyn Iterator<Item = T::IterItem> + 'g>,
43}
44
45impl<'g, T: ContextImpl<'g>> GenerateContext<'g, T> {
46 pub fn new(fixture: &'g JsonFixture, args: &T::IterArgs, skip_existing: bool) -> Result<Self> {
47 let mut file_template = T::dir_name(fixture);
48 file_template.push("REPLACE_THIS_FILE_NAME");
49
50 std::fs::create_dir_all(
51 file_template
52 .parent()
53 .expect("file_template should not return root or prefix"),
54 )?;
55 let iter = T::iter(fixture, args);
56 Ok(Self {
57 fixture,
58 skip_existing,
59 file_template,
60 iter,
61 })
62 }
63}
64
65impl<'g, T: ContextImpl<'g>> Iterator for GenerateContext<'g, T> {
66 type Item = Result<ContextItem<'g, T>>;
67
68 fn next(&mut self) -> Option<Self::Item> {
69 let item = self.iter.next()?;
70
71 let mut path = self.file_template.clone();
72 path.set_file_name(T::file_name(self.fixture, &item));
73 let existing = if self.skip_existing {
74 None
76 } else {
77 match read_contents(&path) {
78 Ok(Some(contents)) => match T::parse_existing(&path, contents) {
79 Ok(existing) => Some(existing),
80 Err(err) => return Some(Err(err)),
81 },
82 Ok(None) => None,
83 Err(err) => return Some(Err(err)),
84 }
85 };
86
87 Some(Ok(ContextItem {
88 fixture: self.fixture,
89 path,
90 item,
91 existing,
92 }))
93 }
94}
95
96pub struct ContextItem<'g, T: ContextImpl<'g>> {
97 fixture: &'g JsonFixture,
98 path: Utf8PathBuf,
99 item: T::IterItem,
100 existing: Option<T::Existing>,
101}
102
103impl<'g, T: ContextImpl<'g>> ContextItem<'g, T> {
104 pub fn path(&self) -> &Utf8Path {
105 &self.path
106 }
107
108 pub fn is_changed(&self) -> bool {
109 match &self.existing {
110 Some(existing) => T::is_changed(&self.item, existing),
111 None => {
112 true
114 }
115 }
116 }
117
118 pub fn diff(&self) -> String {
119 T::diff(self.fixture, &self.item, self.existing.as_ref())
120 }
121
122 pub fn write_to_path(&self) -> Result<()> {
123 let mut out = String::new();
124
125 if let Err(err) = T::write_to_string(self.fixture, &self.item, &mut out) {
126 eprintln!("** Partially generated output:\n{}", out);
127 bail!(
128 "Error while writing to string: {}\n\nPartially generated output:\n{}",
129 err,
130 out
131 );
132 }
133
134 Ok(std::fs::write(&self.path, &out)?)
135 }
136}
137
138fn read_contents(file: &Utf8Path) -> Result<Option<String>> {
139 let contents = match std::fs::read_to_string(file) {
140 Ok(data) => data,
141 Err(err) => {
142 if err.kind() == std::io::ErrorKind::NotFound {
143 return Ok(None);
145 }
146 return Err(err.into());
147 }
148 };
149
150 Ok(Some(contents))
151}