use shep_core::dogs::manifest::Manifest;
use crate::dog_index::{AvailableDog, DogSourceKind};
use crate::exit::ExitCode;
use crate::output::Streams;
const DOG_DEPENDENCY_NOTICE: &str = "dog_dependency_missing";
const DOG_SHEP_TOO_OLD_NOTICE: &str = "dog_needs_newer_shep";
fn is_present(name: &str, adopted: &[String]) -> bool {
crate::dog::BUILT_IN_DOGS.contains(&name) || adopted.iter().any(|dog| dog == name)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Refusal {
NotIndexed {
dependent: String,
dependency: String,
},
NotInstallable {
dependent: String,
dependency: String,
},
BadRange {
dependent: String,
dependency: String,
range: String,
},
Unmet {
dependent: String,
dependency: String,
range: String,
version: String,
},
Cycle {
dependent: String,
dependency: String,
trail: Vec<String>,
},
}
impl Refusal {
pub(super) fn code(&self) -> ExitCode {
match self {
Self::NotIndexed { .. } => ExitCode::NotFound,
_ => ExitCode::Failure,
}
}
}
impl core::fmt::Display for Refusal {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotIndexed {
dependent,
dependency,
} => write!(
f,
"`{dependent}` depends on `{dependency}`, which the dog index does not carry; \
adopt it with `shep adopt <path>` first"
),
Self::NotInstallable {
dependent,
dependency,
} => write!(
f,
"`{dependent}` depends on `{dependency}`, which the dog index lists but approves \
no version shep can install; adopt it with `shep adopt <path>` first"
),
Self::BadRange {
dependent,
dependency,
range,
} => write!(
f,
"`{dependent}` depends on `{dependency}` at {range:?}, which is not a version range"
),
Self::Unmet {
dependent,
dependency,
range,
version,
} => write!(
f,
"`{dependent}` needs `{dependency}` {range}, and the dog index approves \
{version}"
),
Self::Cycle {
dependent,
dependency,
trail,
} => write!(
f,
"`{dependent}` depends on `{dependency}`, which is already being installed \
({}): a dependency cycle",
trail
.iter()
.map(String::as_str)
.chain([dependency.as_str()])
.collect::<Vec<_>>()
.join(" -> ")
),
}
}
}
impl core::error::Error for Refusal {}
pub(super) fn missing<'a>(
manifest: &Manifest,
dependent: &str,
trail: &[String],
adopted: &[String],
index: &'a [AvailableDog],
) -> Result<Vec<&'a AvailableDog>, Refusal> {
let mut wanted: Vec<&AvailableDog> = Vec::new();
for dependency in &manifest.depends.dogs {
let name = dependency.name.as_str();
if is_present(name, adopted) || wanted.iter().any(|dog| dog.adopt_as == name) {
continue;
}
let (dependent, dependency_name) = (dependent.to_owned(), name.to_owned());
if trail.iter().any(|above| above == name) {
return Err(Refusal::Cycle {
dependent,
dependency: dependency_name,
trail: trail.to_vec(),
});
}
let Some(row) = index.iter().find(|dog| dog.adopt_as == name) else {
return Err(Refusal::NotIndexed {
dependent,
dependency: dependency_name,
});
};
let DogSourceKind::Cargo {
version: Some(version),
..
} = &row.source
else {
return Err(Refusal::NotInstallable {
dependent,
dependency: dependency_name,
});
};
let range =
semver::VersionReq::parse(&dependency.version).map_err(|_| Refusal::BadRange {
dependent: dependent.clone(),
dependency: dependency_name.clone(),
range: dependency.version.clone(),
})?;
let met = semver::Version::parse(version).is_ok_and(|version| range.matches(&version));
if !met {
return Err(Refusal::Unmet {
dependent,
dependency: dependency_name,
range: dependency.version.clone(),
version: version.clone(),
});
}
wanted.push(row);
}
Ok(wanted)
}
pub(super) fn warn_of_unmet_depends(
streams: &mut Streams<'_>,
name: &str,
manifest: &Manifest,
adopted: &[String],
) {
for dependency in &manifest.depends.dogs {
if !is_present(&dependency.name, adopted) {
let message = format!(
"`{name}` depends on `{dep}`, which is not adopted, so `{name}` may not work \
until you run `shep adopt` for `{dep}`",
dep = dependency.name
);
streams.aside(DOG_DEPENDENCY_NOTICE, &message);
}
}
let Some(minimum) = &manifest.depends.shep else {
return;
};
let this = env!("CARGO_PKG_VERSION");
let too_old = match (
semver::Version::parse(minimum),
semver::Version::parse(this),
) {
(Ok(minimum), Ok(this)) => this < minimum,
_ => false,
};
if too_old {
let message = format!(
"`{name}` needs shep {minimum} or newer, and this is shep {this}, so `{name}` may \
not work until you upgrade shep"
);
streams.aside(DOG_SHEP_TOO_OLD_NOTICE, &message);
}
}