Skip to main content

haystack_core/ontology/
validation.rs

1// Validation issue types for ontology and graph validation.
2
3/// Describes why an entity does not fit a type.
4#[derive(Debug, Clone, PartialEq)]
5pub enum FitIssue {
6    /// Entity is missing a mandatory marker tag.
7    MissingMarker {
8        /// The missing marker name.
9        tag: String,
10        /// The type that requires this marker.
11        spec: String,
12    },
13    /// Tag value has wrong type.
14    WrongType {
15        /// The tag name.
16        tag: String,
17        /// Expected type description.
18        expected: String,
19        /// Actual type description.
20        actual: String,
21    },
22    /// Ref tag points to wrong entity type.
23    InvalidRef {
24        /// The ref tag name.
25        tag: String,
26        /// Expected target type.
27        expected: String,
28        /// Actual target type or description.
29        actual: String,
30    },
31    /// Entity is missing a required choice selection.
32    MissingChoice {
33        /// The choice tag name.
34        tag: String,
35        /// Valid options for this choice.
36        options: Vec<String>,
37    },
38    /// A value constraint was violated (minVal, maxVal, pattern, etc.)
39    ConstraintViolation {
40        /// The tag name.
41        tag: String,
42        /// The constraint that was violated.
43        constraint: String,
44        /// Human-readable detail.
45        detail: String,
46    },
47    /// A choice slot has an invalid value.
48    InvalidChoice {
49        /// The tag name.
50        tag: String,
51        /// The invalid value.
52        value: String,
53        /// Valid options.
54        valid_options: Vec<String>,
55    },
56}
57
58/// A validation problem found in an entity or graph.
59#[derive(Debug, Clone, PartialEq)]
60pub struct ValidationIssue {
61    /// The entity ref val, or `None` for schema-level issues.
62    pub entity: Option<String>,
63    /// Category such as `"missing_marker"` or `"invalid_ref"`.
64    pub issue_type: String,
65    /// Human-readable description of the problem.
66    pub detail: String,
67}
68
69impl std::fmt::Display for FitIssue {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        match self {
72            FitIssue::MissingMarker { tag, spec } => {
73                write!(f, "missing mandatory marker '{tag}' for spec '{spec}'")
74            }
75            FitIssue::WrongType {
76                tag,
77                expected,
78                actual,
79            } => {
80                write!(
81                    f,
82                    "wrong type for '{tag}': expected {expected}, got {actual}"
83                )
84            }
85            FitIssue::InvalidRef {
86                tag,
87                expected,
88                actual,
89            } => {
90                write!(
91                    f,
92                    "invalid ref for '{tag}': expected {expected}, got {actual}"
93                )
94            }
95            FitIssue::MissingChoice { tag, options } => {
96                write!(
97                    f,
98                    "missing choice for '{tag}': options are {}",
99                    options.join(", ")
100                )
101            }
102            FitIssue::ConstraintViolation {
103                tag,
104                constraint,
105                detail,
106            } => {
107                write!(f, "constraint '{constraint}' violated on '{tag}': {detail}")
108            }
109            FitIssue::InvalidChoice {
110                tag,
111                value,
112                valid_options,
113            } => {
114                write!(
115                    f,
116                    "invalid choice for '{tag}': '{}' not in [{}]",
117                    value,
118                    valid_options.join(", ")
119                )
120            }
121        }
122    }
123}
124
125impl std::fmt::Display for ValidationIssue {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(
128            f,
129            "[{}] {}: {}",
130            self.issue_type,
131            self.entity.as_deref().unwrap_or("?"),
132            self.detail
133        )
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn fit_issue_missing_marker() {
143        let issue = FitIssue::MissingMarker {
144            tag: "equip".to_string(),
145            spec: "ahu".to_string(),
146        };
147        match &issue {
148            FitIssue::MissingMarker { tag, spec } => {
149                assert_eq!(tag, "equip");
150                assert_eq!(spec, "ahu");
151            }
152            _ => panic!("wrong variant"),
153        }
154    }
155
156    #[test]
157    fn fit_issue_equality() {
158        let a = FitIssue::MissingMarker {
159            tag: "equip".to_string(),
160            spec: "ahu".to_string(),
161        };
162        let b = FitIssue::MissingMarker {
163            tag: "equip".to_string(),
164            spec: "ahu".to_string(),
165        };
166        assert_eq!(a, b);
167    }
168
169    #[test]
170    fn validation_issue() {
171        let issue = ValidationIssue {
172            entity: Some("site-1".to_string()),
173            issue_type: "missing_marker".to_string(),
174            detail: "Entity claims 'ahu' but is missing mandatory marker 'equip'".to_string(),
175        };
176        assert_eq!(issue.entity, Some("site-1".to_string()));
177        assert_eq!(issue.issue_type, "missing_marker");
178    }
179}