Skip to main content

dpm_model/
validation.rs

1use crate::Plan;
2use thiserror::Error;
3
4mod conditions;
5mod dependency;
6mod entities;
7mod events;
8mod graph;
9mod instructions;
10mod provisional;
11
12/// Invalid duration uncertainty supplied by a plan author.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
14pub enum EstimateError {
15    /// At least one bound is NaN or infinite.
16    #[error("estimate values must be finite")]
17    NonFinite,
18    /// The optimistic bound is negative.
19    #[error("optimistic duration must be non-negative")]
20    Negative,
21    /// The bounds are not ordered optimistic <= likely <= pessimistic.
22    #[error("estimate must satisfy optimistic <= likely <= pessimistic")]
23    Unordered,
24}
25
26/// A graph invariant violated by external or edited domain data.
27#[derive(Debug, Error)]
28pub enum ValidationError {
29    /// Malformed entity, reference, or lifecycle invariant.
30    #[error("invalid {entity} {id}: {reason}")]
31    Invalid {
32        /// Entity category containing the invalid value.
33        entity: &'static str,
34        /// Identifier or human-readable context of the invalid entity.
35        id: String,
36        /// Invariant that was violated.
37        reason: String,
38    },
39    /// A task contains invalid duration uncertainty.
40    #[error("invalid estimate for work {work}: {source}")]
41    Estimate {
42        /// Work with the malformed estimate.
43        work: crate::WorkItemId,
44        /// Original estimate validation failure.
45        #[source]
46        source: EstimateError,
47    },
48}
49
50pub(crate) fn invalid(
51    entity: &'static str,
52    id: impl std::fmt::Display,
53    reason: impl Into<String>,
54) -> ValidationError {
55    ValidationError::Invalid {
56        entity,
57        id: id.to_string(),
58        reason: reason.into(),
59    }
60}
61
62impl Plan {
63    /// Validate references, keys, containment, execution contracts, and numerical inputs.
64    ///
65    /// Call this at import, command, and persistence boundaries. It does not mutate the plan.
66    pub fn validate(&self) -> Result<(), ValidationError> {
67        if self.format_version != 3 {
68            return Err(invalid(
69                "plan format",
70                self.workspace.id,
71                format!("unsupported version {}; expected 3", self.format_version),
72            ));
73        }
74        for work in self.work_items.values() {
75            if !work.order.is_valid() {
76                return Err(invalid(
77                    "work order",
78                    work.id,
79                    "order requires 1..128 digits and a nonzero final digit",
80                ));
81            }
82        }
83        crate::asset::validate(self)?;
84        entities::validate(self)?;
85        crate::tracking::validate(self)?;
86        graph::validate(self)?;
87        conditions::validate(self)?;
88        dependency::validate(self)?;
89        crate::calendar::validate::validate(self)?;
90        provisional::validate(self)
91    }
92}
93
94#[cfg(test)]
95mod tests;