Skip to main content

xml_arrow/
schema.rs

1use std::{
2    collections::{HashMap, HashSet},
3    sync::Arc,
4};
5
6use arrow_schema::{DataType, Field, Schema, SchemaRef};
7
8use crate::{
9    validation::{validate_fields, validate_namespaces},
10    Error, Result,
11};
12
13/// An Arrow field with a relative XML path.
14#[derive(Clone, Debug)]
15pub struct XmlField {
16    pub(crate) field: Field,
17    pub(crate) path: String,
18    pub(crate) children: Vec<XmlField>,
19}
20
21impl XmlField {
22    /// Maps a scalar column to a relative path, such as `.`, `@id`, or `author/name`.
23    pub fn new(
24        name: impl Into<String>,
25        path: impl Into<String>,
26        data_type: DataType,
27        nullable: bool,
28    ) -> Self {
29        Self {
30            field: Field::new(name, data_type, nullable),
31            path: path.into(),
32            children: Vec::new(),
33        }
34    }
35
36    /// Maps a struct with child paths relative to its element.
37    pub fn structure(
38        name: impl Into<String>,
39        path: impl Into<String>,
40        nullable: bool,
41        children: Vec<XmlField>,
42    ) -> Self {
43        let data_type =
44            DataType::Struct(children.iter().map(|child| child.field.clone()).collect());
45        Self {
46            field: Field::new(name, data_type, nullable),
47            path: path.into(),
48            children,
49        }
50    }
51
52    /// Maps a list container; the item path selects repeated values inside it.
53    /// Empty containers become `[]`; missing or nil nullable containers become null.
54    pub fn list(
55        name: impl Into<String>,
56        path: impl Into<String>,
57        nullable: bool,
58        item: XmlField,
59    ) -> Self {
60        Self {
61            field: Field::new(name, DataType::List(Arc::new(item.field.clone())), nullable),
62            path: path.into(),
63            children: vec![item],
64        }
65    }
66
67    /// Arrow field definition.
68    pub fn arrow_field(&self) -> &Field {
69        &self.field
70    }
71
72    /// XML path relative to its parent element.
73    pub fn path(&self) -> &str {
74        &self.path
75    }
76
77    /// Struct children, one list item mapping, or an empty slice for a scalar.
78    pub fn children(&self) -> &[XmlField] {
79        &self.children
80    }
81
82    pub(crate) fn error(&self, message: impl Into<String>) -> Error {
83        Error::Field {
84            name: self.field.name().clone(),
85            path: self.path.clone(),
86            message: message.into(),
87        }
88    }
89}
90
91/// A schema prefix bound to a namespace URI.
92#[derive(Clone, Copy, Debug)]
93pub struct NamespaceBinding<'a> {
94    /// Prefix used in schema and record paths.
95    pub prefix: &'a str,
96    /// XML namespace URI matched independently of input prefixes.
97    pub uri: &'a str,
98}
99
100/// Validated column mappings and namespace bindings.
101#[derive(Clone, Debug)]
102pub struct XmlSchema {
103    pub(crate) fields: Vec<XmlField>,
104    arrow: SchemaRef,
105    pub(crate) namespaces: HashMap<String, String>,
106}
107
108impl XmlSchema {
109    /// Validates column mappings. An empty schema counts rows.
110    pub fn new(fields: Vec<XmlField>) -> Result<Self> {
111        Self::with_namespaces(fields, &[])
112    }
113
114    /// Binds path prefixes to namespace URIs; input prefixes may differ.
115    /// `xml` is predefined; `xml` and `xmlns` cannot be rebound.
116    pub fn with_namespaces(
117        fields: Vec<XmlField>,
118        bindings: &[NamespaceBinding<'_>],
119    ) -> Result<Self> {
120        let namespaces = validate_namespaces(bindings)?;
121        validate_fields(&fields, &namespaces)?;
122        let arrow = Arc::new(Schema::new(
123            fields.iter().map(|f| f.field.clone()).collect::<Vec<_>>(),
124        ));
125        Ok(Self {
126            fields,
127            arrow,
128            namespaces,
129        })
130    }
131
132    /// Ordered column mappings.
133    pub fn fields(&self) -> &[XmlField] {
134        &self.fields
135    }
136
137    /// Arrow output schema.
138    pub fn arrow_schema(&self) -> SchemaRef {
139        Arc::clone(&self.arrow)
140    }
141
142    pub(crate) fn project(&self, projection: &[usize]) -> Result<Self> {
143        let mut seen = HashSet::new();
144        let mut fields = Vec::with_capacity(projection.len());
145        for &index in projection {
146            let field = self.fields.get(index).ok_or_else(|| {
147                Error::Configuration(format!("projection index {index} is out of bounds"))
148            })?;
149            if !seen.insert(index) {
150                return Err(Error::Configuration(format!(
151                    "duplicate projection index {index}"
152                )));
153            }
154            fields.push(field.clone());
155        }
156        let arrow = Arc::new(self.arrow.project(projection)?);
157        Ok(Self {
158            fields,
159            arrow,
160            namespaces: self.namespaces.clone(),
161        })
162    }
163}