molframe-cif 0.6.4

PDBx/mmCIF reading and writing for molframe
Documentation
use super::*;
use std::fmt::Write as _;
use std::sync::Arc;

fn document(text: &str) -> Document {
    let input = InputBuffer::from_bytes(text.as_bytes().to_vec());
    match parse(&input) {
        Ok((document, _)) => document,
        Err(findings) => panic!("parse failed: {findings:?}"),
    }
}

fn findings(text: &str) -> Vec<Code> {
    let input = InputBuffer::from_bytes(text.as_bytes().to_vec());
    let reported = match parse(&input) {
        Ok((_, findings)) | Err(findings) => findings,
    };
    reported.iter().map(Diagnostic::code).collect()
}

#[test]
fn a_tag_and_value_pair_becomes_one_row_of_one_category() {
    let document = document("data_x\n_entry.id 1ABC\n");
    let id = document
        .first_block()
        .and_then(|block| block.category("entry"))
        .and_then(|entry| entry.text("id", 0));
    assert_eq!(id, Some("1ABC"));
}

#[test]
fn a_loop_becomes_as_many_rows_as_it_has_value_groups() {
    let document =
        document("data_x\nloop_\n_atom_site.id\n_atom_site.type_symbol\n1 N\n2 C\n3 O\n");
    let category = document
        .first_block()
        .and_then(|block| block.category("atom_site"));
    assert_eq!(category.map(Category::row_count), Some(3));
    assert_eq!(category.and_then(|c| c.text("type_symbol", 2)), Some("O"));
    assert_eq!(
        category
            .and_then(|c| c.value("id", 1))
            .and_then(CifValue::as_integer),
        Some(2)
    );
}

#[test]
fn repeated_text_values_share_one_allocation() {
    let document = document("data_x\nloop_\n_a.name\nALA\nALA\n'ALA'\n");
    let Some(category) = document.first_block().and_then(|block| block.category("a")) else {
        panic!("expected a category")
    };
    let Some(CifValue::Text(first)) = category.value("name", 0) else {
        panic!("expected text")
    };
    let Some(CifValue::Text(second)) = category.value("name", 1) else {
        panic!("expected text")
    };
    let Some(CifValue::Text(quoted)) = category.value("name", 2) else {
        panic!("expected text")
    };
    assert!(Arc::ptr_eq(first, second));
    assert!(Arc::ptr_eq(first, quoted));
}

#[test]
fn an_item_name_splits_into_its_category_and_its_item() {
    assert_eq!(split_tag("_atom_site.id"), ("atom_site", "id"));
    assert_eq!(split_tag("atom_site.id"), ("atom_site", "id"));
    assert_eq!(split_tag("_legacy"), ("legacy", "legacy"));
}

#[test]
fn a_loop_row_that_is_short_is_reported_with_what_it_should_have_had() {
    let codes = findings("data_x\nloop_\n_a.one\n_a.two\n_a.three\n1 2 3\n4 5\n");
    assert!(codes.contains(&Code::E1103), "got {codes:?}");
}

#[test]
fn a_file_with_no_block_header_is_refused() {
    let codes = findings("_entry.id 1ABC\n");
    assert!(codes.contains(&Code::E1106), "got {codes:?}");
}

#[test]
fn several_blocks_are_all_kept() {
    let document = document("data_one\n_a.b 1\ndata_two\n_a.b 2\n");
    assert_eq!(document.len(), 2);
    let names: Vec<_> = document.blocks().map(DataBlock::name).collect();
    assert_eq!(names, ["one", "two"]);
}

#[test]
fn rows_walk_a_category_one_row_at_a_time() {
    let document = document("data_x\nloop_\n_a.id\n_a.name\n1 first\n2 second\n");
    let Some(category) = document.first_block().and_then(|b| b.category("a")) else {
        panic!("expected a category")
    };
    let mut rows = Rows::new(category);
    assert_eq!(rows.text("name"), Some("first"));
    assert_eq!(rows.integer("id"), Some(1));
    assert!(rows.advance());
    assert_eq!(rows.text("name"), Some("second"));
    assert!(!rows.advance());
}

#[test]
fn an_item_a_row_does_not_carry_reads_as_absent_rather_than_as_a_guess() {
    let document = document("data_x\n_a.present yes\n");
    let Some(category) = document.first_block().and_then(|b| b.category("a")) else {
        panic!("expected a category")
    };
    let rows = Rows::new(category);
    assert_eq!(rows.text("absent"), None);
    assert!(!rows.is_recorded("absent"));
}

