playdate_build/metadata/
validation.rs

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
88/// Check the implementor validity.
89pub trait Validate {
90	/// Check critical requirements, returns it as errors.
91	/// Also returns warnings fo not so critical problems.
92	/// Use it before render the final result.
93	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		// required fields
131		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
162/// Lint the crate-level source.
163pub trait ValidateCrate: PackageSource {
164	fn validate<'t>(&'t self) -> impl IntoIterator<Item = Problem> + 't {
165		// - main manifest missing fields
166		// - main manifest fields in bad format
167		// - for each final target manifest:
168		//   -> same as for the main manifest
169
170
171		// Check that all targets are exists
172		// - search the target in the crate for each in meta.all_targets
173		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}