Skip to main content

clayers_spec/
assembly.rs

1use std::path::Path;
2
3use xot::Xot;
4
5use crate::namespace;
6
7/// Assemble a combined document from spec files by merging all children
8/// under a `<cmb:spec>` root element.
9///
10/// Each input file should be a `<spec:clayers>` document. Children of each
11/// file's root are moved under the combined root, preserving all namespaces.
12///
13/// # Errors
14///
15/// Returns an error if any file cannot be read or parsed as XML.
16pub fn assemble_combined(
17    file_paths: &[impl AsRef<Path>],
18) -> Result<(Xot, xot::Node), crate::Error> {
19    let mut xot = Xot::new();
20
21    // Create <cmb:spec> root element with all namespace declarations
22    let cmb_ns = xot.add_namespace(namespace::COMBINED);
23    let spec_name = xot.add_name_ns("spec", cmb_ns);
24    let root = xot.new_element(spec_name);
25
26    for (prefix, uri) in namespace::PREFIX_MAP {
27        let ns_id = xot.add_namespace(uri);
28        let prefix_id = xot.add_prefix(prefix);
29        xot.namespaces_mut(root).insert(prefix_id, ns_id);
30    }
31
32    // Parse each file and move its root element's children under <cmb:spec>
33    for file_path in file_paths {
34        let content = std::fs::read_to_string(file_path.as_ref())?;
35        let doc = xot.parse(&content).map_err(xot::Error::from)?;
36        let file_root = xot.document_element(doc)?;
37
38        // Collect children first to avoid iterator invalidation during moves
39        let children: Vec<_> = xot.children(file_root).collect();
40        for child in children {
41            xot.append(root, child)?;
42        }
43    }
44
45    Ok((xot, root))
46}
47
48/// Assemble combined document and return it as an XML string.
49///
50/// # Errors
51///
52/// Returns an error if any file cannot be read or parsed.
53pub fn assemble_combined_string(file_paths: &[impl AsRef<Path>]) -> Result<String, crate::Error> {
54    let (xot, root) = assemble_combined(file_paths)?;
55    Ok(xot.to_string(root).unwrap_or_default())
56}
57
58// Public API surface (used by ast-grep for structural verification).
59#[cfg(any())]
60mod _api {
61    use super::*;
62    pub fn assemble_combined(
63        file_paths: &[impl AsRef<Path>],
64    ) -> Result<(Xot, xot::Node), crate::Error>;
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use std::path::PathBuf;
71
72    fn spec_dir() -> PathBuf {
73        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
74            .join("../../clayers/clayers")
75            .canonicalize()
76            .expect("clayers/clayers/ not found")
77    }
78
79    fn spec_files() -> Vec<PathBuf> {
80        crate::discovery::discover_spec_files(&spec_dir().join("index.xml"))
81            .expect("discovery failed")
82    }
83
84    #[test]
85    fn assemble_shipped_spec_has_combined_root() {
86        let files = spec_files();
87        let (mut xot, root) = assemble_combined(&files).expect("assembly failed");
88
89        let cmb_ns = xot.add_namespace(namespace::COMBINED);
90        let spec_name = xot.add_name_ns("spec", cmb_ns);
91        assert!(xot.element(root).is_some_and(|e| e.name() == spec_name));
92    }
93
94    #[test]
95    fn combined_doc_has_elements_from_multiple_layers() {
96        let files = spec_files();
97        let (xot, root) = assemble_combined(&files).expect("assembly failed");
98        let xml = xot.to_string(root).unwrap_or_default();
99
100        // Should contain elements from at least prose, terminology, and relation layers
101        assert!(xml.contains("urn:clayers:prose"), "missing prose namespace");
102        assert!(
103            xml.contains("urn:clayers:terminology"),
104            "missing terminology namespace"
105        );
106        assert!(
107            xml.contains("urn:clayers:relation"),
108            "missing relation namespace"
109        );
110    }
111
112    #[test]
113    fn combined_doc_preserves_ids() {
114        let files = spec_files();
115        let (xot, root) = assemble_combined(&files).expect("assembly failed");
116        let xml = xot.to_string(root).unwrap_or_default();
117
118        // Known IDs from the self-referential spec
119        assert!(xml.contains("\"term-layer\""), "missing term-layer id");
120        assert!(
121            xml.contains("\"layered-architecture\""),
122            "missing layered-architecture id"
123        );
124    }
125
126    #[test]
127    fn assemble_single_file() {
128        let spec = spec_dir();
129        let overview = spec.join("overview.xml");
130        let binding = [&overview];
131        let (xot, root) = assemble_combined(&binding).expect("assembly failed");
132        let xml = xot.to_string(root).unwrap_or_default();
133        assert!(xml.contains("cmb:spec"), "missing combined root");
134    }
135}