use std::collections::{BTreeSet, HashMap};
use std::path::PathBuf;
use super::error::ConfigError;
use super::paths;
use super::reqfile::{Block, Check, GitRef, Kind, Location, Origin, Reqfile};
pub fn assets_dir(reqfile_dir: &str, id: &str) -> String {
paths::join(reqfile_dir, &format!(".reqfile/{id}"))
}
pub struct Source {
pub repo: String,
pub reference: GitRef,
pub commit: String,
pub offline: bool,
pub root: PathBuf,
pub reqfiles: Vec<Reqfile>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Assets {
Repo(String),
External(PathBuf),
}
#[derive(Debug, Clone)]
pub struct Effective {
pub reqfile: String,
pub dir: String,
pub line: usize,
pub id: String,
pub kind: Kind,
pub must: String,
pub why: String,
pub who: Option<String>,
pub reference: Option<String>,
pub checks: Vec<Check>,
pub checks_source: String,
pub imported: Option<Imported>,
pub assets: Assets,
}
#[derive(Debug, Clone)]
pub struct Imported {
pub location: Location,
pub definition: String,
pub commit: Option<String>,
pub checks_inherited: bool,
pub definition_assets: Assets,
}
impl Imported {
pub fn describe(&self) -> String {
match &self.commit {
Some(commit) => format!(
"{} ({} at commit {commit})",
self.location.describe(),
self.definition
),
None => format!("{} ({})", self.location.describe(), self.definition),
}
}
}
impl Effective {
pub fn block(&self) -> String {
format!("{}:{}", self.reqfile, self.line)
}
}
pub fn check_unique_definitions(reqfiles: &[Reqfile]) -> Vec<ConfigError> {
let mut seen: HashMap<&str, &Reqfile> = HashMap::new();
let mut errors = Vec::new();
for reqfile in reqfiles {
for block in reqfile
.blocks
.iter()
.filter(|b| matches!(b.origin, Origin::Definition { .. }))
{
match seen.get(block.id.as_str()) {
Some(first) => errors.push(ConfigError::at(
&reqfile.path,
block.line,
format!(
"requirement {} is also defined in {}; define it once and take it elsewhere with `use`",
block.id, first.path
),
)),
None => {
seen.insert(&block.id, reqfile);
}
}
}
}
errors
}
pub fn resolve(
reqfiles: &[Reqfile],
sources: &[Source],
asset_files: &BTreeSet<String>,
) -> Result<Vec<Effective>, Vec<ConfigError>> {
let mut effective = Vec::new();
let mut errors = Vec::new();
for reqfile in reqfiles {
for block in &reqfile.blocks {
match resolve_block(reqfiles, reqfile, block, sources, asset_files) {
Ok(e) => effective.push(e),
Err(e) => errors.push(e),
}
}
}
if errors.is_empty() {
Ok(effective)
} else {
Err(errors)
}
}
fn resolve_block(
reqfiles: &[Reqfile],
reqfile: &Reqfile,
block: &Block,
sources: &[Source],
asset_files: &BTreeSet<String>,
) -> Result<Effective, ConfigError> {
let error = |message: String| ConfigError::at(&reqfile.path, block.line, message);
let own_assets = assets_dir(&reqfile.dir, &block.id);
let location = match &block.origin {
Origin::Definition { must, reference } => {
let checks = block.checks.as_ref().expect("a definition has checks");
return Ok(Effective {
reqfile: reqfile.path.clone(),
dir: reqfile.dir.clone(),
line: block.line,
id: block.id.clone(),
kind: block.kind,
must: must.clone(),
why: block.why.clone().expect("a definition has a why"),
who: block.who.clone(),
reference: reference.clone(),
checks: checks.list.clone(),
checks_source: checks.source.clone(),
imported: None,
assets: Assets::Repo(own_assets),
});
}
Origin::Use(location) => location,
};
let (found, commit, definition_assets) = match location {
Location::Local(path) => {
let folder = paths::normalize(&paths::join(&reqfile.dir, path)).ok_or_else(|| {
error(format!(
"`use: {path}` points above the repository root; a folder source must be inside the repository"
))
})?;
let shown = paths::display_dir(&folder).to_string();
let (source_reqfile, definition) =
find_definition(reqfiles, &folder, &shown, &block.id)
.map_err(|e| error(format!("`use: {path}`: {e}")))?;
let assets = Assets::Repo(assets_dir(&source_reqfile.dir, &block.id));
((source_reqfile, definition), None, assets)
}
Location::Git { repo, reference } => {
let source = sources
.iter()
.find(|s| &s.repo == repo && &s.reference == reference)
.expect("every git source was loaded");
let describe = location.describe();
let shown = format!("the root of {repo} at commit {}", source.commit);
let (source_reqfile, definition) =
find_definition(&source.reqfiles, "", &shown, &block.id)
.map_err(|e| error(format!("`use: {describe}`: {e}")))?;
let assets =
Assets::External(source.root.join(assets_dir(&source_reqfile.dir, &block.id)));
(
(source_reqfile, definition),
Some(source.commit.clone()),
assets,
)
}
};
let (source_reqfile, definition) = found;
let Origin::Definition { must, reference } = &definition.origin else {
unreachable!("only definitions are found");
};
if definition.kind != block.kind {
return Err(error(format!(
"requirement {} is listed under `{}`, but its definition in {}:{} is a {} requirement; list it under `{}`",
block.id,
block.kind.as_str(),
source_reqfile.path,
definition.line,
definition.kind.as_str(),
definition.kind.as_str()
)));
}
let definition_checks = definition.checks.as_ref().expect("a definition has checks");
let (checks, assets) = match &block.checks {
Some(local) => (local, Assets::Repo(own_assets)),
None => {
let prefix = format!("{own_assets}/");
let examples = format!("{own_assets}/examples/");
if let Some(file) = asset_files
.iter()
.find(|f| f.starts_with(&prefix) && !f.starts_with(&examples))
{
return Err(error(format!(
"requirement {} takes its checks from its definition, whose files are in its own .reqfile/{}/, so {own_assets}/ may only hold examples/; found {file}. Set `checks` here to use local files, or move the file",
block.id, block.id
)));
}
(definition_checks, definition_assets.clone())
}
};
Ok(Effective {
reqfile: reqfile.path.clone(),
dir: reqfile.dir.clone(),
line: block.line,
id: block.id.clone(),
kind: block.kind,
must: must.clone(),
why: block
.why
.clone()
.or_else(|| definition.why.clone())
.expect("a definition has a why"),
who: block.who.clone().or_else(|| definition.who.clone()),
reference: reference.clone(),
checks: checks.list.clone(),
checks_source: checks.source.clone(),
imported: Some(Imported {
location: location.clone(),
definition: format!("{}:{}", source_reqfile.path, definition.line),
commit,
checks_inherited: block.checks.is_none(),
definition_assets,
}),
assets,
})
}
fn find_definition<'r>(
reqfiles: &'r [Reqfile],
folder: &str,
shown: &str,
id: &str,
) -> Result<(&'r Reqfile, &'r Block), String> {
let found: Vec<(&Reqfile, &Block)> = reqfiles
.iter()
.filter(|r| paths::contains_dir(folder, &r.dir))
.flat_map(|r| r.blocks.iter().map(move |b| (r, b)))
.filter(|(_, b)| b.id == id)
.collect();
let definitions: Vec<&(&Reqfile, &Block)> = found
.iter()
.filter(|(_, b)| matches!(b.origin, Origin::Definition { .. }))
.collect();
match definitions.as_slice() {
[one] => Ok(**one),
[] => match found.first() {
Some((reqfile, block)) => Err(format!(
"{id} in {}:{} is a use block, not a definition; point `use` to the folder that defines {id}",
reqfile.path, block.line
)),
None => Err(format!("no Reqfile at or under {shown} defines {id}")),
},
[first, second, ..] => Err(format!(
"{id} is defined twice, in {} and {}",
first.0.path, second.0.path
)),
}
}
pub fn applicable<'e>(blocks: &'e [Effective], dir: &str) -> Vec<&'e Effective> {
let mut nearest: Vec<&Effective> = Vec::new();
for block in blocks.iter().filter(|b| paths::contains_dir(&b.dir, dir)) {
match nearest.iter_mut().find(|b| b.id == block.id) {
Some(existing) if depth(&block.dir) > depth(&existing.dir) => *existing = block,
Some(_) => {}
None => nearest.push(block),
}
}
nearest.sort_by_key(|b| depth(&b.dir));
nearest
}
fn depth(dir: &str) -> usize {
super::config::depth(dir)
}
pub struct Scope {
dir: String,
shadowed: Vec<String>,
}
impl Scope {
pub fn of(blocks: &[Effective], index: usize) -> Self {
let block = &blocks[index];
let shadowed = blocks
.iter()
.filter(|b| b.id == block.id && b.dir != block.dir)
.filter(|b| paths::contains_dir(&block.dir, &b.dir))
.map(|b| b.dir.clone())
.collect();
Self {
dir: block.dir.clone(),
shadowed,
}
}
pub fn relative<'p>(&self, path: &'p str) -> Option<&'p str> {
let relative = paths::relative_to(&self.dir, path)?;
let dir = paths::parent(path);
(!self
.shadowed
.iter()
.any(|shadow| paths::contains_dir(shadow, dir)))
.then_some(relative)
}
}
pub fn nearest(blocks: &[Effective], dir: &str, id: &str) -> Option<usize> {
blocks
.iter()
.enumerate()
.filter(|(_, b)| b.id == id && paths::contains_dir(&b.dir, dir))
.max_by_key(|(_, b)| depth(&b.dir))
.map(|(i, _)| i)
}
pub fn fingerprint(block: &Effective, assets: &str, settings: &str) -> String {
let imported = block.imported.as_ref().map(|i| {
format!(
"{}\n{}\n{:?}\n{}",
i.location.describe(),
i.definition,
i.commit,
i.checks_inherited
)
});
super::runlog::fingerprint(&format!(
"{}\n{}\n{}\n{}\n{}\n{:?}\n{:?}\n{}\n{:?}\n{assets}\n{settings}",
block.reqfile,
block.id,
block.kind.as_str(),
block.must,
block.why,
block.who,
block.reference,
block.checks_source,
imported,
))
}