fixture_manager/
context.rs

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