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 crate::model::fields::{NodeType, Priority, Span};
76    use crate::model::file::{AgmFile, Header};
77    use crate::model::node::Node;
78
79    use super::*;
80
81    fn minimal_header() -> Header {
82        Header {
83            agm: "1.0".to_owned(),
84            package: "test.pkg".to_owned(),
85            version: "0.1.0".to_owned(),
86            title: None,
87            owner: None,
88            imports: None,
89            default_load: None,
90            description: None,
91            tags: None,
92            status: None,
93            load_profiles: None,
94            target_runtime: None,
95        }
96    }
97
98    fn make_node(id: &str) -> Node {
99        Node {
100            id: id.to_owned(),
101            node_type: NodeType::Facts,
102            summary: format!("node {id}"),
103            priority: Some(Priority::High),
104            items: Some(vec!["item1".to_owned()]),
105            detail: Some("full detail".to_owned()),
106            span: Span::new(1, 5),
107            ..Default::default()
108        }
109    }
110
111    fn make_file(nodes: Vec<Node>) -> AgmFile {
112        AgmFile {
113            header: minimal_header(),
114            nodes,
115        }
116    }
117
118    #[test]
119    fn test_load_returns_all_nodes_filtered() {
120        let file = make_file(vec![make_node("a"), make_node("b")]);
121        let result = load(&file, LoadMode::Summary);
122        assert_eq!(result.nodes.len(), 2);
123        // `detail` is Full-only; must be absent in Summary mode.
124        assert!(result.nodes[0].detail.is_none());
125        assert!(result.nodes[1].detail.is_none());
126        // priority is Summary-always; must be present.
127        assert!(result.nodes[0].priority.is_some());
128    }
129
130    #[test]
131    fn test_load_nodes_returns_only_requested_ids() {
132        let file = make_file(vec![make_node("a"), make_node("b"), make_node("c")]);
133        let result = load_nodes(&file, LoadMode::Summary, &["a", "c"]);
134        assert_eq!(result.nodes.len(), 2);
135        let ids: Vec<&str> = result.nodes.iter().map(|n| n.id.as_str()).collect();
136        assert!(ids.contains(&"a"));
137        assert!(ids.contains(&"c"));
138        assert!(!ids.contains(&"b"));
139    }
140
141    #[test]
142    fn test_load_nodes_preserves_file_order() {
143        let file = make_file(vec![make_node("x"), make_node("y"), make_node("z")]);
144        let result = load_nodes(&file, LoadMode::Summary, &["z", "x"]);
145        // Must follow file order (x before z), not the order of the id list.
146        assert_eq!(result.nodes[0].id, "x");
147        assert_eq!(result.nodes[1].id, "z");
148    }
149
150    #[test]
151    fn test_load_nodes_ignores_unknown_ids() {
152        let file = make_file(vec![make_node("a")]);
153        let result = load_nodes(&file, LoadMode::Summary, &["a", "nonexistent"]);
154        assert_eq!(result.nodes.len(), 1);
155        assert_eq!(result.nodes[0].id, "a");
156    }
157}