Skip to main content

jobberdb/
check.rs

1//! Check a job before insertion into job database.
2
3use super::prelude::*;
4use std::collections::HashSet;
5use strum::IntoEnumIterator;
6use strum_macros::EnumIter;
7
8/// Selectable checks
9#[derive(Hash, Eq, PartialEq, EnumIter, Debug)]
10pub enum Check {
11    /// Emit `Warning::Overlaps` if job would overlap another in time.
12    Overlaps,
13    /// Emit `Warning::UnknownTags` if any tags of the new job are unknown within the database.
14    UnknownTags,
15    /// Emit `Warning::ConfirmDeletion` if a job is about to be deleted.
16    /// This check is done outside of `Checks`.
17    ConfirmDeletion,
18}
19
20/// A set of selectable checks.
21#[derive(Debug)]
22pub struct Checks(HashSet<Check>);
23
24impl Checks {
25    /// Select all checks.
26    pub fn all() -> Self {
27        Self(HashSet::from_iter(Check::iter()))
28    }
29    /// Select all checks but the given one.
30    pub fn all_but(check: Check) -> Self {
31        Self(HashSet::from_iter(Check::iter().filter(|c| *c != check)))
32    }
33    /// Omit all checks.
34    pub fn omit() -> Self {
35        Self(HashSet::new())
36    }
37    /// omit checks which lead to user confirmation
38    pub fn no_confirm() -> Self {
39        Self(HashSet::from([Check::Overlaps]))
40    }
41    /// Return `true` if the given check is included.
42    pub fn has(&self, check: Check) -> bool {
43        self.0.contains(&check)
44    }
45    /// Check a job.
46    /// # Arguments
47    /// - `jobs`: Reference to the database.
48    /// - `pos`: Position at which the job will be stored. This is important for modifying existing jobs.
49    /// - `job`: The job to add or modify.
50    /// - `context`: Temporal context to use.
51    pub fn check(
52        &self,
53        jobs: &Jobs,
54        pos: Option<usize>,
55        job: &Job,
56        context: &Context,
57    ) -> Result<(), Error> {
58        let mut warnings = Vec::new();
59
60        // check for temporal plausibility
61        if let Some(end) = job.end {
62            if job.start >= end {
63                return Err(Error::EndBeforeStart(job.start, end));
64            }
65        }
66
67        // check for overlapping
68        if self.has(Check::Overlaps) {
69            let mut overlapping = JobList::new_from(jobs);
70            for (n, j) in jobs.iter().enumerate() {
71                if !j.is_deleted() {
72                    if let Some(pos) = pos {
73                        if n != pos && job.overlaps(j, context) {
74                            overlapping.push(n, j);
75                        }
76                    } else if job.overlaps(j, context) {
77                        overlapping.push(n, j);
78                    }
79                }
80            }
81            if !overlapping.is_empty() {
82                warnings.push(Warning::Overlaps {
83                    new: job.clone(),
84                    existing: overlapping.into(),
85                });
86            }
87        }
88
89        // check for unknown tag
90        if self.has(Check::UnknownTags) {
91            let tags = jobs.tags();
92            let unknown_tags: TagSet = job.tags.filter(|tag| !tags.contains(tag));
93            if !unknown_tags.0.is_empty() {
94                warnings.push(Warning::UnknownTags(unknown_tags));
95            }
96        }
97
98        // check for colliding tags
99        jobs.configuration.get_checked(&job.tags)?;
100
101        // react if any warnings
102        if !warnings.is_empty() {
103            return Err(Error::Warnings(warnings));
104        }
105        Ok(())
106    }
107}