1use eyre::ContextCompat as _;
4use std::{
5 fs,
6 path::{self, Path, PathBuf},
7};
8
9use itertools::Itertools as _;
10use tap::Pipe as _;
11use walkdir::WalkDir;
12
13use crate::{
14 config::Config,
15 output_path::OutputPath,
16 stdx::{self, PathExt as _},
17};
18
19use std::collections::BTreeMap;
20
21use crate::analysis::Analysis;
22use crate::config::GITHUB;
23use crate::config::Marker;
24
25use eyre::{Context as _, Error, Result, bail, eyre};
26use handlebars::Handlebars;
27use simply_colored::*;
28
29#[derive(Debug)]
32pub struct World {
33 pub root: PathBuf,
35 pub links: Vec<Link>,
37 pub files: Vec<File>,
39}
40
41#[derive(Debug)]
43pub struct Link {
44 pub url: String,
46 pub contents: String,
48 pub path: PathBuf,
51 pub sha256: Option<String>,
53 pub marker: Option<String>,
57}
58
59#[derive(Debug)]
61pub struct File {
62 pub old_location: PathBuf,
64 pub contents: String,
66 pub output: OutputPath,
68 pub input: PathBuf,
70}
71
72impl World {
73 pub fn process(self) -> Result<Analysis, Vec<Error>> {
77 let mut errors = vec![];
78
79 let links = self
80 .links
81 .into_iter()
82 .map(
83 |Link {
84 contents,
85 path,
86 sha256,
87 marker,
88 url,
89 }| {
90 let actual_sha256 = sha256::digest(&contents);
91
92 if let Some(expected_sha256) = sha256
93 && actual_sha256 != *expected_sha256
94 {
95 let mismatch = format!("link {BLUE}{url}{RESET}");
96 let actual = format!("actual {CYAN}{actual_sha256}{RESET}");
97 let expected = format!("expected {CYAN}{expected_sha256}{RESET}");
98 bail!("hash mismatch\n {mismatch}\n {actual}\n {expected}");
99 }
100
101 let path = self.root.join(path);
103
104 let marker = marker.as_ref().map_or(String::new(), |marker_args| {
106 format!("{}{marker_args}", Marker::MARKER)
107 .pipe(|marker| commented::comment(marker, &path))
108 + "\n"
109 });
110
111 let file_contents = format!("{marker}{contents}");
112
113 let marker = file_contents
114 .lines()
115 .next()
116 .filter(|line| line.contains(Marker::MARKER))
117 .map(|line| format!("{line}\n"))
118 .unwrap_or_default();
119
120 let contents = if let Some(first_line) = file_contents.lines().next()
121 && first_line.contains(Marker::MARKER)
122 {
123 file_contents.lines().skip(1).collect::<Vec<_>>().join("\n")
124 } else {
125 file_contents.to_string()
126 };
127
128 let generated_notice = [
129 format!("@generated by `{}` <{GITHUB}>", Marker::MARKER),
130 "Do not edit by hand.".to_string(),
131 String::new(),
132 format!("downloaded from: {url}"),
133 ]
134 .into_iter()
135 .fold(String::new(), |previous_lines, line| {
136 format!("{previous_lines}{}\n", commented::comment(line, &path))
137 });
138
139 let contents = format!("{marker}{generated_notice}{contents}");
140
141 Ok(crate::analysis::WritePath { path, contents })
142 },
143 )
144 .partition_result::<Vec<_>, Vec<_>, _, _>()
145 .pipe(|(oks, errs)| {
146 errors.extend(errs);
147 oks
148 });
149
150 let files = self
151 .files
152 .into_iter()
153 .map(
154 |File {
155 old_location,
156 contents,
157 output,
158 input,
159 }| {
160 let relative_location = old_location
161 .strip_prefix(&self.root)?
162 .strip_prefix(&input)?;
163
164 let (file_contents, new_location) = if let Some(first_line) =
165 contents.lines().next()
166 && let Some(marker_start_pos) = first_line.find(Marker::MARKER)
167 && let Some(marker_args) =
168 first_line.get(marker_start_pos + Marker::MARKER.len()..)
169 && let Ok(args) = marker_args.parse::<Marker>()
170 && let Some(path) = args.path
171 {
172 (
173 contents.lines().skip(1).collect_vec().join(","),
175 path,
176 )
177 } else {
178 (
179 contents,
180 output
181 .as_ref()
182 .join(relative_location)
183 .pipe(OutputPath::new),
184 )
185 };
186
187 let mut handlebars = Handlebars::new();
188 handlebars
189 .register_template_string("t1", file_contents)
190 .with_context(|| eyre!("failed to parse template for {new_location}"))?;
191
192 let contents = handlebars
193 .render("t1", &BTreeMap::<u8, u8>::new())
194 .with_context(|| eyre!("failed to render template for {new_location}"))?;
195
196 Ok::<_, Error>(crate::analysis::WritePath {
197 path: new_location.into_inner(),
198 contents,
199 })
200 },
201 )
202 .partition_result::<Vec<_>, Vec<_>, _, _>()
203 .pipe(|(oks, errs)| {
204 errors.extend(errs);
205 oks
206 });
207
208 if !errors.is_empty() {
209 return Err(errors);
210 }
211
212 Ok(Analysis {
213 writes: links.into_iter().chain(files).collect(),
214 })
215 }
216
217 pub fn new(cwd: &Path) -> Result<Self, Vec<Error>> {
219 let root = cwd
221 .pipe_ref(stdx::traverse_upwards)
222 .find(|dir| dir.join(Config::FILE_NAME).exists())
223 .with_context(|| {
224 eyre!(
225 "failed to find directory that contains a `{}`. traversed upwards from {}",
226 Config::FILE_NAME,
227 cwd.show()
228 )
229 })
230 .map_err(single_err)?;
231
232 let config = root
233 .join(Config::FILE_NAME)
234 .pipe(fs::read_to_string)
235 .with_context(|| eyre!("failed to read config file {}", Config::FILE_NAME))
236 .map_err(single_err)?
237 .pipe_deref(toml::de::from_str::<Config>)
238 .context("failed to parse config file")
239 .map_err(single_err)?
240 .pipe(|mut conf| {
241 conf.root = root;
242 conf
243 });
244
245 let mut errors = vec![];
246
247 let links = config
248 .links
249 .into_iter()
250 .map(
251 |crate::config::Link {
252 url,
253 path,
254 sha256,
255 marker,
256 }| {
257 Ok::<_, Error>(Link {
258 contents: ureq::get(&url).call()?.body_mut().read_to_string()?,
259 path,
260 sha256,
261 marker,
262 url,
263 })
264 },
265 )
266 .partition_result::<Vec<_>, Vec<_>, _, _>()
267 .pipe(|(oks, errs)| {
268 errors.extend(errs);
269 oks
270 });
271
272 let files = config
273 .dirs
274 .into_iter()
275 .flat_map(|crate::config::Dir { input, output }| {
276 WalkDir::new(config.root.join(&input))
277 .into_iter()
278 .flatten()
279 .filter(|dir_entry| dir_entry.file_type().is_file())
280 .map(move |file| {
281 let old_location = path::absolute(file.path())?;
283
284 let contents = fs::read_to_string(&old_location).with_context(|| {
285 eyre!("failed to read path {}", old_location.show())
286 })?;
287
288 Ok::<_, Error>(File {
289 old_location,
290 contents,
291 output: output.clone(),
292 input: input.clone(),
293 })
294 })
295 })
296 .partition_result::<Vec<_>, Vec<_>, _, _>()
297 .pipe(|(oks, errs)| {
298 errors.extend(errs);
299 oks
300 });
301
302 if !errors.is_empty() {
303 return Err(errors);
304 }
305
306 Ok(Self {
307 root: config.root,
308 links,
309 files,
310 })
311 }
312}
313
314fn single_err(err: impl Into<Error>) -> Vec<Error> {
318 vec![err.into()]
319}