Skip to main content

agm_core/loader/
mod.rs

1//! Loader: produces filtered views of an AGM AST based on loading modes.
2//!
3//! Supports summary, operational, executable, full, and profile-based
4//! loading as defined in the AGM specification.
5
6pub mod filter;
7pub mod mode;
8pub mod profile;
9
10pub use filter::filter_node;
11pub use mode::LoadMode;
12pub use profile::LoadError;
13
14use crate::model::file::AgmFile;
15
16// ---------------------------------------------------------------------------
17// load
18// ---------------------------------------------------------------------------
19
20/// Returns a filtered view of all nodes in `file` at the given `mode`.
21///
22/// Every node in the file is kept; only their fields are filtered.
23#[must_use]
24pub fn load(file: &AgmFile, mode: LoadMode) -> AgmFile {
25    let nodes = file.nodes.iter().map(|n| filter_node(n, mode)).collect();
26
27    AgmFile {
28        header: file.header.clone(),
29        nodes,
30    }
31}
32
33// ---------------------------------------------------------------------------
34// load_nodes
35// ---------------------------------------------------------------------------
36
37/// Returns a filtered view containing only the nodes whose IDs appear in
38/// `node_ids`, in the same order as they appear in `file.nodes`.
39///
40/// Unknown IDs are silently ignored.
41#[must_use]
42pub fn load_nodes(file: &AgmFile, mode: LoadMode, node_ids: &[&str]) -> AgmFile {
43    let id_set: std::collections::HashSet<&str> = node_ids.iter().copied().collect();
44
45    let nodes = file
46        .nodes
47        .iter()
48        .filter(|n| id_set.contains(n.id.as_str()))
49        .map(|n| filter_node(n, mode))
50        .collect();
51
52    AgmFile {
53        header: file.header.clone(),
54        nodes,
55    }
56}
57
58// ---------------------------------------------------------------------------
59// load_profile
60// ---------------------------------------------------------------------------
61
62/// Resolves and applies the named load profile (or the file's `default_load`).
63///
64/// See [`profile::resolve_and_apply`] for full semantics.
65pub fn load_profile(file: &AgmFile, profile_name: Option<&str>) -> Result<AgmFile, LoadError> {
66    profile::resolve_and_apply(file, profile_name)
67}
68
69// ---------------------------------------------------------------------------
70// Tests
71// ---------------------------------------------------------------------------
72
73#[cfg(test)]
74mod tests {
75    use std::collections::BTreeMap;
76
77    use crate::model::fields::{NodeType, Priority, Span};
78    use crate::model::file::{AgmFile, Header};
79    use crate::model::node::Node;
80
81    use super::*;
82
83    fn minimal_header() -> Header {
84        Header {
85            agm: "1.0".to_owned(),
86            package: "test.pkg".to_owned(),
87            version: "0.1.0".to_owned(),
88            title: None,
89            owner: None,
90            imports: None,
91            default_load: None,
92            description: None,
93            tags: None,
94            status: None,
95            load_profiles: None,
96            target_runtime: None,
97        }
98    }
99
100    fn make_node(id: &str) -> Node {
101        Node {
102            id: id.to_owned(),
103            node_type: NodeType::Facts,
104            summary: format!("node {id}"),
105            priority: Some(Priority::High),
106            stability: None,
107            confidence: None,
108            status: None,
109            depends: None,
110            related_to: None,
111            replaces: None,
112            conflicts: None,
113            see_also: None,
114            items: Some(vec!["item1".to_owned()]),
115            steps: None,
116            fields: None,
117            input: None,
118            output: None,
119            detail: Some("full detail".to_owned()),
120            rationale: None,
121            tradeoffs: None,
122            resolution: None,
123            examples: None,
124            notes: None,
125            code: None,
126            code_blocks: None,
127            verify: None,
128            agent_context: None,
129            target: None,
130            execution_status: None,
131            executed_by: None,
132            executed_at: None,
133            execution_log: None,
134            retry_count: None,
135            parallel_groups: None,
136            memory: None,
137            scope: None,
138            applies_when: None,
139            valid_from: None,
140            valid_until: None,
141            tags: None,
142            aliases: None,
143            keywords: None,
144            extra_fields: BTreeMap::new(),
145            span: Span::new(1, 5),
146        }
147    }
148
149    fn make_file(nodes: Vec<Node>) -> AgmFile {
150        AgmFile {
151            header: minimal_header(),
152            nodes,
153        }
154    }
155
156    #[test]
157    fn test_load_returns_all_nodes_filtered() {
158        let file = make_file(vec![make_node("a"), make_node("b")]);
159        let result = load(&file, LoadMode::Summary);
160        assert_eq!(result.nodes.len(), 2);
161        // `detail` is Full-only; must be absent in Summary mode.
162        assert!(result.nodes[0].detail.is_none());
163        assert!(result.nodes[1].detail.is_none());
164        // priority is Summary-always; must be present.
165        assert!(result.nodes[0].priority.is_some());
166    }
167
168    #[test]
169    fn test_load_nodes_returns_only_requested_ids() {
170        let file = make_file(vec![make_node("a"), make_node("b"), make_node("c")]);
171        let result = load_nodes(&file, LoadMode::Summary, &["a", "c"]);
172        assert_eq!(result.nodes.len(), 2);
173        let ids: Vec<&str> = result.nodes.iter().map(|n| n.id.as_str()).collect();
174        assert!(ids.contains(&"a"));
175        assert!(ids.contains(&"c"));
176        assert!(!ids.contains(&"b"));
177    }
178
179    #[test]
180    fn test_load_nodes_preserves_file_order() {
181        let file = make_file(vec![make_node("x"), make_node("y"), make_node("z")]);
182        let result = load_nodes(&file, LoadMode::Summary, &["z", "x"]);
183        // Must follow file order (x before z), not the order of the id list.
184        assert_eq!(result.nodes[0].id, "x");
185        assert_eq!(result.nodes[1].id, "z");
186    }
187
188    #[test]
189    fn test_load_nodes_ignores_unknown_ids() {
190        let file = make_file(vec![make_node("a")]);
191        let result = load_nodes(&file, LoadMode::Summary, &["a", "nonexistent"]);
192        assert_eq!(result.nodes.len(), 1);
193        assert_eq!(result.nodes[0].id, "a");
194    }
195}