use std::collections::BTreeMap;
use anyhow::{Result, anyhow};
use super::types::{ExplainSource, FallbackPolicy, MismatchPolicy};
use super::{ResolveError, join_labels};
use crate::chain::FailurePolicy;
use crate::config::LoadedConfig;
use crate::types::{PackageManager, TaskRunner, TaskSource};
pub(super) fn is_env_truthy(raw: &str) -> bool {
let v = raw.trim();
!v.is_empty()
&& !ENV_BOOL_FALSY
.iter()
.any(|token| v.eq_ignore_ascii_case(token))
}
pub(super) const ENV_BOOL_FALSY: &[&str] = &["0", "false", "no", "off"];
pub(super) const ENV_BOOL_TRUTHY: &[&str] = &["1", "true", "yes", "on"];
pub(super) fn parse_fallback_label(raw: &str) -> Result<FallbackPolicy> {
FallbackPolicy::ALL
.into_iter()
.find(|policy| policy.label() == raw)
.ok_or_else(|| {
anyhow!(
"unknown fallback policy {raw:?}; expected one of {}",
join_labels(FallbackPolicy::ALL.iter().map(|p| p.label())),
)
})
}
pub(super) fn resolve_fallback_policy(
cli: Option<&str>,
env: Option<&str>,
config: Option<&LoadedConfig>,
) -> Result<FallbackPolicy> {
if let Some(raw) = cli.map(str::trim).filter(|s| !s.is_empty()) {
return parse_fallback_label(raw);
}
if let Some(raw) = env.map(str::trim).filter(|s| !s.is_empty()) {
return parse_fallback_label(raw);
}
if let Some(loaded) = config
&& let Some(raw) = loaded
.config
.resolution
.fallback
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
return parse_fallback_label(raw);
}
Ok(FallbackPolicy::default())
}
pub(super) fn parse_prefer_runners(config: Option<&LoadedConfig>) -> Result<Vec<TaskRunner>> {
let Some(loaded) = config else {
return Ok(Vec::new());
};
let raw = &loaded.config.task_runner.prefer;
if raw.is_empty() {
return Ok(Vec::new());
}
let mut out = Vec::with_capacity(raw.len());
for entry in raw {
let trimmed = entry.trim();
if trimmed.is_empty() {
continue;
}
let runner = TaskRunner::from_label(trimmed).ok_or_else(|| {
anyhow!(
"[task_runner].prefer: unknown runner {trimmed:?}; expected one of {}",
join_labels(TaskRunner::all().iter().map(|r| r.label())),
)
})?;
out.push(runner);
}
Ok(out)
}
fn resolve_source_label(raw: &str) -> Result<Vec<TaskSource>> {
let label = raw.trim();
if let Some(pm) = PackageManager::from_label(label) {
return Ok(pm.owned_task_sources().to_vec());
}
if let Some(runner) = TaskRunner::from_label(label) {
return Ok(runner.task_source().into_iter().collect());
}
if let Some(source) = TaskSource::from_label(label) {
return Ok(vec![source]);
}
Err(anyhow!(
"unknown source {label:?}; expected a task runner ({}), a package manager ({}), or a \
source name like package.json",
join_labels(TaskRunner::all().iter().map(|r| r.label())),
join_labels(
PackageManager::all()
.iter()
.copied()
.map(PackageManager::label)
),
))
}
pub(super) fn parse_tasks_prefer(config: Option<&LoadedConfig>) -> Result<Vec<TaskSource>> {
let Some(loaded) = config else {
return Ok(Vec::new());
};
let mut out: Vec<TaskSource> = Vec::new();
for entry in &loaded.config.tasks.prefer {
let trimmed = entry.trim();
if trimmed.is_empty() {
continue;
}
let sources = resolve_source_label(trimmed).map_err(|e| anyhow!("[tasks].prefer: {e}"))?;
for source in sources {
if !out.contains(&source) {
out.push(source);
}
}
}
Ok(out)
}
pub(super) fn parse_tasks_overrides(
config: Option<&LoadedConfig>,
) -> Result<BTreeMap<String, Vec<TaskSource>>> {
let Some(loaded) = config else {
return Ok(BTreeMap::new());
};
let mut out = BTreeMap::new();
for (task, label) in &loaded.config.tasks.overrides {
let sources =
resolve_source_label(label).map_err(|e| anyhow!("[tasks.overrides] {task:?}: {e}"))?;
if sources.is_empty() {
return Err(anyhow!(
"[tasks.overrides] {task:?}: {label:?} names no task source to pin to",
));
}
out.insert(task.clone(), sources);
}
Ok(out)
}
pub(super) fn parse_mismatch_label(raw: &str) -> Result<MismatchPolicy> {
MismatchPolicy::ALL
.into_iter()
.find(|policy| policy.label() == raw)
.ok_or_else(|| {
anyhow!(
"unknown on-mismatch policy {raw:?}; expected one of {}",
join_labels(MismatchPolicy::ALL.iter().map(|p| p.label())),
)
})
}
pub(super) fn resolve_mismatch_policy(
cli: Option<&str>,
env: Option<&str>,
config: Option<&LoadedConfig>,
) -> Result<MismatchPolicy> {
if let Some(raw) = cli.map(str::trim).filter(|s| !s.is_empty()) {
return parse_mismatch_label(raw);
}
if let Some(raw) = env.map(str::trim).filter(|s| !s.is_empty()) {
return parse_mismatch_label(raw);
}
if let Some(loaded) = config
&& let Some(raw) = loaded
.config
.resolution
.on_mismatch
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
return parse_mismatch_label(raw);
}
Ok(MismatchPolicy::default())
}
pub(super) fn resolve_failure_policy(
keep_going: ExplainSource<'_>,
kill_on_fail: ExplainSource<'_>,
config: Option<&LoadedConfig>,
) -> Result<FailurePolicy> {
let keep_env = parse_env_bool(keep_going.env);
let kill_env = parse_env_bool(kill_on_fail.env);
if let Some(source) =
single_source_conflict(&keep_going, &kill_on_fail, keep_env, kill_env, config)
{
return Err(ResolveError::ConflictingFailurePolicy { source }.into());
}
let keep = chain_bool_layer(
keep_going.cli,
keep_env,
config.and_then(|c| c.config.chain.keep_going),
);
let kill = chain_bool_layer(
kill_on_fail.cli,
kill_env,
config.and_then(|c| c.config.chain.kill_on_fail),
);
match (keep, kill) {
(None, None) => Ok(FailurePolicy::FailFast),
(Some(_), None) => Ok(FailurePolicy::KeepGoing),
(None, Some(_)) => Ok(FailurePolicy::KillOnFail),
(Some(keep_layer), Some(kill_layer)) => match keep_layer.cmp(&kill_layer) {
std::cmp::Ordering::Greater => Ok(FailurePolicy::KeepGoing),
std::cmp::Ordering::Less => Ok(FailurePolicy::KillOnFail),
std::cmp::Ordering::Equal => Err(ResolveError::ConflictingFailurePolicy {
source: "cross-source",
}
.into()),
},
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum ChainBoolLayer {
Config,
Env,
Cli,
}
fn chain_bool_layer(cli: bool, env: Option<bool>, config: Option<bool>) -> Option<ChainBoolLayer> {
if cli {
return Some(ChainBoolLayer::Cli);
}
match env {
Some(true) => Some(ChainBoolLayer::Env),
Some(false) => None,
None => (config == Some(true)).then_some(ChainBoolLayer::Config),
}
}
fn parse_env_bool(env: Option<&str>) -> Option<bool> {
let raw = env.map(str::trim).filter(|s| !s.is_empty())?;
Some(is_env_truthy(raw))
}
fn single_source_conflict(
keep: &ExplainSource<'_>,
kill: &ExplainSource<'_>,
keep_env: Option<bool>,
kill_env: Option<bool>,
config: Option<&LoadedConfig>,
) -> Option<&'static str> {
if keep.cli && kill.cli {
return Some("CLI flags");
}
if keep_env == Some(true) && kill_env == Some(true) {
return Some("env vars");
}
if let Some(loaded) = config
&& loaded.config.chain.keep_going == Some(true)
&& loaded.config.chain.kill_on_fail == Some(true)
&& keep_env != Some(false)
&& kill_env != Some(false)
{
return Some("[chain] config");
}
None
}