Skip to main content

agm_core/model/
fields.rs

1//! Primitive field types, enums, and spans used throughout the AGM model.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6
7// ---------------------------------------------------------------------------
8// Errors
9// ---------------------------------------------------------------------------
10
11/// Error returned when parsing a string into an enum variant fails.
12#[derive(Debug, Clone, PartialEq, thiserror::Error)]
13#[error("invalid {type_name} value: {value:?}")]
14pub struct ParseEnumError {
15    pub type_name: &'static str,
16    pub value: String,
17}
18
19// ---------------------------------------------------------------------------
20// FieldValue
21// ---------------------------------------------------------------------------
22
23/// A dynamically-typed AGM field value.
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25#[serde(untagged)]
26pub enum FieldValue {
27    Scalar(String),
28    List(Vec<String>),
29    Block(String),
30}
31
32// ---------------------------------------------------------------------------
33// Span
34// ---------------------------------------------------------------------------
35
36/// Source location span for diagnostics. Line numbers are 1-indexed.
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
38pub struct Span {
39    pub start_line: usize,
40    pub end_line: usize,
41}
42
43impl Span {
44    #[must_use]
45    pub fn new(start_line: usize, end_line: usize) -> Self {
46        Self {
47            start_line,
48            end_line,
49        }
50    }
51}
52
53// ---------------------------------------------------------------------------
54// NodeType
55// ---------------------------------------------------------------------------
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum NodeType {
60    Facts,
61    Rules,
62    Workflow,
63    Entity,
64    Decision,
65    Exception,
66    Example,
67    Glossary,
68    AntiPattern,
69    Orchestration,
70    Custom(String),
71}
72
73impl fmt::Display for NodeType {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::Facts => write!(f, "facts"),
77            Self::Rules => write!(f, "rules"),
78            Self::Workflow => write!(f, "workflow"),
79            Self::Entity => write!(f, "entity"),
80            Self::Decision => write!(f, "decision"),
81            Self::Exception => write!(f, "exception"),
82            Self::Example => write!(f, "example"),
83            Self::Glossary => write!(f, "glossary"),
84            Self::AntiPattern => write!(f, "anti_pattern"),
85            Self::Orchestration => write!(f, "orchestration"),
86            Self::Custom(s) => write!(f, "{s}"),
87        }
88    }
89}
90
91impl FromStr for NodeType {
92    type Err = std::convert::Infallible;
93
94    fn from_str(s: &str) -> Result<Self, Self::Err> {
95        Ok(match s {
96            "facts" => Self::Facts,
97            "rules" => Self::Rules,
98            "workflow" => Self::Workflow,
99            "entity" => Self::Entity,
100            "decision" => Self::Decision,
101            "exception" => Self::Exception,
102            "example" => Self::Example,
103            "glossary" => Self::Glossary,
104            "anti_pattern" => Self::AntiPattern,
105            "orchestration" => Self::Orchestration,
106            other => Self::Custom(other.to_owned()),
107        })
108    }
109}
110
111// ---------------------------------------------------------------------------
112// Priority
113// ---------------------------------------------------------------------------
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116#[serde(rename_all = "snake_case")]
117pub enum Priority {
118    Critical,
119    High,
120    Normal,
121    Low,
122}
123
124impl fmt::Display for Priority {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        match self {
127            Self::Critical => write!(f, "critical"),
128            Self::High => write!(f, "high"),
129            Self::Normal => write!(f, "normal"),
130            Self::Low => write!(f, "low"),
131        }
132    }
133}
134
135impl FromStr for Priority {
136    type Err = ParseEnumError;
137
138    fn from_str(s: &str) -> Result<Self, Self::Err> {
139        match s {
140            "critical" => Ok(Self::Critical),
141            "high" => Ok(Self::High),
142            "normal" => Ok(Self::Normal),
143            "low" => Ok(Self::Low),
144            _ => Err(ParseEnumError {
145                type_name: "Priority",
146                value: s.to_owned(),
147            }),
148        }
149    }
150}
151
152// ---------------------------------------------------------------------------
153// Stability
154// ---------------------------------------------------------------------------
155
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum Stability {
159    High,
160    Medium,
161    Low,
162    Volatile,
163}
164
165impl fmt::Display for Stability {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        match self {
168            Self::High => write!(f, "high"),
169            Self::Medium => write!(f, "medium"),
170            Self::Low => write!(f, "low"),
171            Self::Volatile => write!(f, "volatile"),
172        }
173    }
174}
175
176impl FromStr for Stability {
177    type Err = ParseEnumError;
178
179    fn from_str(s: &str) -> Result<Self, Self::Err> {
180        match s {
181            "high" => Ok(Self::High),
182            "medium" => Ok(Self::Medium),
183            "low" => Ok(Self::Low),
184            "volatile" => Ok(Self::Volatile),
185            _ => Err(ParseEnumError {
186                type_name: "Stability",
187                value: s.to_owned(),
188            }),
189        }
190    }
191}
192
193// ---------------------------------------------------------------------------
194// Confidence
195// ---------------------------------------------------------------------------
196
197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198#[serde(rename_all = "snake_case")]
199pub enum Confidence {
200    High,
201    Medium,
202    Low,
203    Inferred,
204    Tentative,
205}
206
207impl fmt::Display for Confidence {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        match self {
210            Self::High => write!(f, "high"),
211            Self::Medium => write!(f, "medium"),
212            Self::Low => write!(f, "low"),
213            Self::Inferred => write!(f, "inferred"),
214            Self::Tentative => write!(f, "tentative"),
215        }
216    }
217}
218
219impl FromStr for Confidence {
220    type Err = ParseEnumError;
221
222    fn from_str(s: &str) -> Result<Self, Self::Err> {
223        match s {
224            "high" => Ok(Self::High),
225            "medium" => Ok(Self::Medium),
226            "low" => Ok(Self::Low),
227            "inferred" => Ok(Self::Inferred),
228            "tentative" => Ok(Self::Tentative),
229            _ => Err(ParseEnumError {
230                type_name: "Confidence",
231                value: s.to_owned(),
232            }),
233        }
234    }
235}
236
237// ---------------------------------------------------------------------------
238// NodeStatus
239// ---------------------------------------------------------------------------
240
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242#[serde(rename_all = "snake_case")]
243pub enum NodeStatus {
244    Active,
245    Draft,
246    Deprecated,
247    Superseded,
248}
249
250impl fmt::Display for NodeStatus {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        match self {
253            Self::Active => write!(f, "active"),
254            Self::Draft => write!(f, "draft"),
255            Self::Deprecated => write!(f, "deprecated"),
256            Self::Superseded => write!(f, "superseded"),
257        }
258    }
259}
260
261impl FromStr for NodeStatus {
262    type Err = ParseEnumError;
263
264    fn from_str(s: &str) -> Result<Self, Self::Err> {
265        match s {
266            "active" => Ok(Self::Active),
267            "draft" => Ok(Self::Draft),
268            "deprecated" => Ok(Self::Deprecated),
269            "superseded" => Ok(Self::Superseded),
270            _ => Err(ParseEnumError {
271                type_name: "NodeStatus",
272                value: s.to_owned(),
273            }),
274        }
275    }
276}
277
278// ---------------------------------------------------------------------------
279// Tests
280// ---------------------------------------------------------------------------
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_node_type_from_str_known_returns_variant() {
288        assert_eq!("facts".parse::<NodeType>().unwrap(), NodeType::Facts);
289        assert_eq!("rules".parse::<NodeType>().unwrap(), NodeType::Rules);
290        assert_eq!("workflow".parse::<NodeType>().unwrap(), NodeType::Workflow);
291        assert_eq!("entity".parse::<NodeType>().unwrap(), NodeType::Entity);
292        assert_eq!("decision".parse::<NodeType>().unwrap(), NodeType::Decision);
293        assert_eq!(
294            "exception".parse::<NodeType>().unwrap(),
295            NodeType::Exception
296        );
297        assert_eq!("example".parse::<NodeType>().unwrap(), NodeType::Example);
298        assert_eq!("glossary".parse::<NodeType>().unwrap(), NodeType::Glossary);
299        assert_eq!(
300            "anti_pattern".parse::<NodeType>().unwrap(),
301            NodeType::AntiPattern
302        );
303        assert_eq!(
304            "orchestration".parse::<NodeType>().unwrap(),
305            NodeType::Orchestration
306        );
307    }
308
309    #[test]
310    fn test_node_type_from_str_unknown_returns_custom() {
311        assert_eq!(
312            "unknown_custom".parse::<NodeType>().unwrap(),
313            NodeType::Custom("unknown_custom".to_owned())
314        );
315    }
316
317    #[test]
318    fn test_node_type_display_roundtrip() {
319        let types = [
320            NodeType::Facts,
321            NodeType::Rules,
322            NodeType::Workflow,
323            NodeType::Entity,
324            NodeType::Decision,
325            NodeType::Exception,
326            NodeType::Example,
327            NodeType::Glossary,
328            NodeType::AntiPattern,
329            NodeType::Orchestration,
330            NodeType::Custom("my_type".to_owned()),
331        ];
332        for t in &types {
333            let s = t.to_string();
334            let parsed: NodeType = s.parse().unwrap();
335            assert_eq!(&parsed, t);
336        }
337    }
338
339    #[test]
340    fn test_priority_from_str_valid_returns_ok() {
341        assert_eq!("critical".parse::<Priority>().unwrap(), Priority::Critical);
342        assert_eq!("high".parse::<Priority>().unwrap(), Priority::High);
343        assert_eq!("normal".parse::<Priority>().unwrap(), Priority::Normal);
344        assert_eq!("low".parse::<Priority>().unwrap(), Priority::Low);
345    }
346
347    #[test]
348    fn test_priority_from_str_invalid_returns_error() {
349        let err = "invalid".parse::<Priority>().unwrap_err();
350        assert_eq!(err.type_name, "Priority");
351        assert_eq!(err.value, "invalid");
352    }
353
354    #[test]
355    fn test_priority_display_roundtrip() {
356        for p in [
357            Priority::Critical,
358            Priority::High,
359            Priority::Normal,
360            Priority::Low,
361        ] {
362            let s = p.to_string();
363            assert_eq!(s.parse::<Priority>().unwrap(), p);
364        }
365    }
366
367    #[test]
368    fn test_stability_from_str_valid_returns_ok() {
369        assert_eq!("high".parse::<Stability>().unwrap(), Stability::High);
370        assert_eq!("medium".parse::<Stability>().unwrap(), Stability::Medium);
371        assert_eq!("low".parse::<Stability>().unwrap(), Stability::Low);
372        assert_eq!(
373            "volatile".parse::<Stability>().unwrap(),
374            Stability::Volatile
375        );
376    }
377
378    #[test]
379    fn test_stability_from_str_invalid_returns_error() {
380        let err = "wrong".parse::<Stability>().unwrap_err();
381        assert_eq!(err.type_name, "Stability");
382    }
383
384    #[test]
385    fn test_stability_display_roundtrip() {
386        for s in [
387            Stability::High,
388            Stability::Medium,
389            Stability::Low,
390            Stability::Volatile,
391        ] {
392            let text = s.to_string();
393            assert_eq!(text.parse::<Stability>().unwrap(), s);
394        }
395    }
396
397    #[test]
398    fn test_confidence_from_str_valid_returns_ok() {
399        assert_eq!("high".parse::<Confidence>().unwrap(), Confidence::High);
400        assert_eq!("medium".parse::<Confidence>().unwrap(), Confidence::Medium);
401        assert_eq!("low".parse::<Confidence>().unwrap(), Confidence::Low);
402        assert_eq!(
403            "inferred".parse::<Confidence>().unwrap(),
404            Confidence::Inferred
405        );
406        assert_eq!(
407            "tentative".parse::<Confidence>().unwrap(),
408            Confidence::Tentative
409        );
410    }
411
412    #[test]
413    fn test_confidence_from_str_invalid_returns_error() {
414        let err = "maybe".parse::<Confidence>().unwrap_err();
415        assert_eq!(err.type_name, "Confidence");
416    }
417
418    #[test]
419    fn test_confidence_display_roundtrip() {
420        for c in [
421            Confidence::High,
422            Confidence::Medium,
423            Confidence::Low,
424            Confidence::Inferred,
425            Confidence::Tentative,
426        ] {
427            let text = c.to_string();
428            assert_eq!(text.parse::<Confidence>().unwrap(), c);
429        }
430    }
431
432    #[test]
433    fn test_node_status_from_str_valid_returns_ok() {
434        assert_eq!("active".parse::<NodeStatus>().unwrap(), NodeStatus::Active);
435        assert_eq!("draft".parse::<NodeStatus>().unwrap(), NodeStatus::Draft);
436        assert_eq!(
437            "deprecated".parse::<NodeStatus>().unwrap(),
438            NodeStatus::Deprecated
439        );
440        assert_eq!(
441            "superseded".parse::<NodeStatus>().unwrap(),
442            NodeStatus::Superseded
443        );
444    }
445
446    #[test]
447    fn test_node_status_from_str_invalid_returns_error() {
448        let err = "archived".parse::<NodeStatus>().unwrap_err();
449        assert_eq!(err.type_name, "NodeStatus");
450    }
451
452    #[test]
453    fn test_node_status_display_roundtrip() {
454        for ns in [
455            NodeStatus::Active,
456            NodeStatus::Draft,
457            NodeStatus::Deprecated,
458            NodeStatus::Superseded,
459        ] {
460            let text = ns.to_string();
461            assert_eq!(text.parse::<NodeStatus>().unwrap(), ns);
462        }
463    }
464
465    #[test]
466    fn test_field_value_scalar_debug() {
467        let v = FieldValue::Scalar("hello".to_owned());
468        assert!(format!("{v:?}").contains("Scalar"));
469    }
470
471    #[test]
472    fn test_field_value_list_clone() {
473        let v = FieldValue::List(vec!["a".to_owned(), "b".to_owned()]);
474        assert_eq!(v.clone(), v);
475    }
476
477    #[test]
478    fn test_field_value_serde_roundtrip_scalar() {
479        let v = FieldValue::Scalar("test".to_owned());
480        let json = serde_json::to_string(&v).unwrap();
481        let back: FieldValue = serde_json::from_str(&json).unwrap();
482        assert_eq!(v, back);
483    }
484
485    #[test]
486    fn test_field_value_serde_roundtrip_list() {
487        let v = FieldValue::List(vec!["a".to_owned(), "b".to_owned()]);
488        let json = serde_json::to_string(&v).unwrap();
489        let back: FieldValue = serde_json::from_str(&json).unwrap();
490        assert_eq!(v, back);
491    }
492
493    #[test]
494    fn test_span_new() {
495        let s = Span::new(1, 10);
496        assert_eq!(s.start_line, 1);
497        assert_eq!(s.end_line, 10);
498    }
499}