omf/validate/
validator.rs

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    /// Call to validate the object, returning errors and warnings.
21    ///
22    /// The `Ok` value is a `Problems` object contain that is either empty or contains
23    /// only warnings. The `Err` value is a `Problems` object containing at least one
24    /// error.
25    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 that if you have only warnings the result is `Ok`.
408    #[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, // location error
458                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(), // length error
468                    colormap: None,
469                },
470            ),
471            Attribute::new(
472                "c",
473                Location::Vertices, // error
474                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), // missing file error
489                },
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") // error
497            .finite(f64::NAN, "nan") // error
498            .finite_seq([0.0, f64::NEG_INFINITY, f64::NAN], "seq") // error
499            .above_zero_seq([1.0, 2.0, 3.0], "normal")
500            .above_zero_seq([1.0, 0.0, -1.0], "seq") // error
501            .above_zero_seq([1.0, f64::NAN], "seq_nan")
502            .min_max(NumberRange::Float {
503                // error
504                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            }) // error
515            .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") // error
518            .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]) // error
522            .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]) // error
524            .attrs_on_geometry(
525                &attrs,
526                &PointSet::new(Array::new("5.parquet".to_owned(), 100)).into(),
527            ) // 3 errors
528            .array(
529                &mut Array::<array_type::Text>::new("1.parquet".to_owned(), 10),
530                array::Constraint::String,
531                "fine",
532            )
533            .array(
534                // error
535                &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]) // error
542            .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) // warning
547            .unique(["a", "b", "c", "d", "c", "a", "a"], "multiple", false) // 2 warnings
548            .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}