use std::collections::BTreeSet;
use super::{
ConflictEntry, Contributor, ExactInput, ListInput, ListRequirements, MemberInputs,
ResolvedExactList, ResolvedListRequirements, ResolvedRequirement, ResolvedStringMembers,
resolve_all_equal,
};
use crate::types::Provenance;
pub fn resolve_list(
key: &str,
items: Vec<ListInput>,
conflicts: &mut Vec<ConflictEntry>,
) -> ResolvedListRequirements {
let mut grouped = ListGroups::default();
collect_list_groups(items, &mut grouped);
let resolved_contains = resolve_members(grouped.contains);
let resolved_excludes = resolve_members(grouped.excludes);
report_contains_excludes_conflicts(key, &resolved_contains, &resolved_excludes, conflicts);
let exact = resolve_exact_list(key, grouped.exact_items, conflicts);
if let Some(exact) = &exact {
report_exact_conflicts(
key,
exact,
&resolved_contains,
&resolved_excludes,
conflicts,
);
}
ResolvedListRequirements {
contains: resolved_contains,
excludes: resolved_excludes,
exact,
}
}
pub fn resolve_exact_list(
key: &str,
items: Vec<ExactInput>,
conflicts: &mut Vec<ConflictEntry>,
) -> Option<ResolvedExactList> {
let resolved = resolve_all_equal(
key,
"exact-mismatch",
items,
|(list, _)| format!("exact [{}]", list.join(", ")),
conflicts,
)?;
Some(ResolvedRequirement {
merged: resolved.merged.0,
collected: resolved.collected,
})
}
#[must_use]
pub fn render_list_requirement(requirements: &ListRequirements) -> String {
let mut parts = Vec::new();
if !requirements.contains.is_empty() {
parts.push(format!(
"contains [{}]",
requirements
.contains
.keys()
.cloned()
.collect::<Vec<_>>()
.join(", ")
));
}
if !requirements.excludes.is_empty() {
parts.push(format!(
"excludes [{}]",
requirements
.excludes
.keys()
.cloned()
.collect::<Vec<_>>()
.join(", ")
));
}
if let Some((items, _)) = &requirements.exact {
parts.push(format!("exact [{}]", items.join(", ")));
}
if parts.is_empty() {
"list".to_owned()
} else {
format!("list {}", parts.join("; "))
}
}
#[derive(Default)]
struct ListGroups {
contains: MemberInputs,
excludes: MemberInputs,
exact_items: Vec<ExactInput>,
}
fn collect_list_groups(items: Vec<ListInput>, grouped: &mut ListGroups) {
for (prov, list) in items {
for (name, msg) in list.contains {
grouped
.contains
.entry(name)
.or_default()
.push((prov.clone(), msg));
}
for (name, msg) in list.excludes {
grouped
.excludes
.entry(name)
.or_default()
.push((prov.clone(), msg));
}
if let Some(exact) = list.exact {
grouped.exact_items.push((prov, exact));
}
}
}
fn resolve_members(grouped: MemberInputs) -> ResolvedStringMembers {
let mut resolved = ResolvedStringMembers::new();
for (name, collected) in grouped {
let _ = resolved.insert(
name,
ResolvedRequirement {
merged: (),
collected,
},
);
}
resolved
}
fn report_contains_excludes_conflicts(
key: &str,
resolved_contains: &ResolvedStringMembers,
resolved_excludes: &ResolvedStringMembers,
conflicts: &mut Vec<ConflictEntry>,
) {
for name in resolved_contains.keys() {
if let Some(exclude) = resolved_excludes.get(name) {
let mut contributors = Vec::new();
if let Some(include) = resolved_contains.get(name) {
contributors.extend(
include
.collected
.iter()
.map(|(prov, _)| contains_contributor(prov)),
);
}
contributors.extend(
exclude
.collected
.iter()
.map(|(prov, _)| excludes_contributor(prov)),
);
conflicts.push(ConflictEntry {
key: format!("{key}.{name}"),
reason: "list-contains-and-excludes".to_owned(),
contributors,
});
}
}
}
fn report_exact_conflicts(
key: &str,
exact: &ResolvedExactList,
resolved_contains: &ResolvedStringMembers,
resolved_excludes: &ResolvedStringMembers,
conflicts: &mut Vec<ConflictEntry>,
) {
let allowed = exact.merged.iter().cloned().collect::<BTreeSet<_>>();
report_missing_exact_contains(key, exact, resolved_contains, &allowed, conflicts);
report_excluded_exact_members(key, exact, resolved_excludes, &allowed, conflicts);
}
fn report_missing_exact_contains(
key: &str,
exact: &ResolvedExactList,
resolved_contains: &ResolvedStringMembers,
allowed: &BTreeSet<String>,
conflicts: &mut Vec<ConflictEntry>,
) {
for (name, include) in resolved_contains {
if allowed.contains(name) {
continue;
}
let mut contributors = exact_contributors(exact);
contributors.extend(
include
.collected
.iter()
.map(|(prov, _)| contains_contributor(prov)),
);
conflicts.push(ConflictEntry {
key: format!("{key}.{name}"),
reason: "list-exact-missing-contained-item".to_owned(),
contributors,
});
}
}
fn report_excluded_exact_members(
key: &str,
exact: &ResolvedExactList,
resolved_excludes: &ResolvedStringMembers,
allowed: &BTreeSet<String>,
conflicts: &mut Vec<ConflictEntry>,
) {
for (name, exclude) in resolved_excludes {
if !allowed.contains(name) {
continue;
}
let mut contributors = exact_contributors(exact);
contributors.extend(
exclude
.collected
.iter()
.map(|(prov, _)| excludes_contributor(prov)),
);
conflicts.push(ConflictEntry {
key: format!("{key}.{name}"),
reason: "list-exact-contains-excluded-item".to_owned(),
contributors,
});
}
}
fn exact_contributors(exact: &ResolvedExactList) -> Vec<Contributor> {
exact
.collected
.iter()
.map(|(prov, _)| (prov.clone(), "exact".to_owned()))
.collect()
}
fn contains_contributor(prov: &Provenance) -> Contributor {
(prov.clone(), "contains".to_owned())
}
fn excludes_contributor(prov: &Provenance) -> Contributor {
(prov.clone(), "excludes".to_owned())
}