use std::fmt::Display;
use super::source::PackageSource;
use super::source::ManifestSourceOptExt;
use super::source::MetadataSource;
#[derive(Debug, Clone)]
pub enum Problem {
UnknownTarget { name: String },
MissingField { field: String },
Warning(Warning),
}
#[derive(Debug, Clone)]
pub enum Warning {
MissingMetadata,
StrangeValue {
field: String,
value: String,
reason: Option<&'static str>,
},
UnknownField {
field: String,
reason: Option<&'static str>,
},
MissingField {
field: String,
reason: Option<&'static str>,
},
}
impl Display for Warning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingMetadata => write!(f, "Metadata not found"),
Self::StrangeValue { field, value, reason } => {
write!(f, "Strange value {value:?} for field '{field}'")?;
if let Some(reason) = reason {
write!(f, ", {reason}")
} else {
Ok(())
}
},
Self::UnknownField { field, reason } => {
write!(f, "Unknown field '{field}'")?;
if let Some(reason) = reason {
write!(f, ", {reason}")
} else {
Ok(())
}
},
Self::MissingField { field, reason } => {
write!(f, "Missing field '{field}'")?;
if let Some(reason) = reason {
write!(f, ", {reason}")
} else {
Ok(())
}
},
}
}
}
impl Problem {
pub fn is_err(&self) -> bool {
match self {
Problem::Warning(_) => false,
_ => true,
}
}
pub fn is_warn(&self) -> bool { !self.is_err() }
}
impl Display for Problem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownTarget { name } => write!(f, "Unknown cargo-target {name:?}"),
Self::MissingField { field } => write!(f, "Missing field: {field:?}"),
Self::Warning(warning) => warning.fmt(f),
}
}
}
pub trait Validate {
fn validate(&self) -> impl IntoIterator<Item = Problem>;
}
impl<T: ManifestSourceOptExt> Validate for T {
fn validate(&self) -> impl IntoIterator<Item = Problem> {
let is_not_empty = |s: &&str| !s.trim().is_empty();
fn check_some<T>(name: &'static str, v: Option<T>) -> Option<Problem> {
v.is_none().then(|| Problem::MissingField { field: name.into() })
}
fn warn_none<T>(name: &'static str, v: Option<T>, warn_msg: Option<&'static str>) -> Option<Problem> {
v.is_none().then(|| {
Problem::Warning(Warning::MissingField { field: name.into(),
reason: warn_msg })
})
}
let missed = [
(
"build-number",
self.build_number().is_some(),
Some("required for sideloaded games."),
),
("description", self.description().is_some(), None),
].into_iter()
.filter_map(|(k, v, msg)| warn_none(k, v.then_some(()), msg));
let unknown = self.iter_extra().into_iter().flatten().map(|(k, _)| {
Problem::Warning(Warning::UnknownField { field: k.as_ref()
.to_owned(),
reason: None })
});
let errors = [
("name", self.name().filter(is_not_empty)),
("version", self.version().filter(is_not_empty)),
("bundle-id", self.bundle_id().filter(is_not_empty)),
].into_iter()
.filter_map(|(k, v)| check_some(k, v));
errors.chain(missed)
.chain(self.version().into_iter().filter_map(validate_version))
.chain(unknown)
}
}
fn validate_version(value: &str) -> Option<Problem> {
let re = regex::Regex::new(r"^\d+(?:\.\d+){0,2}$").unwrap();
if !re.is_match(value.trim()) {
if semver::Version::parse(value).is_err() {
Some(Problem::Warning(Warning::StrangeValue { field: "version".into(),
value: value.into(),
reason: Some("can be confusing.") }))
} else {
None
}
} else {
None
}
}
pub trait ValidateCrate: PackageSource {
fn validate<'t>(&'t self) -> impl IntoIterator<Item = Problem> + 't {
let missed =
self.metadata().into_iter().flat_map(|meta| {
let bins = meta.bin_targets()
.into_iter()
.filter(|name| !self.bins().contains(name))
.map(|name| Problem::UnknownTarget { name: name.to_owned() });
let examples = meta.example_targets()
.into_iter()
.filter(|name| !self.examples().contains(name))
.map(|name| Problem::UnknownTarget { name: name.to_owned() });
bins.chain(examples).collect::<Vec<_>>()
});
let crate_name_eq =
self.metadata().into_iter().flat_map(|meta| {
let mut targets = meta.all_targets().into_iter();
if let Some(name) = targets.find(|name| name == &self.name()) {
let msg = "target name is the same as the crate name";
Some(Problem::Warning(Warning::StrangeValue { field:
"target".into(),
value: name.into(),
reason: Some(msg) }))
} else {
None
}
});
let no_meta = self.metadata()
.is_none()
.then(|| Problem::Warning(Warning::MissingMetadata));
missed.into_iter().chain(crate_name_eq).chain(no_meta)
}
fn validate_for(&self, target: &str) -> impl IntoIterator<Item = Problem> {
let no_meta = self.metadata()
.is_none()
.then(|| Problem::Warning(Warning::MissingMetadata));
let man = self.bins()
.contains(&target)
.then_some(false)
.or_else(|| self.examples().contains(&target).then_some(true))
.map(|dev| self.manifest_override_or_crate(target.into(), dev))
.map_or_else(
|| vec![Problem::UnknownTarget { name: target.to_owned(), }],
|m| m.validate().into_iter().collect::<Vec<_>>(),
);
no_meta.into_iter().chain(man)
}
}
impl<T> ValidateCrate for T where T: PackageSource {}
#[cfg(test)]
mod tests {
use super::Validate;
use super::super::format::Manifest;
#[test]
fn validate_version() {
let cases = [
("0", true),
("0.0", true),
("0.0.0", true),
("0.0.0-pre", true),
("", false),
("0.0.a", false),
("beta", false),
];
for (i, (ver, ok)) in cases.iter().enumerate() {
let result = super::validate_version(ver);
assert_eq!(*ok, result.is_none(), "{i}: {result:?}");
}
}
#[test]
fn manifest_empty() {
let m = Manifest::<&str>::default();
let errors = m.validate().into_iter().collect::<Vec<_>>();
assert_eq!(5, errors.len(), "{:#?}", errors);
assert_eq!(3, errors.iter().filter(|e| e.is_err()).count());
}
#[test]
fn manifest_valid() {
let m = Manifest::<&str> { name: "name".into(),
version: "0.0".into(),
author: "author".into(),
bundle_id: "bundle.id".into(),
description: "description".into(),
image_path: "image_path".into(),
launch_sound_path: "launch_sound_path".into(),
content_warning: "content_warning".into(),
content_warning2: "content_warning2".into(),
build_number: 42.into() };
let errors = m.validate().into_iter().collect::<Vec<_>>();
assert!(errors.is_empty(), "{:#?}", errors);
}
}