#[derive(Clone, Debug, PartialEq)]
enum OwnedScalar {
    Inapplicable,
    Unknown,
    Text(String),
    Integer(i64),
    Float(f64),
}

#[derive(Default)]
struct EventProbe {
    values: Vec<(String, String, OwnedScalar)>,
    rows: usize,
}

impl CifEventSink for EventProbe {
    type Output = Self;

    fn block(&mut self, _name: &str) {}

    fn value(&mut self, category: &str, item: &str, value: CifScalar<'_>, _span: ByteSpan) {
        let owned = match value {
            CifScalar::Inapplicable => OwnedScalar::Inapplicable,
            CifScalar::Unknown => OwnedScalar::Unknown,
            CifScalar::Text(value) => OwnedScalar::Text(value.to_owned()),
            CifScalar::Integer(value) => OwnedScalar::Integer(value),
            CifScalar::Float(value) => OwnedScalar::Float(value),
        };
        self.values
            .push((category.to_owned(), item.to_owned(), owned));
    }

    fn end_row(&mut self) {
        self.rows += 1;
    }

    fn finish(self) -> Self::Output {
        self
    }
}

#[test]
fn event_projection_matches_scalar_loop_sentinel_and_multiline_semantics() {
    let source = "data_x\n_entry.id 'model one'\nloop_\n_a.id\n_a.score\n_a.note\n\
1 2.5 .\n2 ?\n;line one\nline two\n;\n";
    let input = InputBuffer::from_bytes(source.as_bytes().to_vec());
    let (_, document_findings) = parse(&input).expect("document oracle parses");
    let (events, event_findings) =
        parse_events(&input, EventProbe::default()).expect("event projection parses");
    assert_eq!(event_findings, document_findings);
    assert_eq!(events.rows, 3);
    assert_eq!(
        events.values,
        vec![
            (
                "entry".to_owned(),
                "id".to_owned(),
                OwnedScalar::Text("model one".to_owned())
            ),
            ("a".to_owned(), "id".to_owned(), OwnedScalar::Integer(1)),
            ("a".to_owned(), "score".to_owned(), OwnedScalar::Float(2.5)),
            ("a".to_owned(), "note".to_owned(), OwnedScalar::Inapplicable),
            ("a".to_owned(), "id".to_owned(), OwnedScalar::Integer(2)),
            ("a".to_owned(), "score".to_owned(), OwnedScalar::Unknown),
            (
                "a".to_owned(),
                "note".to_owned(),
                OwnedScalar::Text("line one\nline two".to_owned())
            ),
        ]
    );
}

#[test]
fn event_projection_and_document_report_the_same_malformed_loop() {
    let source = "data_x\nloop_\n_a.one\n_a.two\n1 2\n3\n";
    let input = InputBuffer::from_bytes(source.as_bytes().to_vec());
    let document = match parse(&input) {
        Ok((_, findings)) | Err(findings) => findings,
    };
    let events = match parse_events(&input, EventProbe::default()) {
        Ok((_, findings)) | Err(findings) => findings,
    };
    assert_eq!(
        events.iter().map(Diagnostic::code).collect::<Vec<_>>(),
        document.iter().map(Diagnostic::code).collect::<Vec<_>>()
    );
}

#[test]
fn event_projection_scales_without_materialising_cells() {
    let mut source = String::from("data_x\nloop_\n_a.id\n_a.value\n");
    for row in 0..10_000 {
        let _ = writeln!(source, "{row} repeated");
    }
    let input = InputBuffer::from_bytes(source.into_bytes());
    let (events, findings) =
        parse_events(&input, EventCounter::default()).expect("large event loop parses");
    assert!(findings.is_empty());
    assert_eq!(events.rows, 10_000);
    assert_eq!(events.values, 20_000);
}

#[derive(Default)]
struct EventCounter {
    values: usize,
    rows: usize,
}

impl CifEventSink for EventCounter {
    type Output = Self;

    fn block(&mut self, _name: &str) {}

    fn value(&mut self, _category: &str, _item: &str, _value: CifScalar<'_>, _span: ByteSpan) {
        self.values += 1;
    }

    fn end_row(&mut self) {
        self.rows += 1;
    }

    fn finish(self) -> Self::Output {
        self
    }
}