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::TaskRunner;
pub(super) fn is_env_truthy(raw: &str) -> bool {
let v = raw.trim();
!v.is_empty()
&& v != "0"
&& !v.eq_ignore_ascii_case("false")
&& !v.eq_ignore_ascii_case("no")
&& !v.eq_ignore_ascii_case("off")
}
fn parse_fallback_label(raw: &str) -> Result<FallbackPolicy> {
match raw {
"probe" => Ok(FallbackPolicy::Probe),
"npm" => Ok(FallbackPolicy::Npm),
"error" => Ok(FallbackPolicy::Error),
other => Err(anyhow!(
"unknown fallback policy {other:?}; expected one of probe, npm, error",
)),
}
}
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)
}
pub(super) fn parse_mismatch_label(raw: &str) -> Result<MismatchPolicy> {
match raw {
"warn" => Ok(MismatchPolicy::Warn),
"error" => Ok(MismatchPolicy::Error),
"ignore" => Ok(MismatchPolicy::Ignore),
other => Err(anyhow!(
"unknown on-mismatch policy {other:?}; expected one of warn, error, ignore",
)),
}
}
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 = resolve_chain_bool(
keep_going.cli,
keep_env,
config.and_then(|c| c.config.chain.keep_going),
);
let kill = resolve_chain_bool(
kill_on_fail.cli,
kill_env,
config.and_then(|c| c.config.chain.kill_on_fail),
);
match (keep, kill) {
(false, false) => Ok(FailurePolicy::FailFast),
(true, false) => Ok(FailurePolicy::KeepGoing),
(false, true) => Ok(FailurePolicy::KillOnFail),
(true, true) => Err(ResolveError::ConflictingFailurePolicy {
source: "cross-source",
}
.into()),
}
}
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 resolve_chain_bool(cli: bool, env: Option<bool>, config: Option<bool>) -> bool {
if cli {
return true;
}
if let Some(value) = env {
return value;
}
config.unwrap_or(false)
}
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
}