use super::narrow::{contract_edges, drop_negatives, Dropped};
use super::reject_empty_selection;
use crate::core::{resolver, types};
use std::collections::HashSet;
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct Selectors<'a> {
pub resource: Option<&'a str>,
pub group: Option<&'a str>,
pub subset: Option<&'a str>,
pub resource_filter: Option<&'a str>,
pub goals: &'a [String],
pub exclude: Option<&'a str>,
pub skip: Option<&'a str>,
pub only_machine: Option<&'a str>,
pub exclude_machine: Option<&'a str>,
pub tag: Option<&'a str>,
}
impl<'a> Selectors<'a> {
pub(crate) fn with_scope(mut self, scope: &crate::cli::apply_scope::ApplyScope<'a>) -> Self {
self.skip = scope.skip;
self.only_machine = scope.only_machine;
self.exclude_machine = scope.exclude_machine;
self.resource_filter = scope.resource_filter;
self
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct Selection {
pub total: usize,
pub selected: usize,
pub dependencies_added: usize,
pub removed: Vec<String>,
pub cut_edges: Vec<(String, String)>,
}
pub(crate) fn resolve_selection(
config: &mut types::ForjarConfig,
sel: &Selectors<'_>,
verbose: bool,
) -> Result<Selection, String> {
resolver::build_execution_order(config)?;
check_existence(config, sel)?;
let positive = positive_ids(config, sel)?;
let closure = resolver::goal_closure(config, &positive)?;
let mut out = Selection {
total: config.resources.len(),
selected: positive.len(),
dependencies_added: closure.len().saturating_sub(positive.len()),
removed: Vec::new(),
cut_edges: Vec::new(),
};
let dropped = drop_negatives(config, sel, &closure, verbose)?;
let removed: HashSet<String> = dropped.iter().map(|(id, _)| id.clone()).collect();
let keep: Vec<String> = config
.resources
.keys()
.filter(|id| closure.contains(*id) && !removed.contains(*id))
.cloned()
.collect();
if keep.is_empty() && dropped.iter().any(|(_, c)| is_resource_negative(c)) {
return Err(empty_after_narrowing(&dropped, closure.len()));
}
out.cut_edges = contract_edges(config, &keep, &removed);
out.removed = dropped.iter().map(|(id, _)| id.clone()).collect();
prune(config, &keep);
if !config.resources.is_empty() {
resolver::build_execution_order(config)?;
}
if verbose {
report(config, sel, &out, &dropped);
}
Ok(out)
}
fn is_resource_negative(cause: &str) -> bool {
cause.starts_with("--exclude '") || cause.starts_with("--skip '")
}
fn empty_after_narrowing(dropped: &[(String, String)], selected: usize) -> String {
let mut causes: Vec<&str> = Vec::new();
for (_, cause) in dropped {
if !causes.contains(&cause.as_str()) {
causes.push(cause);
}
}
format!(
"no resources remain: {} removed every selected resource ({selected} of {selected})",
causes.join(", ")
)
}
fn known(keys: impl Iterator<Item = String>) -> String {
let mut v: Vec<String> = keys.collect();
v.sort_unstable();
v.join(", ")
}
fn check_existence(config: &types::ForjarConfig, sel: &Selectors<'_>) -> Result<(), String> {
reject_empty_selection(config, sel.resource, sel.tag, sel.group)?;
check_glob_selectors(config, sel)?;
check_skip(config, sel.skip)?;
check_machine(config, sel.only_machine, "--only-machine")?;
check_machine(config, sel.exclude_machine, "--exclude-machine")?;
resolver::goal_closure(config, sel.goals)?;
Ok(())
}
fn glob_matches_any(config: &types::ForjarConfig, pattern: &str) -> bool {
config
.resources
.keys()
.any(|id| crate::cli::helpers_state::simple_glob_match(pattern, id))
}
fn check_glob_selectors(config: &types::ForjarConfig, sel: &Selectors<'_>) -> Result<(), String> {
if let Some(p) = sel.subset {
if !glob_matches_any(config, p) {
return Err(format!("no resources match subset pattern '{p}'"));
}
}
if let Some(p) = sel.resource_filter {
if !glob_matches_any(config, p) {
return Err(format!(
"--resource-filter: no resources match subset pattern '{p}'"
));
}
}
Ok(())
}
fn check_skip(config: &types::ForjarConfig, skip: Option<&str>) -> Result<(), String> {
let Some(id) = skip else { return Ok(()) };
if config.resources.contains_key(id) {
return Ok(());
}
Err(format!(
"--skip '{id}' matches no resource in this config. Known: {}",
known(config.resources.keys().cloned())
))
}
fn check_machine(
config: &types::ForjarConfig,
machine: Option<&str>,
flag: &str,
) -> Result<(), String> {
let Some(m) = machine else { return Ok(()) };
if config.machines.contains_key(m) {
return Ok(());
}
Err(format!(
"{flag} '{m}' matches no machine in this config. Known: {}",
known(config.machines.keys().cloned())
))
}
impl Selectors<'_> {
fn has_positive(&self) -> bool {
self.resource.is_some()
|| self.group.is_some()
|| self.subset.is_some()
|| self.resource_filter.is_some()
|| !self.goals.is_empty()
}
fn describe_positive(&self) -> String {
let mut parts: Vec<String> = Vec::new();
push_flag(&mut parts, "--resource", self.resource);
push_flag(&mut parts, "--group", self.group);
push_flag(&mut parts, "--subset", self.subset);
push_flag(&mut parts, "--resource-filter", self.resource_filter);
if !self.goals.is_empty() {
parts.push(format!("goals {:?}", self.goals));
}
parts.join(", ")
}
}
fn push_flag(parts: &mut Vec<String>, flag: &str, value: Option<&str>) {
if let Some(v) = value {
parts.push(format!("{flag} '{v}'"));
}
}
fn positive_ids(config: &types::ForjarConfig, sel: &Selectors<'_>) -> Result<Vec<String>, String> {
if !sel.has_positive() {
return Ok(config.resources.keys().cloned().collect());
}
let ids: Vec<String> = config
.resources
.iter()
.filter(|(id, r)| matches_ids(id, r, sel) && matches_globs(id, sel))
.map(|(id, _)| id.clone())
.collect();
if ids.is_empty() {
return Err(format!(
"no resources match the selectors: {}",
sel.describe_positive()
));
}
Ok(ids)
}
fn matches_ids(id: &str, r: &types::Resource, sel: &Selectors<'_>) -> bool {
sel.resource.is_none_or(|want| want == id)
&& sel
.group
.is_none_or(|g| r.resource_group.as_deref() == Some(g))
&& (sel.goals.is_empty() || sel.goals.iter().any(|g| g == id))
}
fn matches_globs(id: &str, sel: &Selectors<'_>) -> bool {
let matches = |p: &str| crate::cli::helpers_state::simple_glob_match(p, id);
sel.subset.is_none_or(matches) && sel.resource_filter.is_none_or(matches)
}
fn prune(config: &mut types::ForjarConfig, keep: &[String]) {
let set: HashSet<&str> = keep.iter().map(String::as_str).collect();
config.resources.retain(|id, _| set.contains(id.as_str()));
}
fn added_suffix(out: &Selection) -> String {
if out.dependencies_added == 0 {
return String::new();
}
format!(", +{} dependencies", out.dependencies_added)
}
fn report(config: &types::ForjarConfig, sel: &Selectors<'_>, out: &Selection, dropped: &Dropped) {
report_positive(config, sel, out);
report_negative(config, sel, out, dropped);
report_cut_edges(out, dropped);
}
fn report_positive(config: &types::ForjarConfig, sel: &Selectors<'_>, out: &Selection) {
let suffix = added_suffix(out);
if let Some(p) = sel.subset {
eprintln!(
"Subset filter '{p}': {} resources selected{suffix}",
out.selected
);
}
if out.dependencies_added > 0 {
if let Some(id) = sel.resource {
eprintln!("Resource '{id}': {} selected{suffix}", out.selected);
}
if let Some(g) = sel.group {
eprintln!("Group '{g}': {} selected{suffix}", out.selected);
}
}
if !sel.goals.is_empty() {
eprintln!(
"Goals {:?}: {} of {} resources in the prerequisite closure",
sel.goals,
config.resources.len(),
out.total
);
}
}
fn report_negative(
config: &types::ForjarConfig,
sel: &Selectors<'_>,
out: &Selection,
dropped: &Dropped,
) {
if let Some(p) = sel.exclude {
let cause = format!("--exclude '{p}'");
let n = dropped.iter().filter(|(_, c)| *c == cause).count();
eprintln!(
"Exclude filter '{}': removed {} resources ({} remaining)",
p,
n,
config.resources.len()
);
}
if scope_selector_used(sel) {
eprintln!(
"Scope selectors: {} resources selected",
out.total - out.removed.len()
);
}
}
fn scope_selector_used(sel: &Selectors<'_>) -> bool {
sel.skip.is_some()
|| sel.only_machine.is_some()
|| sel.exclude_machine.is_some()
|| sel.resource_filter.is_some()
}
fn report_cut_edges(out: &Selection, dropped: &Dropped) {
for (dependent, dependency) in &out.cut_edges {
let cause = dropped
.iter()
.find(|(id, _)| id == dependency)
.map_or("selection", |(_, c)| c.as_str());
eprintln!(
"{cause}: '{dependent}' depends on it; running '{dependent}' with '{dependency}' assumed satisfied"
);
}
}