Skip to main content

molframe_cif/pdbml/
codec.rs

1//! PDBML/XML conversion through the lossless mmCIF document model.
2
3use crate::document::{CifValue, DataBlock, Document};
4use crate::lexer::Quoting;
5use molframe_core::ByteSpan;
6use molframe_core::io::ReadOptions;
7use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
8use quick_xml::{Reader, Writer};
9
10/// Malformed or unsupported PDBML framing.
11#[derive(Debug, thiserror::Error)]
12#[non_exhaustive]
13pub enum PdbmlError {
14    /// XML tokenization or entity decoding failed.
15    #[error("invalid PDBML/XML: {0}")]
16    Xml(#[from] quick_xml::Error),
17    /// XML output could not be written.
18    #[error("could not write PDBML/XML: {0}")]
19    Io(#[from] std::io::Error),
20    /// The document has no PDBML datablock.
21    #[error("PDBML/XML has no datablock")]
22    MissingDatablock,
23    /// PDBML rows/categories are nested inconsistently.
24    #[error("invalid PDBML category or row nesting")]
25    InvalidNesting,
26    /// XML output was not UTF-8, which cannot represent PDBML.
27    #[error("PDBML writer produced invalid UTF-8")]
28    InvalidUtf8,
29    /// An XML entity reference was not predefined or numeric.
30    #[error("unsupported PDBML XML entity reference")]
31    InvalidEntity,
32}
33
34/// Failure either in XML conversion or existing mmCIF semantic lowering.
35#[derive(Debug)]
36#[non_exhaustive]
37pub enum PdbmlReadError {
38    /// PDBML syntax/framing failure.
39    Pdbml(PdbmlError),
40    /// Normal structure-lowering findings that prevented a structure.
41    Findings(Vec<molframe_core::Diagnostic>),
42}
43
44impl std::fmt::Display for PdbmlReadError {
45    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            Self::Pdbml(error) => error.fmt(formatter),
48            Self::Findings(findings) => {
49                write!(
50                    formatter,
51                    "PDBML semantic lowering failed with {} findings",
52                    findings.len()
53                )
54            }
55        }
56    }
57}
58impl std::error::Error for PdbmlReadError {}
59
60/// Parses PDBML/XML into the same lossless document used by mmCIF.
61/// Unknown categories and items are retained in source order.
62///
63/// # Errors
64///
65/// Returns an error for malformed XML, absent datablocks, or inconsistent
66/// category/row nesting.
67pub fn parse_pdbml_document(bytes: &[u8]) -> Result<Document, PdbmlError> {
68    let mut reader = Reader::from_reader(bytes);
69    reader.config_mut().trim_text(false);
70    let mut document = Document::new();
71    let mut block = None;
72    let mut category: Option<String> = None;
73    let mut row: Option<Row> = None;
74    let mut item: Option<Item> = None;
75    loop {
76        match reader.read_event()? {
77            Event::Start(element) => {
78                let name = local_name(element.name().as_ref()).to_owned();
79                if name == "datablock" {
80                    block = Some(DataBlock::new(datablock_name(&element, &reader)?));
81                } else if let Some(value) = name.strip_suffix("Category") {
82                    if category.is_some() || row.is_some() {
83                        return Err(PdbmlError::InvalidNesting);
84                    }
85                    category = Some(value.to_owned());
86                } else if category.as_deref() == Some(&name) {
87                    if row.is_some() {
88                        return Err(PdbmlError::InvalidNesting);
89                    }
90                    row = Some(Row::from_attributes(&element, &reader)?);
91                } else if row.is_some() {
92                    item = Some(Item {
93                        name,
94                        text: String::new(),
95                        nil: nil_attribute(&element, &reader)?,
96                    });
97                }
98            }
99            Event::Empty(element) if row.is_some() => {
100                let name = local_name(element.name().as_ref()).to_owned();
101                let value = if nil_attribute(&element, &reader)? {
102                    CifValue::Unknown
103                } else {
104                    CifValue::Text("".into())
105                };
106                if let Some(active) = &mut row {
107                    active.values.push((name, value));
108                }
109            }
110            Event::Text(text) => {
111                if let Some(active) = &mut item {
112                    active
113                        .text
114                        .push_str(&text.decode().map_err(|_| PdbmlError::InvalidUtf8)?);
115                }
116            }
117            Event::CData(text) => {
118                if let Some(active) = &mut item {
119                    active
120                        .text
121                        .push_str(&text.decode().map_err(|_| PdbmlError::InvalidUtf8)?);
122                }
123            }
124            Event::GeneralRef(reference) => {
125                if let Some(active) = &mut item {
126                    let reference = reference.decode().map_err(|_| PdbmlError::InvalidUtf8)?;
127                    active.text.push_str(&resolve_reference(&reference)?);
128                }
129            }
130            Event::End(element) => {
131                let name = local_name(element.name().as_ref()).to_owned();
132                if item.as_ref().is_some_and(|active| active.name == name) {
133                    let active = item.take().ok_or(PdbmlError::InvalidNesting)?;
134                    let value = if active.nil {
135                        CifValue::Unknown
136                    } else {
137                        CifValue::parse(active.text.trim(), Quoting::Bare)
138                    };
139                    row.as_mut()
140                        .ok_or(PdbmlError::InvalidNesting)?
141                        .values
142                        .push((active.name, value));
143                } else if category.as_deref() == Some(&name) {
144                    let active = row.take().ok_or(PdbmlError::InvalidNesting)?;
145                    let block = block.as_mut().ok_or(PdbmlError::MissingDatablock)?;
146                    append_row(block, &name, active);
147                } else if name.ends_with("Category") {
148                    category = None;
149                } else if name == "datablock" {
150                    document.push(block.take().ok_or(PdbmlError::MissingDatablock)?);
151                }
152            }
153            Event::Eof => break,
154            _ => {}
155        }
156    }
157    if document.is_empty() {
158        return Err(PdbmlError::MissingDatablock);
159    }
160    Ok(document)
161}
162
163/// Parses and semantically lowers PDBML through the existing mmCIF lowering.
164///
165/// # Errors
166///
167/// Returns the XML error or the full semantic finding list.
168pub fn read_pdbml(
169    bytes: &[u8],
170    options: &ReadOptions,
171) -> Result<
172    (
173        Document,
174        molframe_core::Structure,
175        Vec<molframe_core::Diagnostic>,
176    ),
177    PdbmlReadError,
178> {
179    let document = parse_pdbml_document(bytes).map_err(PdbmlReadError::Pdbml)?;
180    let (structure, findings) =
181        crate::lower(&document, options).map_err(PdbmlReadError::Findings)?;
182    options
183        .finish(structure, findings)
184        .map(|(structure, findings)| (document, structure, findings))
185        .map_err(PdbmlReadError::Findings)
186}
187
188/// Writes one lossless document as PDBML/XML.
189///
190/// Both CIF missing sentinels map to XML Schema `nil`, because PDBML has one
191/// missing representation rather than CIF's two distinct sentinels.
192///
193/// # Errors
194///
195/// Returns an error when the document has other than one datablock or XML
196/// generation fails.
197pub fn write_pdbml(document: &Document) -> Result<String, PdbmlError> {
198    if document.len() != 1 {
199        return Err(PdbmlError::MissingDatablock);
200    }
201    let block = document.first_block().ok_or(PdbmlError::MissingDatablock)?;
202    let mut writer = Writer::new_with_indent(Vec::new(), b' ', 3);
203    writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
204    let mut root = BytesStart::new("PDBx:datablock");
205    root.push_attribute(("datablockName", block.name()));
206    root.push_attribute(("xmlns:PDBx", "http://pdbml.pdb.org/schema/pdbx-v50.xsd"));
207    root.push_attribute(("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"));
208    writer.write_event(Event::Start(root))?;
209    for category in block.categories() {
210        let category_name = format!("PDBx:{}Category", category.name());
211        writer.write_event(Event::Start(BytesStart::new(&category_name)))?;
212        for row in 0..category.row_count() {
213            let row_name = format!("PDBx:{}", category.name());
214            writer.write_event(Event::Start(BytesStart::new(&row_name)))?;
215            for item_name in category.items() {
216                let value = match category.value(item_name, row) {
217                    Some(value) => value,
218                    None => &CifValue::Unknown,
219                };
220                write_item(&mut writer, item_name, value)?;
221            }
222            writer.write_event(Event::End(BytesEnd::new(&row_name)))?;
223        }
224        writer.write_event(Event::End(BytesEnd::new(&category_name)))?;
225    }
226    writer.write_event(Event::End(BytesEnd::new("PDBx:datablock")))?;
227    String::from_utf8(writer.into_inner()).map_err(|_| PdbmlError::InvalidUtf8)
228}
229
230struct Row {
231    values: Vec<(String, CifValue)>,
232}
233impl Row {
234    fn from_attributes(
235        element: &BytesStart<'_>,
236        reader: &Reader<&[u8]>,
237    ) -> Result<Self, PdbmlError> {
238        let mut values = Vec::new();
239        for attribute in element.attributes().with_checks(false) {
240            let attribute = attribute.map_err(quick_xml::Error::from)?;
241            let name = local_name(attribute.key.as_ref());
242            if name == "nil" || name == "schemaLocation" || name.starts_with("xmlns") {
243                continue;
244            }
245            let text = attribute.decoded_and_normalized_value(
246                quick_xml::XmlVersion::Implicit1_0,
247                reader.decoder(),
248            )?;
249            values.push((name.to_owned(), CifValue::parse(&text, Quoting::Bare)));
250        }
251        Ok(Self { values })
252    }
253}
254struct Item {
255    name: String,
256    text: String,
257    nil: bool,
258}
259
260fn datablock_name(element: &BytesStart<'_>, reader: &Reader<&[u8]>) -> Result<String, PdbmlError> {
261    let _ = reader;
262
263    for attribute in element.attributes().with_checks(false) {
264        let attribute = attribute.map_err(quick_xml::Error::from)?;
265
266        if local_name(attribute.key.as_ref()) == "datablockName" {
267            return Ok(attribute
268                .normalized_value(quick_xml::XmlVersion::Implicit1_0)?
269                .into_owned());
270        }
271    }
272
273    Err(PdbmlError::MissingDatablock)
274}
275
276fn nil_attribute(element: &BytesStart<'_>, reader: &Reader<&[u8]>) -> Result<bool, PdbmlError> {
277    let _ = reader;
278
279    for attribute in element.attributes().with_checks(false) {
280        let attribute = attribute.map_err(quick_xml::Error::from)?;
281
282        if local_name(attribute.key.as_ref()) == "nil" {
283            return Ok(attribute
284                .normalized_value(quick_xml::XmlVersion::Implicit1_0)?
285                .eq_ignore_ascii_case("true"));
286        }
287    }
288
289    Ok(false)
290}
291
292fn local_name(name: &[u8]) -> &str {
293    let local = name
294        .iter()
295        .rposition(|byte| *byte == b':')
296        .map_or(name, |position| &name[position + 1..]);
297    std::str::from_utf8(local).map_or("", |value| value)
298}
299fn resolve_reference(reference: &str) -> Result<String, PdbmlError> {
300    if let Some(value) = quick_xml::escape::resolve_predefined_entity(reference) {
301        return Ok(value.to_owned());
302    }
303    let number = if let Some(hexadecimal) = reference.strip_prefix("#x") {
304        u32::from_str_radix(hexadecimal, 16).ok()
305    } else {
306        reference
307            .strip_prefix('#')
308            .and_then(|value| value.parse().ok())
309    };
310    number
311        .and_then(char::from_u32)
312        .map(|value| value.to_string())
313        .ok_or(PdbmlError::InvalidEntity)
314}
315fn append_row(block: &mut DataBlock, category_name: &str, row: Row) {
316    let category = block.category_mut(category_name, ByteSpan::default());
317    let row_index = category.row_count();
318    let existing: Vec<_> = category.items().map(str::to_owned).collect();
319    for item in existing {
320        let value = row
321            .values
322            .iter()
323            .find(|(name, _)| name == &item)
324            .map_or(CifValue::Unknown, |(_, value)| value.clone());
325        category.column_mut(&item).push(value, Quoting::Bare);
326    }
327    for (item, value) in row.values {
328        if category.column(&item).is_some() {
329            continue;
330        }
331        let column = category.column_mut(&item);
332        for _ in 0..row_index {
333            column.push(CifValue::Unknown, Quoting::Bare);
334        }
335        column.push(value, Quoting::Bare);
336    }
337}
338fn write_item(
339    writer: &mut Writer<Vec<u8>>,
340    item_name: &str,
341    value: &CifValue,
342) -> Result<(), PdbmlError> {
343    let name = format!("PDBx:{item_name}");
344    match value {
345        CifValue::Unknown | CifValue::Inapplicable => {
346            let mut element = BytesStart::new(&name);
347            element.push_attribute(("xsi:nil", "true"));
348            writer.write_event(Event::Empty(element))?;
349        }
350        CifValue::Text(text) => write_text_element(writer, &name, text)?,
351        CifValue::Integer(value) => write_text_element(writer, &name, &value.to_string())?,
352        CifValue::Float(value) => write_text_element(writer, &name, &value.to_string())?,
353    }
354    Ok(())
355}
356fn write_text_element(
357    writer: &mut Writer<Vec<u8>>,
358    name: &str,
359    text: &str,
360) -> Result<(), PdbmlError> {
361    writer.write_event(Event::Start(BytesStart::new(name)))?;
362    writer.write_event(Event::Text(BytesText::new(text)))?;
363    writer.write_event(Event::End(BytesEnd::new(name)))?;
364    Ok(())
365}
366
367#[cfg(test)]
368#[path = "codec_tests.rs"]
369mod tests;