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