Skip to main content

agm_core/loader/
mode.rs

1//! Load mode definitions for the AGM loader.
2//!
3//! Defines [`LoadMode`] and [`FieldCategory`] used to classify which fields
4//! are included when producing a filtered view of an AGM file.
5
6use std::fmt;
7use std::str::FromStr;
8
9use serde::{Deserialize, Serialize};
10
11// ---------------------------------------------------------------------------
12// ParseLoadModeError
13// ---------------------------------------------------------------------------
14
15/// Error returned when parsing a string into a [`LoadMode`] fails.
16#[derive(Debug, Clone, PartialEq, thiserror::Error)]
17#[error("invalid load mode: {value:?}; expected one of: summary, operational, executable, full")]
18pub struct ParseLoadModeError {
19    pub value: String,
20}
21
22// ---------------------------------------------------------------------------
23// LoadMode
24// ---------------------------------------------------------------------------
25
26/// The four loading modes defined in the AGM specification.
27///
28/// Each mode is a superset of the previous: Summary ⊂ Operational ⊂
29/// Executable ⊂ Full.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum LoadMode {
33    /// Minimal view: id, type, summary, priority, stability, depends, tags.
34    Summary,
35    /// Adds operational fields: items, steps, fields, input, output.
36    Operational,
37    /// Adds executable fields: code, verify, agent_context, execution state.
38    Executable,
39    /// Everything, including explanatory and relational fields.
40    Full,
41}
42
43impl fmt::Display for LoadMode {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::Summary => write!(f, "summary"),
47            Self::Operational => write!(f, "operational"),
48            Self::Executable => write!(f, "executable"),
49            Self::Full => write!(f, "full"),
50        }
51    }
52}
53
54impl FromStr for LoadMode {
55    type Err = ParseLoadModeError;
56
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        match s {
59            "summary" => Ok(Self::Summary),
60            "operational" => Ok(Self::Operational),
61            "executable" => Ok(Self::Executable),
62            "full" => Ok(Self::Full),
63            _ => Err(ParseLoadModeError {
64                value: s.to_owned(),
65            }),
66        }
67    }
68}
69
70// ---------------------------------------------------------------------------
71// FieldCategory
72// ---------------------------------------------------------------------------
73
74/// The category a node field belongs to, determining which [`LoadMode`]
75/// first includes it.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub(crate) enum FieldCategory {
78    Summary,
79    Operational,
80    Executable,
81    Full,
82}
83
84impl FieldCategory {
85    /// Returns `true` if this category's fields are included in `mode`.
86    #[must_use]
87    pub(crate) fn included_in(self, mode: LoadMode) -> bool {
88        match mode {
89            LoadMode::Summary => self == FieldCategory::Summary,
90            LoadMode::Operational => {
91                matches!(self, FieldCategory::Summary | FieldCategory::Operational)
92            }
93            LoadMode::Executable => matches!(
94                self,
95                FieldCategory::Summary | FieldCategory::Operational | FieldCategory::Executable
96            ),
97            LoadMode::Full => true,
98        }
99    }
100}
101
102// ---------------------------------------------------------------------------
103// Tests
104// ---------------------------------------------------------------------------
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_load_mode_from_str_valid_returns_ok() {
112        assert_eq!("summary".parse::<LoadMode>().unwrap(), LoadMode::Summary);
113        assert_eq!(
114            "operational".parse::<LoadMode>().unwrap(),
115            LoadMode::Operational
116        );
117        assert_eq!(
118            "executable".parse::<LoadMode>().unwrap(),
119            LoadMode::Executable
120        );
121        assert_eq!("full".parse::<LoadMode>().unwrap(), LoadMode::Full);
122    }
123
124    #[test]
125    fn test_load_mode_from_str_invalid_returns_error() {
126        let err = "debug".parse::<LoadMode>().unwrap_err();
127        assert!(err.value == "debug");
128    }
129
130    #[test]
131    fn test_load_mode_display_roundtrip() {
132        for mode in [
133            LoadMode::Summary,
134            LoadMode::Operational,
135            LoadMode::Executable,
136            LoadMode::Full,
137        ] {
138            let s = mode.to_string();
139            let parsed: LoadMode = s.parse().unwrap();
140            assert_eq!(parsed, mode);
141        }
142    }
143
144    #[test]
145    fn test_field_category_summary_included_only_in_summary_and_above() {
146        assert!(FieldCategory::Summary.included_in(LoadMode::Summary));
147        assert!(FieldCategory::Summary.included_in(LoadMode::Operational));
148        assert!(FieldCategory::Summary.included_in(LoadMode::Executable));
149        assert!(FieldCategory::Summary.included_in(LoadMode::Full));
150    }
151
152    #[test]
153    fn test_field_category_operational_not_in_summary() {
154        assert!(!FieldCategory::Operational.included_in(LoadMode::Summary));
155        assert!(FieldCategory::Operational.included_in(LoadMode::Operational));
156        assert!(FieldCategory::Operational.included_in(LoadMode::Executable));
157        assert!(FieldCategory::Operational.included_in(LoadMode::Full));
158    }
159
160    #[test]
161    fn test_field_category_executable_not_in_summary_or_operational() {
162        assert!(!FieldCategory::Executable.included_in(LoadMode::Summary));
163        assert!(!FieldCategory::Executable.included_in(LoadMode::Operational));
164        assert!(FieldCategory::Executable.included_in(LoadMode::Executable));
165        assert!(FieldCategory::Executable.included_in(LoadMode::Full));
166    }
167
168    #[test]
169    fn test_field_category_full_only_in_full() {
170        assert!(!FieldCategory::Full.included_in(LoadMode::Summary));
171        assert!(!FieldCategory::Full.included_in(LoadMode::Operational));
172        assert!(!FieldCategory::Full.included_in(LoadMode::Executable));
173        assert!(FieldCategory::Full.included_in(LoadMode::Full));
174    }
175
176    #[test]
177    fn test_load_mode_serde_roundtrip() {
178        let m = LoadMode::Executable;
179        let json = serde_json::to_string(&m).unwrap();
180        assert_eq!(json, "\"executable\"");
181        let back: LoadMode = serde_json::from_str(&json).unwrap();
182        assert_eq!(m, back);
183    }
184
185    #[test]
186    fn test_parse_load_mode_error_message_contains_value() {
187        let err = "bad_mode".parse::<LoadMode>().unwrap_err();
188        let msg = err.to_string();
189        assert!(msg.contains("bad_mode"));
190    }
191}