Skip to main content

ergo_sbe/xml/
entry.rs

1//! Public parse entry points.
2
3use std::collections::HashSet;
4use std::path::{Path, PathBuf};
5
6use roxmltree::{Document, Node};
7
8use crate::ir::Ir;
9
10use super::error::{Fault, ParseError, named_source};
11use super::registry::TypeRegistry;
12use super::schema::{IncludeWalk, parse_schema};
13use super::warn::WarnState;
14
15/// Parse an SBE schema from a string. Returns a token [`Ir`] ready for
16/// [`crate::Schema::from_ir`].
17///
18/// # Errors
19/// [`ParseError`] with source spans when the XML is malformed or the schema
20/// is structurally invalid.
21pub fn parse(xml: &str) -> Result<Ir, ParseError> {
22    let warn_state = WarnState::new("<xml>".into());
23    parse_with_context(
24        xml,
25        None,
26        &mut IncludeWalk::new(),
27        TypeRegistry::new(),
28        &warn_state,
29        &mut Vec::new(),
30    )
31}
32
33/// [`parse`], resolving type references against an already-parsed shared
34/// schema's composites/enums/sets first — so `xml` need not `<include>` or
35/// redeclare them.
36///
37/// Only composites, enums, and sets round-trip through `shared`'s [`Ir`];
38/// bare top-level `<type>` typedefs are inlined and dropped during parsing,
39/// so reference those via a `<composite>`/`<enum>`/`<set>` in the shared
40/// schema instead.
41///
42/// # Errors
43///
44/// Same as [`parse`].
45#[allow(clippy::result_large_err)]
46pub fn parse_with_shared(xml: &str, shared: &Ir) -> Result<Ir, ParseError> {
47    let warn_state = WarnState::new("<xml>".into());
48    parse_with_context(
49        xml,
50        None,
51        &mut IncludeWalk::new(),
52        TypeRegistry::from_parsed_schema(shared),
53        &warn_state,
54        &mut Vec::new(),
55    )
56}
57
58/// [`parse`] after [`crate::validate_against_sbe_xsd`].
59///
60/// Use in CI for schema authors. Still not a full W3C XSD engine — see
61/// [`crate::xsd`]. [`parse`] alone already rejects malformed XML, a bad
62/// root, unexpected elements, and unknown attributes; this adds the XSD's
63/// wider element/attribute shape check on top.
64///
65/// # Errors
66///
67/// XSD structural failures or any [`parse`] error.
68#[allow(clippy::result_large_err)]
69pub fn parse_with_xsd_validation(xml: &str) -> Result<Ir, ParseError> {
70    if let Err(e) = crate::xsd::validate_against_sbe_xsd(xml) {
71        return Err(match &e {
72            crate::xsd::XsdValidationError::MalformedXml(_) => {
73                ParseError::malformed_xml("<xml>", e.to_string(), xml)
74            }
75            _ => ParseError::Invalid {
76                what: "SBE schema".into(),
77                value: e.to_string(),
78                source_code: named_source("<xml>", xml),
79                span: None,
80            },
81        });
82    }
83    parse(xml)
84}
85
86/// Schema parse result plus every file that contributed to it.
87///
88/// `parse_file` keeps returning [`Ir`] only. Build helpers use this so Cargo
89/// watches the root and every resolved include.
90pub(crate) struct ParsedFile {
91    pub ir: Ir,
92    /// Root first, then remaining resolved includes, each path once, sorted.
93    pub dependencies: Vec<PathBuf>,
94}
95
96/// Parse a schema file; resolve `xi:include` relative to the file's directory.
97///
98/// # Errors
99///
100/// I/O, XML, or schema validation failures as [`ParseError`].
101#[allow(clippy::result_large_err)]
102pub fn parse_file(path: impl AsRef<Path>) -> Result<Ir, ParseError> {
103    Ok(parse_file_with_deps(path)?.ir)
104}
105
106/// [`parse_file`], also returning the canonical root and every resolved include.
107#[allow(clippy::result_large_err)]
108pub(crate) fn parse_file_with_deps(path: impl AsRef<Path>) -> Result<ParsedFile, ParseError> {
109    parse_path_with_registry(path.as_ref(), TypeRegistry::new())
110}
111
112/// [`parse_file`], resolving type references against an already-parsed
113/// shared schema first — see [`parse_with_shared`].
114///
115/// # Errors
116///
117/// Same as [`parse_file`].
118#[allow(clippy::result_large_err)]
119pub fn parse_file_with_shared(path: impl AsRef<Path>, shared: &Ir) -> Result<Ir, ParseError> {
120    Ok(parse_file_with_shared_deps(path, shared)?.ir)
121}
122
123/// [`parse_file_with_shared`], also returning watched schema files.
124#[allow(clippy::result_large_err)]
125pub(crate) fn parse_file_with_shared_deps(
126    path: impl AsRef<Path>,
127    shared: &Ir,
128) -> Result<ParsedFile, ParseError> {
129    parse_path_with_registry(path.as_ref(), TypeRegistry::from_parsed_schema(shared))
130}
131
132#[allow(clippy::result_large_err)]
133fn parse_path_with_registry(
134    path: &Path,
135    initial_registry: TypeRegistry,
136) -> Result<ParsedFile, ParseError> {
137    let name = path.display().to_string();
138    let warn_state = WarnState::new(name.clone());
139    let xml = std::fs::read_to_string(path).map_err(|e| ParseError::io(path, e))?;
140    let base_dir = path.parent();
141    let mut dependencies = Vec::new();
142    // Seed the include stack with the main file so an include targeting it
143    // is a cycle (self-include or mutual A→B→A).
144    let root = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
145    dependencies.push(root.clone());
146    let mut walk = IncludeWalk::with_root(root);
147    let ir = parse_with_context(
148        &xml,
149        base_dir,
150        &mut walk,
151        initial_registry,
152        &warn_state,
153        &mut dependencies,
154    )?;
155    Ok(ParsedFile {
156        ir,
157        dependencies: stabilize_dependencies(path, dependencies),
158    })
159}
160
161/// Root first (canonical when possible), then remaining unique paths in
162/// sorted order so Cargo directives are stable across runs.
163fn stabilize_dependencies(root: &Path, dependencies: Vec<PathBuf>) -> Vec<PathBuf> {
164    let root_key = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
165    let mut seen = HashSet::new();
166    let mut out = Vec::new();
167    seen.insert(root_key.clone());
168    out.push(root_key);
169    let mut rest = dependencies;
170    rest.sort();
171    for path in rest {
172        let key = path.canonicalize().unwrap_or(path);
173        if seen.insert(key.clone()) {
174            out.push(key);
175        }
176    }
177    out
178}
179
180/// Internal: parse with optional base directory for include resolution and
181/// an initial type registry (seeded from a shared schema, or empty).
182pub(crate) fn parse_with_context(
183    xml: &str,
184    base_dir: Option<&Path>,
185    walk: &mut IncludeWalk,
186    initial_registry: TypeRegistry,
187    warn_state: &WarnState,
188    dependencies: &mut Vec<PathBuf>,
189) -> Result<Ir, ParseError> {
190    let doc = match Document::parse(xml) {
191        Ok(d) => d,
192        Err(e) => {
193            return Err(ParseError::malformed_xml(
194                &warn_state.name,
195                e.to_string(),
196                xml,
197            ));
198        }
199    };
200    let input = doc.input_text();
201    let root = doc
202        .root()
203        .children()
204        .find(Node::is_element)
205        .ok_or_else(|| Fault::missing_no_node("root <messageSchema> element"));
206    let root = match root {
207        Ok(n) => n,
208        Err(fault) => return Err(ParseError::from_fault(&warn_state.name, fault, input)),
209    };
210    if root.tag_name().name() != "messageSchema" {
211        return Err(ParseError::from_fault(
212            &warn_state.name,
213            Fault::missing(root, "root <messageSchema> element"),
214            input,
215        ));
216    }
217    let mut ir = parse_schema(
218        root,
219        base_dir,
220        walk,
221        initial_registry,
222        warn_state,
223        dependencies,
224    )
225    .map_err(|fault| ParseError::from_fault(&warn_state.name, fault, input))?;
226    crate::resolve::resolve_schema(&mut ir, Some(input))?;
227    Ok(ir)
228}