Skip to main content

clayers_xml/
catalog.rs

1//! OASIS XML Catalog parsing (domain-agnostic).
2
3use std::path::Path;
4
5use xot::Xot;
6
7/// A single entry from an OASIS XML Catalog `<uri>` element.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct CatalogEntry {
10    /// The namespace URI (`name` attribute).
11    pub namespace: String,
12    /// The relative file path (`uri` attribute).
13    pub path: String,
14}
15
16/// Parse an OASIS XML Catalog file, returning namespace-to-path mappings.
17///
18/// Reads `<uri name="..." uri="..."/>` elements from the catalog.
19///
20/// # Errors
21///
22/// Returns an error if the file cannot be read or parsed as XML.
23pub fn parse_catalog(catalog_path: &Path) -> Result<Vec<CatalogEntry>, crate::Error> {
24    let content = std::fs::read_to_string(catalog_path)?;
25    let mut xot = Xot::new();
26    let doc = xot.parse(&content).map_err(xot::Error::from)?;
27    let root = xot.document_element(doc)?;
28
29    let catalog_ns = xot.add_namespace("urn:oasis:names:tc:entity:xmlns:xml:catalog");
30    let uri_el = xot.add_name_ns("uri", catalog_ns);
31    let name_attr = xot.add_name("name");
32    let uri_attr = xot.add_name("uri");
33
34    let mut entries = Vec::new();
35    for child in xot.children(root) {
36        if !xot.is_element(child) {
37            continue;
38        }
39        let Some(el) = xot.element(child) else {
40            continue;
41        };
42        if el.name() != uri_el {
43            continue;
44        }
45        let Some(name) = xot.get_attribute(child, name_attr) else {
46            continue;
47        };
48        let Some(uri) = xot.get_attribute(child, uri_attr) else {
49            continue;
50        };
51        entries.push(CatalogEntry {
52            namespace: name.to_string(),
53            path: uri.to_string(),
54        });
55    }
56
57    Ok(entries)
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn parse_catalog_extracts_entries() {
66        let dir = tempfile::tempdir().unwrap();
67        let catalog = r#"<?xml version="1.0" encoding="UTF-8"?>
68<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
69  <uri name="urn:test:foo" uri="foo.xsd"/>
70  <uri name="urn:test:bar" uri="bar.xsd"/>
71</catalog>"#;
72        let path = dir.path().join("catalog.xml");
73        std::fs::write(&path, catalog).unwrap();
74
75        let entries = parse_catalog(&path).unwrap();
76        assert_eq!(entries.len(), 2);
77        assert_eq!(entries[0].namespace, "urn:test:foo");
78        assert_eq!(entries[0].path, "foo.xsd");
79        assert_eq!(entries[1].namespace, "urn:test:bar");
80        assert_eq!(entries[1].path, "bar.xsd");
81    }
82
83    #[test]
84    fn parse_catalog_skips_non_uri_elements() {
85        let dir = tempfile::tempdir().unwrap();
86        let catalog = r#"<?xml version="1.0" encoding="UTF-8"?>
87<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
88  <!-- A comment -->
89  <uri name="urn:test:only" uri="only.xsd"/>
90</catalog>"#;
91        let path = dir.path().join("catalog.xml");
92        std::fs::write(&path, catalog).unwrap();
93
94        let entries = parse_catalog(&path).unwrap();
95        assert_eq!(entries.len(), 1);
96        assert_eq!(entries[0].namespace, "urn:test:only");
97    }
98
99    #[test]
100    fn parse_catalog_empty_catalog() {
101        let dir = tempfile::tempdir().unwrap();
102        let catalog = r#"<?xml version="1.0" encoding="UTF-8"?>
103<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
104</catalog>"#;
105        let path = dir.path().join("catalog.xml");
106        std::fs::write(&path, catalog).unwrap();
107
108        let entries = parse_catalog(&path).unwrap();
109        assert!(entries.is_empty());
110    }
111}