use eyre::ContextCompat as _;
use std::{
fs,
path::{self, Path, PathBuf},
};
use itertools::Itertools as _;
use tap::Pipe as _;
use walkdir::WalkDir;
use crate::{
config::Config,
output_path::OutputPath,
stdx::{self, PathExt as _},
};
use std::collections::BTreeMap;
use crate::analysis::Analysis;
use crate::config::GITHUB;
use crate::config::Marker;
use eyre::{Context as _, Error, Result, bail, eyre};
use handlebars::Handlebars;
use simply_colored::*;
#[derive(Debug)]
pub struct World {
pub root: PathBuf,
pub links: Vec<Link>,
pub files: Vec<File>,
}
#[derive(Debug)]
pub struct Link {
pub url: String,
pub contents: String,
pub path: PathBuf,
pub sha256: Option<String>,
pub marker: Option<String>,
}
#[derive(Debug)]
pub struct File {
pub old_location: PathBuf,
pub contents: String,
pub output: OutputPath,
pub input: PathBuf,
}
impl World {
pub fn process(self) -> Result<Analysis, Vec<Error>> {
let mut errors = vec![];
let links = self
.links
.into_iter()
.map(
|Link {
contents,
path,
sha256,
marker,
url,
}| {
let actual_sha256 = sha256::digest(&contents);
if let Some(expected_sha256) = sha256
&& actual_sha256 != *expected_sha256
{
let mismatch = format!("link {BLUE}{url}{RESET}");
let actual = format!("actual {CYAN}{actual_sha256}{RESET}");
let expected = format!("expected {CYAN}{expected_sha256}{RESET}");
bail!("hash mismatch\n {mismatch}\n {actual}\n {expected}");
}
let path = self.root.join(path);
let marker = marker.as_ref().map_or(String::new(), |marker_args| {
format!("{}{marker_args}", Marker::MARKER)
.pipe(|marker| commented::comment(marker, &path))
+ "\n"
});
let file_contents = format!("{marker}{contents}");
let marker = file_contents
.lines()
.next()
.filter(|line| line.contains(Marker::MARKER))
.map(|line| format!("{line}\n"))
.unwrap_or_default();
let contents = if let Some(first_line) = file_contents.lines().next()
&& first_line.contains(Marker::MARKER)
{
file_contents.lines().skip(1).collect::<Vec<_>>().join("\n")
} else {
file_contents.to_string()
};
let generated_notice = [
format!("@generated by `{}` <{GITHUB}>", Marker::MARKER),
"Do not edit by hand.".to_string(),
String::new(),
format!("downloaded from: {url}"),
]
.into_iter()
.fold(String::new(), |previous_lines, line| {
format!("{previous_lines}{}\n", commented::comment(line, &path))
});
let contents = format!("{marker}{generated_notice}{contents}");
Ok(crate::analysis::WritePath { path, contents })
},
)
.partition_result::<Vec<_>, Vec<_>, _, _>()
.pipe(|(oks, errs)| {
errors.extend(errs);
oks
});
let files = self
.files
.into_iter()
.map(
|File {
old_location,
contents,
output,
input,
}| {
let relative_location = old_location
.strip_prefix(&self.root)?
.strip_prefix(&input)?;
let (file_contents, new_location) = if let Some(first_line) =
contents.lines().next()
&& let Some(marker_start_pos) = first_line.find(Marker::MARKER)
&& let Some(marker_args) =
first_line.get(marker_start_pos + Marker::MARKER.len()..)
&& let Ok(args) = marker_args.parse::<Marker>()
&& let Some(path) = args.path
{
(
contents.lines().skip(1).collect_vec().join(","),
path,
)
} else {
(
contents,
output
.as_ref()
.join(relative_location)
.pipe(OutputPath::new),
)
};
let mut handlebars = Handlebars::new();
handlebars
.register_template_string("t1", file_contents)
.with_context(|| eyre!("failed to parse template for {new_location}"))?;
let contents = handlebars
.render("t1", &BTreeMap::<u8, u8>::new())
.with_context(|| eyre!("failed to render template for {new_location}"))?;
Ok::<_, Error>(crate::analysis::WritePath {
path: new_location.into_inner(),
contents,
})
},
)
.partition_result::<Vec<_>, Vec<_>, _, _>()
.pipe(|(oks, errs)| {
errors.extend(errs);
oks
});
if !errors.is_empty() {
return Err(errors);
}
Ok(Analysis {
writes: links.into_iter().chain(files).collect(),
})
}
pub fn new(cwd: &Path) -> Result<Self, Vec<Error>> {
let root = cwd
.pipe_ref(stdx::traverse_upwards)
.find(|dir| dir.join(Config::FILE_NAME).exists())
.with_context(|| {
eyre!(
"failed to find directory that contains a `{}`. traversed upwards from {}",
Config::FILE_NAME,
cwd.show()
)
})
.map_err(single_err)?;
let config = root
.join(Config::FILE_NAME)
.pipe(fs::read_to_string)
.with_context(|| eyre!("failed to read config file {}", Config::FILE_NAME))
.map_err(single_err)?
.pipe_deref(toml::de::from_str::<Config>)
.context("failed to parse config file")
.map_err(single_err)?
.pipe(|mut conf| {
conf.root = root;
conf
});
let mut errors = vec![];
let links = config
.links
.into_iter()
.map(
|crate::config::Link {
url,
path,
sha256,
marker,
}| {
Ok::<_, Error>(Link {
contents: ureq::get(&url).call()?.body_mut().read_to_string()?,
path,
sha256,
marker,
url,
})
},
)
.partition_result::<Vec<_>, Vec<_>, _, _>()
.pipe(|(oks, errs)| {
errors.extend(errs);
oks
});
let files = config
.dirs
.into_iter()
.flat_map(|crate::config::Dir { input, output }| {
WalkDir::new(config.root.join(&input))
.into_iter()
.flatten()
.filter(|dir_entry| dir_entry.file_type().is_file())
.map(move |file| {
let old_location = path::absolute(file.path())?;
let contents = fs::read_to_string(&old_location).with_context(|| {
eyre!("failed to read path {}", old_location.show())
})?;
Ok::<_, Error>(File {
old_location,
contents,
output: output.clone(),
input: input.clone(),
})
})
})
.partition_result::<Vec<_>, Vec<_>, _, _>()
.pipe(|(oks, errs)| {
errors.extend(errs);
oks
});
if !errors.is_empty() {
return Err(errors);
}
Ok(Self {
root: config.root,
links,
files,
})
}
}
fn single_err(err: impl Into<Error>) -> Vec<Error> {
vec![err.into()]
}