1use std::{
2 cell::RefCell,
3 collections::{HashMap, HashSet},
4 fmt::Debug,
5 hash::Hash,
6 rc::Rc,
7};
8
9use crate::{
10 Attribute, AttributeData, Geometry, Location, SubblockMode, Vector3, array,
11 colormap::NumberRange,
12};
13
14use super::{Problems, Reason};
15
16pub trait Validate {
17 #[doc(hidden)]
18 fn validate_inner(&mut self, _val: &mut Validator);
19
20 fn validate(&mut self) -> Result<Problems, Problems> {
26 let mut val = Validator::new();
27 self.validate_inner(&mut val);
28 val.finish().into_result()
29 }
30}
31
32impl<T: Validate> Validate for Option<T> {
33 #[doc(hidden)]
34 fn validate_inner(&mut self, val: &mut Validator) {
35 if let Some(obj) = self {
36 obj.validate_inner(val);
37 }
38 }
39}
40
41fn normalise([x, y, z]: Vector3) -> Vector3 {
42 let mag = (x * x + y * y + z * z).sqrt();
43 if mag == 0.0 {
44 [0.0, 0.0, 0.0]
45 } else {
46 [x / mag, y / mag, z / mag]
47 }
48}
49
50fn ortho(a: Vector3, b: Vector3) -> bool {
51 const THRESHOLD: f64 = 1e-6;
52 let [x0, y0, z0] = normalise(a);
53 let [x1, y1, z1] = normalise(b);
54 (x0 * x1 + y0 * y1 + z0 * z1).abs() < THRESHOLD
55}
56
57#[derive(Debug)]
58pub struct Validator<'n> {
59 filenames: Rc<Option<HashSet<String>>>,
60 problems: Rc<RefCell<Problems>>,
61 ty: &'static str,
62 name: Option<&'n str>,
63 limit: Option<usize>,
64 extra_errors: u32,
65 extra_warnings: u32,
66}
67
68impl<'n> Validator<'n> {
69 pub(crate) fn new() -> Self {
70 Self {
71 filenames: Default::default(),
72 problems: Default::default(),
73 ty: "",
74 name: None,
75 limit: None,
76 extra_errors: 0,
77 extra_warnings: 0,
78 }
79 }
80
81 pub(crate) fn finish(mut self) -> Problems {
82 if self.extra_warnings > 0 {
83 self.push(Reason::MoreWarnings(self.extra_warnings), None);
84 }
85 if self.extra_errors > 0 {
86 self.push(Reason::MoreErrors(self.extra_errors), None);
87 }
88 self.problems.take()
89 }
90
91 fn push_full(
92 &mut self,
93 reason: Reason,
94 ty: &'static str,
95 field: Option<&'static str>,
96 name: Option<&str>,
97 ) {
98 let mut problems = self.problems.borrow_mut();
99 match self.limit {
100 Some(limit) if problems.len() >= limit => {
101 if reason.is_error() {
102 self.extra_errors += 1;
103 } else {
104 self.extra_warnings += 1;
105 }
106 }
107 _ => {
108 problems.push(reason, ty, field, name.map(ToOwned::to_owned));
109 }
110 }
111 }
112
113 fn push(&mut self, reason: Reason, field: Option<&'static str>) {
114 self.push_full(reason, self.ty, field, self.name);
115 }
116
117 pub(crate) fn with_filenames<I, T>(mut self, filenames: I) -> Self
118 where
119 I: IntoIterator<Item = T>,
120 T: Into<String>,
121 {
122 self.filenames = Some(filenames.into_iter().map(Into::into).collect()).into();
123 self
124 }
125
126 pub(crate) fn with_limit(mut self, limit: Option<u32>) -> Self {
127 self.limit = limit.map(|n| n.try_into().expect("u32 fits in usize"));
128 self
129 }
130
131 pub(crate) fn enter(&mut self, ty: &'static str) -> Self {
132 Validator {
133 filenames: self.filenames.clone(),
134 problems: self.problems.clone(),
135 ty,
136 name: self.name,
137 limit: None,
138 extra_errors: 0,
139 extra_warnings: 0,
140 }
141 }
142
143 pub(crate) fn name(mut self, name: &'n str) -> Self {
144 self.name = Some(name);
145 self
146 }
147
148 pub(crate) fn obj(mut self, obj: &mut impl Validate) -> Self {
149 obj.validate_inner(&mut self);
150 self
151 }
152
153 pub(crate) fn array(
154 mut self,
155 array: &mut array::Array<impl array::ArrayType>,
156 constraint: array::Constraint,
157 field: &'static str,
158 ) -> Self {
159 _ = array
160 .constrain(constraint)
161 .map_err(|reason| self.push(reason, Some(field)));
162 for reason in array.run_write_checks() {
163 self.push(reason, Some(field));
164 }
165 if let Some(filenames) = self.filenames.as_ref() {
166 if !filenames.contains(array.filename()) {
167 self.push(
168 Reason::ZipMemberMissing(array.filename().to_owned()),
169 Some(field),
170 );
171 }
172 }
173 self
174 }
175
176 pub(crate) fn array_opt(
177 self,
178 array_opt: Option<&mut array::Array<impl array::ArrayType>>,
179 constraint: array::Constraint,
180 field: &'static str,
181 ) -> Self {
182 if let Some(array) = array_opt {
183 self.array(array, constraint, field)
184 } else {
185 self
186 }
187 }
188
189 pub(crate) fn objs<'c>(
190 mut self,
191 objs: impl IntoIterator<Item = &'c mut (impl Validate + 'c)>,
192 ) -> Self {
193 for obj in objs {
194 self = self.obj(obj);
195 }
196 self
197 }
198
199 pub(crate) fn grid_count(mut self, count: &[u64]) -> Self {
200 const MAX: u64 = u32::MAX as u64;
201 if count.iter().any(|n| *n > MAX) {
202 self.push(Reason::GridTooLarge(count.to_vec()), None);
203 }
204 self
205 }
206
207 pub(crate) fn subblock_mode_and_count(
208 mut self,
209 mode: Option<SubblockMode>,
210 count: [u32; 3],
211 ) -> Self {
212 if mode == Some(SubblockMode::Octree) && !count.iter().all(|n| n.is_power_of_two()) {
213 self.push(Reason::OctreeNotPowerOfTwo(count), None);
214 }
215 self
216 }
217
218 pub(crate) fn finite(mut self, value: f64, field: &'static str) -> Self {
219 if !value.is_finite() {
220 self.push(Reason::NotFinite, Some(field));
221 }
222 self
223 }
224
225 pub(crate) fn finite_seq(
226 mut self,
227 values: impl IntoIterator<Item = f64>,
228 field: &'static str,
229 ) -> Self {
230 for value in values {
231 if !value.is_finite() {
232 self.push(Reason::NotFinite, Some(field));
233 break;
234 }
235 }
236 self
237 }
238
239 pub(crate) fn above_zero<T>(mut self, value: T, field: &'static str) -> Self
240 where
241 T: Default + PartialOrd,
242 {
243 if value <= T::default() {
244 self.push(Reason::NotGreaterThanZero, Some(field));
245 }
246 self
247 }
248
249 pub(crate) fn above_zero_seq<T, I>(mut self, values: I, field: &'static str) -> Self
250 where
251 T: Default + PartialOrd,
252 I: IntoIterator<Item = T>,
253 {
254 for value in values {
255 if value <= T::default() {
256 self.push(Reason::NotGreaterThanZero, Some(field));
257 break;
258 }
259 }
260 self
261 }
262
263 pub(crate) fn min_max(mut self, range: NumberRange) -> Self {
264 let ok = match range {
265 NumberRange::Float { min, max } => {
266 self = self.finite(min, "min").finite(max, "max");
267 !min.is_finite() || !max.is_finite() || min <= max
268 }
269 NumberRange::Integer { min, max } => min <= max,
270 NumberRange::Date { min, max } => min <= max,
271 NumberRange::DateTime { min, max } => min <= max,
272 };
273 if !ok {
274 self.push(Reason::MinMaxOutOfOrder(range), Some("range"));
275 }
276 self
277 }
278
279 pub(crate) fn unique<T: Eq + Hash + Debug + Copy>(
280 mut self,
281 values: impl IntoIterator<Item = T>,
282 field: &'static str,
283 is_error: bool,
284 ) -> Self {
285 let mut seen = HashMap::new();
286 for value in values {
287 let count = seen.entry(value).or_insert(0_usize);
288 if *count == 1 {
289 if is_error {
290 self.push(Reason::NotUnique(format!("{value:?}")), Some(field));
291 } else {
292 self.push(Reason::SoftNotUnique(format!("{value:?}")), Some(field));
293 }
294 }
295 *count += 1;
296 }
297 self
298 }
299
300 pub(crate) fn unit_vector(mut self, [x, y, z]: Vector3, field: &'static str) -> Self {
301 const THRESHOLD: f64 = 1e-6;
302 let mag2 = x * x + y * y + z * z;
303 if (1.0 - mag2).abs() >= THRESHOLD {
304 let len = (mag2.sqrt() * 1e7).floor() / 1e7;
305 self.push(Reason::NotUnitVector([x, y, z], len), Some(field));
306 }
307 self
308 }
309
310 pub(crate) fn vectors_ortho2(mut self, u: Vector3, v: Vector3) -> Self {
311 if !ortho(u, v) {
312 self.push(Reason::NotOrthogonal(u, v), None);
313 }
314 self
315 }
316
317 pub(crate) fn vectors_ortho3(mut self, u: Vector3, v: Vector3, w: Vector3) -> Self {
318 for (a, b) in [(u, v), (u, w), (v, w)] {
319 if !ortho(u, v) {
320 self.push(Reason::NotOrthogonal(a, b), None);
321 break;
322 }
323 }
324 self
325 }
326
327 pub(crate) fn array_size(mut self, size: u64, required: u64, field: &'static str) -> Self {
328 if size != required {
329 self.push(Reason::AttrLengthMismatch(size, required), Some(field));
330 }
331 self
332 }
333
334 pub(crate) fn array_size_opt(
335 self,
336 size_opt: Option<u64>,
337 required: u64,
338 field: &'static str,
339 ) -> Self {
340 if let Some(size) = size_opt {
341 self.array_size(size, required, field)
342 } else {
343 self
344 }
345 }
346
347 pub(crate) fn attrs_on_geometry(mut self, attrs: &Vec<Attribute>, geometry: &Geometry) -> Self {
348 for attr in attrs {
349 if matches!(attr.data, AttributeData::ProjectedTexture { .. })
350 != (attr.location == Location::Projected)
351 {
352 self.push_full(
353 Reason::AttrLocationWrongForAttr(attr.location, attr.data.type_name()),
354 "Attribute",
355 Some("location"),
356 Some(&attr.name),
357 );
358 } else if let Some(geom_len) = geometry.location_len(attr.location) {
359 if geom_len != attr.len() {
360 self.push_full(
361 Reason::AttrLengthMismatch(attr.len(), geom_len),
362 "Attribute",
363 None,
364 Some(&attr.name),
365 );
366 }
367 } else {
368 self.push_full(
369 Reason::AttrLocationWrongForGeom(attr.location, geometry.type_name()),
370 "Attribute",
371 Some("location"),
372 Some(&attr.name),
373 );
374 }
375 }
376 self
377 }
378
379 pub(crate) fn attrs_on_attribute(mut self, attrs: &Vec<Attribute>, n_categories: u64) -> Self {
380 for attr in attrs {
381 if attr.location != Location::Categories {
382 self.push_full(
383 Reason::AttrLocationWrongForGeom(attr.location, "AttributeData::Categories"),
384 "Attribute",
385 Some("location"),
386 Some(&attr.name),
387 );
388 } else if attr.len() != n_categories {
389 self.push_full(
390 Reason::AttrLengthMismatch(attr.len(), n_categories),
391 "Attribute",
392 None,
393 Some(&attr.name),
394 );
395 }
396 }
397 self
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use crate::{Array, PointSet, array_type};
404
405 use super::*;
406
407 #[test]
409 fn problems_into_result() {
410 let mut problems = Problems::default();
411 problems.push(
412 Reason::SoftNotUnique("x".to_owned()),
413 "Test",
414 Some("field"),
415 None,
416 );
417 assert_eq!(problems.into_result().unwrap().len(), 1);
418 }
419
420 #[test]
421 fn validator_basics() {
422 let mut v = Validator::new().enter("Test");
423 v.push(Reason::NotFinite, None);
424 v.push(Reason::NotFinite, Some("field"));
425 v = v.name("name");
426 v.push(Reason::NotFinite, None);
427 v.push(Reason::NotFinite, Some("field"));
428 let errors: Vec<_> = v
429 .finish()
430 .into_iter()
431 .map(|prob| prob.to_string())
432 .collect();
433 assert_eq!(
434 errors,
435 vec![
436 "Error: 'Test' must be finite",
437 "Error: 'Test::field' must be finite",
438 "Error: 'Test' must be finite, inside 'name'",
439 "Error: 'Test::field' must be finite, inside 'name'",
440 ]
441 )
442 }
443
444 #[test]
445 fn validator_checks() {
446 let attrs = vec![
447 Attribute::new(
448 "a",
449 Location::Vertices,
450 AttributeData::Number {
451 values: Array::new("1.parquet".to_owned(), 100).into(),
452 colormap: None,
453 },
454 ),
455 Attribute::new(
456 "b",
457 Location::Primitives, AttributeData::Number {
459 values: Array::new("2.parquet".to_owned(), 100).into(),
460 colormap: None,
461 },
462 ),
463 Attribute::new(
464 "c",
465 Location::Vertices,
466 AttributeData::Number {
467 values: Array::new("3.parquet".to_owned(), 101).into(), colormap: None,
469 },
470 ),
471 Attribute::new(
472 "c",
473 Location::Vertices, AttributeData::ProjectedTexture {
475 orient: Default::default(),
476 width: 10.0,
477 height: 10.0,
478 image: Array::new("4.jpeg".to_owned(), 100),
479 },
480 ),
481 Attribute::new(
482 "d",
483 Location::Projected,
484 AttributeData::ProjectedTexture {
485 orient: Default::default(),
486 width: 10.0,
487 height: 10.0,
488 image: Array::new("6.png".to_owned(), 100), },
490 ),
491 ];
492 let results: Vec<_> = Validator::new()
493 .with_filenames(["1.parquet"])
494 .enter("Test")
495 .finite(0.0, "zero")
496 .finite(f64::INFINITY, "inf") .finite(f64::NAN, "nan") .finite_seq([0.0, f64::NEG_INFINITY, f64::NAN], "seq") .above_zero_seq([1.0, 2.0, 3.0], "normal")
500 .above_zero_seq([1.0, 0.0, -1.0], "seq") .above_zero_seq([1.0, f64::NAN], "seq_nan")
502 .min_max(NumberRange::Float {
503 min: f64::NAN,
505 max: 100.0,
506 })
507 .min_max(NumberRange::Float {
508 min: 100.0,
509 max: 100.0,
510 })
511 .min_max(NumberRange::Float {
512 min: 101.5,
513 max: 100.0,
514 }) .unit_vector([1.0, 0.0, 0.0], "i")
516 .unit_vector([0.5 * 2.0_f64.sqrt(), 0.5 * 2.0_f64.sqrt(), 0.0], "angled")
517 .unit_vector([0.5, 0.0, 0.0], "short") .vectors_ortho2([1.0, 0.0, 0.0], [0.0, 1.0, 0.0])
519 .vectors_ortho2([0.8, 0.0, 0.0], [0.0, 0.0, 1.0])
520 .vectors_ortho2([0.0, 1.0, 0.0], [0.0, 0.0, -1.0])
521 .vectors_ortho2([1.0, 0.0, 0.0], [0.8, 0.2, 0.0]) .vectors_ortho3([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0])
523 .vectors_ortho3([1.0, 0.001, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]) .attrs_on_geometry(
525 &attrs,
526 &PointSet::new(Array::new("5.parquet".to_owned(), 100)).into(),
527 ) .array(
529 &mut Array::<array_type::Text>::new("1.parquet".to_owned(), 10),
530 array::Constraint::String,
531 "fine",
532 )
533 .array(
534 &mut Array::<array_type::Text>::new("2.parquet".to_owned(), 10),
536 array::Constraint::String,
537 "missing",
538 )
539 .subblock_mode_and_count(None, [16, 8, 15])
540 .subblock_mode_and_count(Some(SubblockMode::Full), [16, 8, 5])
541 .subblock_mode_and_count(Some(SubblockMode::Octree), [16, 8, 5]) .subblock_mode_and_count(Some(SubblockMode::Octree), [16, 8, 4])
543 .unique([0; 0], "empty", true)
544 .unique([1], "single", true)
545 .unique([1, 2, 3, 4], "normal", false)
546 .unique([1, 2, 3, 4, 2], "dupped", true) .unique(["a", "b", "c", "d", "c", "a", "a"], "multiple", false) .finish()
549 .into_iter()
550 .map(|p| p.to_string())
551 .collect();
552 let mut expected = vec![
553 "Error: 'Test::inf' must be finite",
554 "Error: 'Test::nan' must be finite",
555 "Error: 'Test::seq' must be finite",
556 "Error: 'Test::seq' must be greater than zero",
557 "Error: 'Test::min' must be finite",
558 "Error: 'Test::range' minimum is greater than maximum in [101.5, 100]",
559 "Error: 'Test::short' must be a unit vector but [0.5, 0.0, 0.0] length is 0.5",
560 "Error: 'Test' vectors are not orthogonal: [1.0, 0.0, 0.0] [0.8, 0.2, 0.0]",
561 "Error: 'Test' vectors are not orthogonal: [1.0, 0.001, 0.0] [0.0, 1.0, 0.0]",
562 "Error: 'Attribute::location' is Primitives which is not valid on PointSet geometry, inside 'b'",
563 "Error: 'Attribute' length 101 does not match geometry (100), inside 'c'",
564 "Error: 'Attribute::location' is Vertices which is not valid on ProjectedTexture attributes, inside 'c'",
565 "Error: 'Test::missing' refers to non-existent archive member '2.parquet'",
566 "Error: 'Test' sub-block counts [16, 8, 5] must be powers of two for octree mode",
567 "Error: 'Test::dupped' must be unique but 2 is repeated",
568 "Warning: 'Test::multiple' contains duplicate of \"c\"",
569 "Warning: 'Test::multiple' contains duplicate of \"a\"",
570 ];
571 let mut unexpected = Vec::new();
572 for s in results {
573 if let Some(index) = expected.iter().position(|e| *e == &s) {
574 expected.remove(index);
575 } else {
576 unexpected.push(s.to_owned());
577 }
578 }
579 if !unexpected.is_empty() || !expected.is_empty() {
580 panic!("unexpected problems: {unexpected:#?}\nexpected but not found: {expected:#?}");
581 }
582 }
583}