1use std::fmt::Display;
2
3use super::source::PackageSource;
4use super::source::ManifestSourceOptExt;
5use super::source::MetadataSource;
6
7
8#[derive(Debug, Clone)]
9pub enum Problem {
10 UnknownTarget { name: String },
11 MissingField { field: String },
12 Warning(Warning),
13}
14
15#[derive(Debug, Clone)]
16pub enum Warning {
17 MissingMetadata,
18 StrangeValue {
19 field: String,
20 value: String,
21 reason: Option<&'static str>,
22 },
23 UnknownField {
24 field: String,
25 reason: Option<&'static str>,
26 },
27 MissingField {
28 field: String,
29 reason: Option<&'static str>,
30 },
31}
32
33impl Display for Warning {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 Self::MissingMetadata => write!(f, "Metadata not found"),
37 Self::StrangeValue { field, value, reason } => {
38 write!(f, "Strange value {value:?} for field '{field}'")?;
39 if let Some(reason) = reason {
40 write!(f, ", {reason}")
41 } else {
42 Ok(())
43 }
44 },
45 Self::UnknownField { field, reason } => {
46 write!(f, "Unknown field '{field}'")?;
47 if let Some(reason) = reason {
48 write!(f, ", {reason}")
49 } else {
50 Ok(())
51 }
52 },
53 Self::MissingField { field, reason } => {
54 write!(f, "Missing field '{field}'")?;
55 if let Some(reason) = reason {
56 write!(f, ", {reason}")
57 } else {
58 Ok(())
59 }
60 },
61 }
62 }
63}
64
65
66impl Problem {
67 pub fn is_err(&self) -> bool {
68 match self {
69 Problem::Warning(_) => false,
70 _ => true,
71 }
72 }
73
74 pub fn is_warn(&self) -> bool { !self.is_err() }
75}
76
77impl Display for Problem {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 Self::UnknownTarget { name } => write!(f, "Unknown cargo-target {name:?}"),
81 Self::MissingField { field } => write!(f, "Missing field: {field:?}"),
82 Self::Warning(warning) => warning.fmt(f),
83 }
84 }
85}
86
87
88pub trait Validate {
90 fn validate(&self) -> impl IntoIterator<Item = Problem>;
94}
95
96impl<T: ManifestSourceOptExt> Validate for T {
97 fn validate(&self) -> impl IntoIterator<Item = Problem> {
98 let is_not_empty = |s: &&str| !s.trim().is_empty();
99
100 fn check_some<T>(name: &'static str, v: Option<T>) -> Option<Problem> {
101 v.is_none().then(|| Problem::MissingField { field: name.into() })
102 }
103
104 fn warn_none<T>(name: &'static str, v: Option<T>, warn_msg: Option<&'static str>) -> Option<Problem> {
105 v.is_none().then(|| {
106 Problem::Warning(Warning::MissingField { field: name.into(),
107 reason: warn_msg })
108 })
109 }
110
111
112 let missed = [
113 (
114 "build-number",
115 self.build_number().is_some(),
116 Some("required for sideloaded games."),
117 ),
118 ("description", self.description().is_some(), None),
119 ].into_iter()
120 .filter_map(|(k, v, msg)| warn_none(k, v.then_some(()), msg));
121
122
123 let unknown = self.iter_extra().into_iter().flatten().map(|(k, _)| {
124 Problem::Warning(Warning::UnknownField { field: k.as_ref()
125 .to_owned(),
126 reason: None })
127 });
128
129
130 let errors = [
132 ("name", self.name().filter(is_not_empty)),
133 ("version", self.version().filter(is_not_empty)),
134 ("bundle-id", self.bundle_id().filter(is_not_empty)),
135 ].into_iter()
136 .filter_map(|(k, v)| check_some(k, v));
137
138
139 errors.chain(missed)
140 .chain(self.version().into_iter().filter_map(validate_version))
141 .chain(unknown)
142 }
143}
144
145
146fn validate_version(value: &str) -> Option<Problem> {
147 let re = regex::Regex::new(r"^\d+(?:\.\d+){0,2}$").unwrap();
148 if !re.is_match(value.trim()) {
149 if semver::Version::parse(value).is_err() {
150 Some(Problem::Warning(Warning::StrangeValue { field: "version".into(),
151 value: value.into(),
152 reason: Some("can be confusing.") }))
153 } else {
154 None
155 }
156 } else {
157 None
158 }
159}
160
161
162pub trait ValidateCrate: PackageSource {
164 fn validate<'t>(&'t self) -> impl IntoIterator<Item = Problem> + 't {
165 let missed =
174 self.metadata().into_iter().flat_map(|meta| {
175 let bins = meta.bin_targets()
176 .into_iter()
177 .filter(|name| !self.bins().contains(name))
178 .map(|name| Problem::UnknownTarget { name: name.to_owned() });
179
180 let examples = meta.example_targets()
181 .into_iter()
182 .filter(|name| !self.examples().contains(name))
183 .map(|name| Problem::UnknownTarget { name: name.to_owned() });
184
185 bins.chain(examples).collect::<Vec<_>>()
186 });
187
188 let crate_name_eq =
189 self.metadata().into_iter().flat_map(|meta| {
190 let mut targets = meta.all_targets().into_iter();
191 if let Some(name) = targets.find(|name| name == &self.name()) {
192 let msg = "target name is the same as the crate name";
193 Some(Problem::Warning(Warning::StrangeValue { field:
194 "target".into(),
195 value: name.into(),
196 reason: Some(msg) }))
197 } else {
198 None
199 }
200 });
201
202 let no_meta = self.metadata()
203 .is_none()
204 .then(|| Problem::Warning(Warning::MissingMetadata));
205
206
207 missed.into_iter().chain(crate_name_eq).chain(no_meta)
208 }
209
210
211 fn validate_for(&self, target: &str) -> impl IntoIterator<Item = Problem> {
212 let no_meta = self.metadata()
213 .is_none()
214 .then(|| Problem::Warning(Warning::MissingMetadata));
215
216 let man = self.bins()
217 .contains(&target)
218 .then_some(false)
219 .or_else(|| self.examples().contains(&target).then_some(true))
220 .map(|dev| self.manifest_override_or_crate(target.into(), dev))
221 .map_or_else(
222 || vec![Problem::UnknownTarget { name: target.to_owned(), }],
223 |m| m.validate().into_iter().collect::<Vec<_>>(),
224 );
225
226 no_meta.into_iter().chain(man)
227 }
228}
229
230impl<T> ValidateCrate for T where T: PackageSource {}
231
232
233#[cfg(test)]
234mod tests {
235 use super::Validate;
236 use super::super::format::Manifest;
237
238
239 #[test]
240 fn validate_version() {
241 let cases = [
242 ("0", true),
243 ("0.0", true),
244 ("0.0.0", true),
245 ("0.0.0-pre", true),
246 ("", false),
247 ("0.0.a", false),
248 ("beta", false),
249 ];
250
251 for (i, (ver, ok)) in cases.iter().enumerate() {
252 let result = super::validate_version(ver);
253 assert_eq!(*ok, result.is_none(), "{i}: {result:?}");
254 }
255 }
256
257
258 #[test]
259 fn manifest_empty() {
260 let m = Manifest::<&str>::default();
261 let errors = m.validate().into_iter().collect::<Vec<_>>();
262 assert_eq!(5, errors.len(), "{:#?}", errors);
263 assert_eq!(3, errors.iter().filter(|e| e.is_err()).count());
264 }
265
266 #[test]
267 fn manifest_valid() {
268 let m = Manifest::<&str> { name: "name".into(),
269 version: "0.0".into(),
270 author: "author".into(),
271 bundle_id: "bundle.id".into(),
272 description: "description".into(),
273 image_path: "image_path".into(),
274 launch_sound_path: "launch_sound_path".into(),
275 content_warning: "content_warning".into(),
276 content_warning2: "content_warning2".into(),
277 build_number: 42.into() };
278 let errors = m.validate().into_iter().collect::<Vec<_>>();
279 assert!(errors.is_empty(), "{:#?}", errors);
280 }
281}