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    Ticket,
71    Custom(String),
72}
73
74impl fmt::Display for NodeType {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            Self::Facts => write!(f, "facts"),
78            Self::Rules => write!(f, "rules"),
79            Self::Workflow => write!(f, "workflow"),
80            Self::Entity => write!(f, "entity"),
81            Self::Decision => write!(f, "decision"),
82            Self::Exception => write!(f, "exception"),
83            Self::Example => write!(f, "example"),
84            Self::Glossary => write!(f, "glossary"),
85            Self::AntiPattern => write!(f, "anti_pattern"),
86            Self::Orchestration => write!(f, "orchestration"),
87            Self::Ticket => write!(f, "ticket"),
88            Self::Custom(s) => write!(f, "{s}"),
89        }
90    }
91}
92
93impl FromStr for NodeType {
94    type Err = std::convert::Infallible;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        Ok(match s {
98            "facts" => Self::Facts,
99            "rules" => Self::Rules,
100            "workflow" => Self::Workflow,
101            "entity" => Self::Entity,
102            "decision" => Self::Decision,
103            "exception" => Self::Exception,
104            "example" => Self::Example,
105            "glossary" => Self::Glossary,
106            "anti_pattern" => Self::AntiPattern,
107            "orchestration" => Self::Orchestration,
108            "ticket" => Self::Ticket,
109            other => Self::Custom(other.to_owned()),
110        })
111    }
112}
113
114// ---------------------------------------------------------------------------
115// TicketAction
116// ---------------------------------------------------------------------------
117
118/// Declares the intent of a ticket emission (spec §14.4.1).
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
120#[serde(rename_all = "snake_case")]
121pub enum TicketAction {
122    #[default]
123    Create,
124    Edit,
125    Close,
126    Archive,
127    Split,
128    Link,
129}
130
131impl fmt::Display for TicketAction {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match self {
134            Self::Create => write!(f, "create"),
135            Self::Edit => write!(f, "edit"),
136            Self::Close => write!(f, "close"),
137            Self::Archive => write!(f, "archive"),
138            Self::Split => write!(f, "split"),
139            Self::Link => write!(f, "link"),
140        }
141    }
142}
143
144impl FromStr for TicketAction {
145    type Err = ParseEnumError;
146
147    fn from_str(s: &str) -> Result<Self, Self::Err> {
148        match s {
149            "create" => Ok(Self::Create),
150            "edit" => Ok(Self::Edit),
151            "close" => Ok(Self::Close),
152            "archive" => Ok(Self::Archive),
153            "split" => Ok(Self::Split),
154            "link" => Ok(Self::Link),
155            _ => Err(ParseEnumError {
156                type_name: "TicketAction",
157                value: s.to_owned(),
158            }),
159        }
160    }
161}
162
163// ---------------------------------------------------------------------------
164// SddPhase
165// ---------------------------------------------------------------------------
166
167/// Suggests the SDD pipeline phase the ticket belongs to (spec §14.4.2).
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
169#[serde(rename_all = "snake_case")]
170pub enum SddPhase {
171    #[default]
172    Backlog,
173    Explore,
174    Propose,
175    Spec,
176    Design,
177    Tasks,
178    Apply,
179    Verify,
180    Archive,
181}
182
183impl fmt::Display for SddPhase {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        match self {
186            Self::Backlog => write!(f, "backlog"),
187            Self::Explore => write!(f, "explore"),
188            Self::Propose => write!(f, "propose"),
189            Self::Spec => write!(f, "spec"),
190            Self::Design => write!(f, "design"),
191            Self::Tasks => write!(f, "tasks"),
192            Self::Apply => write!(f, "apply"),
193            Self::Verify => write!(f, "verify"),
194            Self::Archive => write!(f, "archive"),
195        }
196    }
197}
198
199impl FromStr for SddPhase {
200    type Err = ParseEnumError;
201
202    fn from_str(s: &str) -> Result<Self, Self::Err> {
203        match s {
204            "backlog" => Ok(Self::Backlog),
205            "explore" => Ok(Self::Explore),
206            "propose" => Ok(Self::Propose),
207            "spec" => Ok(Self::Spec),
208            "design" => Ok(Self::Design),
209            "tasks" => Ok(Self::Tasks),
210            "apply" => Ok(Self::Apply),
211            "verify" => Ok(Self::Verify),
212            "archive" => Ok(Self::Archive),
213            _ => Err(ParseEnumError {
214                type_name: "SddPhase",
215                value: s.to_owned(),
216            }),
217        }
218    }
219}
220
221// ---------------------------------------------------------------------------
222// Priority
223// ---------------------------------------------------------------------------
224
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226#[serde(rename_all = "snake_case")]
227pub enum Priority {
228    Critical,
229    High,
230    Normal,
231    Low,
232}
233
234impl fmt::Display for Priority {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        match self {
237            Self::Critical => write!(f, "critical"),
238            Self::High => write!(f, "high"),
239            Self::Normal => write!(f, "normal"),
240            Self::Low => write!(f, "low"),
241        }
242    }
243}
244
245impl FromStr for Priority {
246    type Err = ParseEnumError;
247
248    fn from_str(s: &str) -> Result<Self, Self::Err> {
249        match s {
250            "critical" => Ok(Self::Critical),
251            "high" => Ok(Self::High),
252            "normal" => Ok(Self::Normal),
253            "low" => Ok(Self::Low),
254            _ => Err(ParseEnumError {
255                type_name: "Priority",
256                value: s.to_owned(),
257            }),
258        }
259    }
260}
261
262// ---------------------------------------------------------------------------
263// Stability
264// ---------------------------------------------------------------------------
265
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267#[serde(rename_all = "snake_case")]
268pub enum Stability {
269    High,
270    Medium,
271    Low,
272    Volatile,
273}
274
275impl fmt::Display for Stability {
276    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277        match self {
278            Self::High => write!(f, "high"),
279            Self::Medium => write!(f, "medium"),
280            Self::Low => write!(f, "low"),
281            Self::Volatile => write!(f, "volatile"),
282        }
283    }
284}
285
286impl FromStr for Stability {
287    type Err = ParseEnumError;
288
289    fn from_str(s: &str) -> Result<Self, Self::Err> {
290        match s {
291            "high" => Ok(Self::High),
292            "medium" => Ok(Self::Medium),
293            "low" => Ok(Self::Low),
294            "volatile" => Ok(Self::Volatile),
295            _ => Err(ParseEnumError {
296                type_name: "Stability",
297                value: s.to_owned(),
298            }),
299        }
300    }
301}
302
303// ---------------------------------------------------------------------------
304// Confidence
305// ---------------------------------------------------------------------------
306
307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
308#[serde(rename_all = "snake_case")]
309pub enum Confidence {
310    High,
311    Medium,
312    Low,
313    Inferred,
314    Tentative,
315}
316
317impl fmt::Display for Confidence {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        match self {
320            Self::High => write!(f, "high"),
321            Self::Medium => write!(f, "medium"),
322            Self::Low => write!(f, "low"),
323            Self::Inferred => write!(f, "inferred"),
324            Self::Tentative => write!(f, "tentative"),
325        }
326    }
327}
328
329impl FromStr for Confidence {
330    type Err = ParseEnumError;
331
332    fn from_str(s: &str) -> Result<Self, Self::Err> {
333        match s {
334            "high" => Ok(Self::High),
335            "medium" => Ok(Self::Medium),
336            "low" => Ok(Self::Low),
337            "inferred" => Ok(Self::Inferred),
338            "tentative" => Ok(Self::Tentative),
339            _ => Err(ParseEnumError {
340                type_name: "Confidence",
341                value: s.to_owned(),
342            }),
343        }
344    }
345}
346
347// ---------------------------------------------------------------------------
348// NodeStatus
349// ---------------------------------------------------------------------------
350
351#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
352#[serde(rename_all = "snake_case")]
353pub enum NodeStatus {
354    Active,
355    Draft,
356    Deprecated,
357    Superseded,
358}
359
360impl fmt::Display for NodeStatus {
361    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362        match self {
363            Self::Active => write!(f, "active"),
364            Self::Draft => write!(f, "draft"),
365            Self::Deprecated => write!(f, "deprecated"),
366            Self::Superseded => write!(f, "superseded"),
367        }
368    }
369}
370
371impl FromStr for NodeStatus {
372    type Err = ParseEnumError;
373
374    fn from_str(s: &str) -> Result<Self, Self::Err> {
375        match s {
376            "active" => Ok(Self::Active),
377            "draft" => Ok(Self::Draft),
378            "deprecated" => Ok(Self::Deprecated),
379            "superseded" => Ok(Self::Superseded),
380            _ => Err(ParseEnumError {
381                type_name: "NodeStatus",
382                value: s.to_owned(),
383            }),
384        }
385    }
386}
387
388// ---------------------------------------------------------------------------
389// Tests
390// ---------------------------------------------------------------------------
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    #[test]
397    fn test_node_type_from_str_known_returns_variant() {
398        assert_eq!("facts".parse::<NodeType>().unwrap(), NodeType::Facts);
399        assert_eq!("rules".parse::<NodeType>().unwrap(), NodeType::Rules);
400        assert_eq!("workflow".parse::<NodeType>().unwrap(), NodeType::Workflow);
401        assert_eq!("entity".parse::<NodeType>().unwrap(), NodeType::Entity);
402        assert_eq!("decision".parse::<NodeType>().unwrap(), NodeType::Decision);
403        assert_eq!(
404            "exception".parse::<NodeType>().unwrap(),
405            NodeType::Exception
406        );
407        assert_eq!("example".parse::<NodeType>().unwrap(), NodeType::Example);
408        assert_eq!("glossary".parse::<NodeType>().unwrap(), NodeType::Glossary);
409        assert_eq!(
410            "anti_pattern".parse::<NodeType>().unwrap(),
411            NodeType::AntiPattern
412        );
413        assert_eq!(
414            "orchestration".parse::<NodeType>().unwrap(),
415            NodeType::Orchestration
416        );
417        assert_eq!("ticket".parse::<NodeType>().unwrap(), NodeType::Ticket);
418    }
419
420    #[test]
421    fn test_node_type_from_str_unknown_returns_custom() {
422        assert_eq!(
423            "unknown_custom".parse::<NodeType>().unwrap(),
424            NodeType::Custom("unknown_custom".to_owned())
425        );
426    }
427
428    #[test]
429    fn test_node_type_display_roundtrip() {
430        let types = [
431            NodeType::Facts,
432            NodeType::Rules,
433            NodeType::Workflow,
434            NodeType::Entity,
435            NodeType::Decision,
436            NodeType::Exception,
437            NodeType::Example,
438            NodeType::Glossary,
439            NodeType::AntiPattern,
440            NodeType::Orchestration,
441            NodeType::Ticket,
442            NodeType::Custom("my_type".to_owned()),
443        ];
444        for t in &types {
445            let s = t.to_string();
446            let parsed: NodeType = s.parse().unwrap();
447            assert_eq!(&parsed, t);
448        }
449    }
450
451    #[test]
452    fn test_priority_from_str_valid_returns_ok() {
453        assert_eq!("critical".parse::<Priority>().unwrap(), Priority::Critical);
454        assert_eq!("high".parse::<Priority>().unwrap(), Priority::High);
455        assert_eq!("normal".parse::<Priority>().unwrap(), Priority::Normal);
456        assert_eq!("low".parse::<Priority>().unwrap(), Priority::Low);
457    }
458
459    #[test]
460    fn test_priority_from_str_invalid_returns_error() {
461        let err = "invalid".parse::<Priority>().unwrap_err();
462        assert_eq!(err.type_name, "Priority");
463        assert_eq!(err.value, "invalid");
464    }
465
466    #[test]
467    fn test_priority_display_roundtrip() {
468        for p in [
469            Priority::Critical,
470            Priority::High,
471            Priority::Normal,
472            Priority::Low,
473        ] {
474            let s = p.to_string();
475            assert_eq!(s.parse::<Priority>().unwrap(), p);
476        }
477    }
478
479    #[test]
480    fn test_stability_from_str_valid_returns_ok() {
481        assert_eq!("high".parse::<Stability>().unwrap(), Stability::High);
482        assert_eq!("medium".parse::<Stability>().unwrap(), Stability::Medium);
483        assert_eq!("low".parse::<Stability>().unwrap(), Stability::Low);
484        assert_eq!(
485            "volatile".parse::<Stability>().unwrap(),
486            Stability::Volatile
487        );
488    }
489
490    #[test]
491    fn test_stability_from_str_invalid_returns_error() {
492        let err = "wrong".parse::<Stability>().unwrap_err();
493        assert_eq!(err.type_name, "Stability");
494    }
495
496    #[test]
497    fn test_stability_display_roundtrip() {
498        for s in [
499            Stability::High,
500            Stability::Medium,
501            Stability::Low,
502            Stability::Volatile,
503        ] {
504            let text = s.to_string();
505            assert_eq!(text.parse::<Stability>().unwrap(), s);
506        }
507    }
508
509    #[test]
510    fn test_confidence_from_str_valid_returns_ok() {
511        assert_eq!("high".parse::<Confidence>().unwrap(), Confidence::High);
512        assert_eq!("medium".parse::<Confidence>().unwrap(), Confidence::Medium);
513        assert_eq!("low".parse::<Confidence>().unwrap(), Confidence::Low);
514        assert_eq!(
515            "inferred".parse::<Confidence>().unwrap(),
516            Confidence::Inferred
517        );
518        assert_eq!(
519            "tentative".parse::<Confidence>().unwrap(),
520            Confidence::Tentative
521        );
522    }
523
524    #[test]
525    fn test_confidence_from_str_invalid_returns_error() {
526        let err = "maybe".parse::<Confidence>().unwrap_err();
527        assert_eq!(err.type_name, "Confidence");
528    }
529
530    #[test]
531    fn test_confidence_display_roundtrip() {
532        for c in [
533            Confidence::High,
534            Confidence::Medium,
535            Confidence::Low,
536            Confidence::Inferred,
537            Confidence::Tentative,
538        ] {
539            let text = c.to_string();
540            assert_eq!(text.parse::<Confidence>().unwrap(), c);
541        }
542    }
543
544    #[test]
545    fn test_node_status_from_str_valid_returns_ok() {
546        assert_eq!("active".parse::<NodeStatus>().unwrap(), NodeStatus::Active);
547        assert_eq!("draft".parse::<NodeStatus>().unwrap(), NodeStatus::Draft);
548        assert_eq!(
549            "deprecated".parse::<NodeStatus>().unwrap(),
550            NodeStatus::Deprecated
551        );
552        assert_eq!(
553            "superseded".parse::<NodeStatus>().unwrap(),
554            NodeStatus::Superseded
555        );
556    }
557
558    #[test]
559    fn test_node_status_from_str_invalid_returns_error() {
560        let err = "archived".parse::<NodeStatus>().unwrap_err();
561        assert_eq!(err.type_name, "NodeStatus");
562    }
563
564    #[test]
565    fn test_node_status_display_roundtrip() {
566        for ns in [
567            NodeStatus::Active,
568            NodeStatus::Draft,
569            NodeStatus::Deprecated,
570            NodeStatus::Superseded,
571        ] {
572            let text = ns.to_string();
573            assert_eq!(text.parse::<NodeStatus>().unwrap(), ns);
574        }
575    }
576
577    #[test]
578    fn test_field_value_scalar_debug() {
579        let v = FieldValue::Scalar("hello".to_owned());
580        assert!(format!("{v:?}").contains("Scalar"));
581    }
582
583    #[test]
584    fn test_field_value_list_clone() {
585        let v = FieldValue::List(vec!["a".to_owned(), "b".to_owned()]);
586        assert_eq!(v.clone(), v);
587    }
588
589    #[test]
590    fn test_field_value_serde_roundtrip_scalar() {
591        let v = FieldValue::Scalar("test".to_owned());
592        let json = serde_json::to_string(&v).unwrap();
593        let back: FieldValue = serde_json::from_str(&json).unwrap();
594        assert_eq!(v, back);
595    }
596
597    #[test]
598    fn test_field_value_serde_roundtrip_list() {
599        let v = FieldValue::List(vec!["a".to_owned(), "b".to_owned()]);
600        let json = serde_json::to_string(&v).unwrap();
601        let back: FieldValue = serde_json::from_str(&json).unwrap();
602        assert_eq!(v, back);
603    }
604
605    #[test]
606    fn test_span_new() {
607        let s = Span::new(1, 10);
608        assert_eq!(s.start_line, 1);
609        assert_eq!(s.end_line, 10);
610    }
611
612    #[test]
613    fn test_ticket_action_from_str_valid_returns_ok() {
614        assert_eq!(
615            "create".parse::<TicketAction>().unwrap(),
616            TicketAction::Create
617        );
618        assert_eq!("edit".parse::<TicketAction>().unwrap(), TicketAction::Edit);
619        assert_eq!(
620            "close".parse::<TicketAction>().unwrap(),
621            TicketAction::Close
622        );
623        assert_eq!(
624            "archive".parse::<TicketAction>().unwrap(),
625            TicketAction::Archive
626        );
627        assert_eq!(
628            "split".parse::<TicketAction>().unwrap(),
629            TicketAction::Split
630        );
631        assert_eq!("link".parse::<TicketAction>().unwrap(), TicketAction::Link);
632    }
633
634    #[test]
635    fn test_ticket_action_from_str_invalid_returns_error() {
636        let err = "delete".parse::<TicketAction>().unwrap_err();
637        assert_eq!(err.type_name, "TicketAction");
638        assert_eq!(err.value, "delete");
639    }
640
641    #[test]
642    fn test_ticket_action_display_roundtrip() {
643        for a in [
644            TicketAction::Create,
645            TicketAction::Edit,
646            TicketAction::Close,
647            TicketAction::Archive,
648            TicketAction::Split,
649            TicketAction::Link,
650        ] {
651            let s = a.to_string();
652            assert_eq!(s.parse::<TicketAction>().unwrap(), a);
653        }
654    }
655
656    #[test]
657    fn test_ticket_action_default_is_create() {
658        assert_eq!(TicketAction::default(), TicketAction::Create);
659    }
660
661    #[test]
662    fn test_sdd_phase_from_str_valid_returns_ok() {
663        assert_eq!("backlog".parse::<SddPhase>().unwrap(), SddPhase::Backlog);
664        assert_eq!("explore".parse::<SddPhase>().unwrap(), SddPhase::Explore);
665        assert_eq!("propose".parse::<SddPhase>().unwrap(), SddPhase::Propose);
666        assert_eq!("spec".parse::<SddPhase>().unwrap(), SddPhase::Spec);
667        assert_eq!("design".parse::<SddPhase>().unwrap(), SddPhase::Design);
668        assert_eq!("tasks".parse::<SddPhase>().unwrap(), SddPhase::Tasks);
669        assert_eq!("apply".parse::<SddPhase>().unwrap(), SddPhase::Apply);
670        assert_eq!("verify".parse::<SddPhase>().unwrap(), SddPhase::Verify);
671        assert_eq!("archive".parse::<SddPhase>().unwrap(), SddPhase::Archive);
672    }
673
674    #[test]
675    fn test_sdd_phase_from_str_invalid_returns_error() {
676        let err = "unknown".parse::<SddPhase>().unwrap_err();
677        assert_eq!(err.type_name, "SddPhase");
678        assert_eq!(err.value, "unknown");
679    }
680
681    #[test]
682    fn test_sdd_phase_display_roundtrip() {
683        for p in [
684            SddPhase::Backlog,
685            SddPhase::Explore,
686            SddPhase::Propose,
687            SddPhase::Spec,
688            SddPhase::Design,
689            SddPhase::Tasks,
690            SddPhase::Apply,
691            SddPhase::Verify,
692            SddPhase::Archive,
693        ] {
694            let s = p.to_string();
695            assert_eq!(s.parse::<SddPhase>().unwrap(), p);
696        }
697    }
698
699    #[test]
700    fn test_sdd_phase_default_is_backlog() {
701        assert_eq!(SddPhase::default(), SddPhase::Backlog);
702    }
703}