Skip to main content

stationxml_rs/
format.rs

1//! Format trait and auto-detection.
2//!
3//! The [`StationXmlFormat`] trait is implemented by each format backend
4//! (FDSN, SC3ML). [`detect_format`] inspects the root XML element to
5//! determine which format a document uses.
6
7use crate::error::Result;
8use crate::inventory::Inventory;
9
10/// Supported XML formats.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Format {
13    /// FDSN StationXML 1.2
14    Fdsn,
15    /// SeisComP SC3ML 0.13
16    Sc3ml,
17}
18
19/// Trait implemented by each format backend.
20///
21/// Each format (FDSN, SC3ML, etc.) provides read/write via this trait.
22/// The type parameter is a zero-sized marker struct (e.g. `Fdsn`, `Sc3ml`).
23pub trait StationXmlFormat {
24    /// Deserialize XML string into an [`Inventory`].
25    fn read_from_str(xml: &str) -> Result<Inventory>;
26
27    /// Deserialize XML bytes into an [`Inventory`].
28    fn read_from_bytes(bytes: &[u8]) -> Result<Inventory>;
29
30    /// Serialize an [`Inventory`] to an XML string.
31    fn write_to_string(inventory: &Inventory) -> Result<String>;
32}
33
34/// Detect the XML format by inspecting the root element name.
35///
36/// Uses quick-xml's event reader to skip over XML declarations, comments,
37/// and whitespace, then matches on the first start element:
38/// - `<FDSNStationXML ...>` → [`Format::Fdsn`]
39/// - `<seiscomp ...>` → [`Format::Sc3ml`]
40///
41/// Returns `None` if the root element is not recognized.
42pub fn detect_format(xml: &str) -> Option<Format> {
43    let mut reader = quick_xml::Reader::from_str(xml);
44    loop {
45        match reader.read_event() {
46            Ok(quick_xml::events::Event::Start(e)) => {
47                return match e.local_name().as_ref() {
48                    b"FDSNStationXML" => Some(Format::Fdsn),
49                    b"seiscomp" => Some(Format::Sc3ml),
50                    _ => None,
51                };
52            }
53            Ok(quick_xml::events::Event::Eof) => return None,
54            Err(_) => return None,
55            _ => continue, // skip Declaration, Comment, PI, etc.
56        }
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn detect_fdsn_with_declaration() {
66        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
67<FDSNStationXML xmlns="http://www.fdsn.org/xml/station/1" schemaVersion="1.2">
68  <Source>Test</Source>
69</FDSNStationXML>"#;
70        assert_eq!(detect_format(xml), Some(Format::Fdsn));
71    }
72
73    #[test]
74    fn detect_fdsn_without_declaration() {
75        let xml = r#"<FDSNStationXML schemaVersion="1.2"><Source>T</Source></FDSNStationXML>"#;
76        assert_eq!(detect_format(xml), Some(Format::Fdsn));
77    }
78
79    #[test]
80    fn detect_sc3ml() {
81        let xml = r#"<?xml version="1.0"?>
82<seiscomp xmlns="http://geofon.gfz-potsdam.de/ns/seiscomp3-schema/0.13" version="0.13">
83  <Inventory></Inventory>
84</seiscomp>"#;
85        assert_eq!(detect_format(xml), Some(Format::Sc3ml));
86    }
87
88    #[test]
89    fn detect_with_comments() {
90        let xml = r#"<?xml version="1.0"?>
91<!-- This is a comment -->
92<FDSNStationXML schemaVersion="1.2"><Source>T</Source></FDSNStationXML>"#;
93        assert_eq!(detect_format(xml), Some(Format::Fdsn));
94    }
95
96    #[test]
97    fn detect_unknown() {
98        let xml = r#"<html><body>not station metadata</body></html>"#;
99        assert_eq!(detect_format(xml), None);
100    }
101
102    #[test]
103    fn detect_empty() {
104        assert_eq!(detect_format(""), None);
105    }
106
107    #[test]
108    fn detect_invalid_xml() {
109        assert_eq!(detect_format("not xml at all"), None);
110    }
111
112    #[test]
113    fn format_enum_copy() {
114        let f = Format::Fdsn;
115        let f2 = f; // Copy
116        assert_eq!(f, f2);
117    }
118}