Skip to main content

fixture_manager/
context.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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            // In force mode, treat the on-disk contents as missing.
79            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                // File doesn't exist: treat as changed.
118                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                // Don't fail if the file wasn't found.
149                return Ok(None);
150            }
151            return Err(err.into());
152        }
153    };
154
155    Ok(Some(contents))
156}