molframe-cif 0.3.0

PDBx/mmCIF reading and writing for molframe
Documentation
use super::*;
use molframe_core::io::{InputBuffer, ReadOptions};
use std::io::{self, Write};

const SOURCE: &str = "data_source\n\
loop_\n_atom_site.group_PDB\n_atom_site.id\n_atom_site.type_symbol\n\
_atom_site.label_atom_id\n_atom_site.label_comp_id\n_atom_site.label_asym_id\n\
_atom_site.label_seq_id\n_atom_site.Cartn_x\n_atom_site.Cartn_y\n_atom_site.Cartn_z\n\
_atom_site.pdbx_PDB_model_num\nATOM 44 C CA GLY LONG_CHAIN 9 1 2 3 7\n\
ATOM 45 N N GLY LONG_CHAIN 9 2 2 3 7\n";

#[test]
fn absent_block_identity_refuses_without_an_output_value() {
    let structure = molframe_core::Structure::new(molframe_core::StructureData::empty());
    assert_eq!(
        write_canonical(&structure),
        Err(CifWriteError::MissingBlockId)
    );
}

#[test]
fn explicit_block_identity_preserves_atom_model_and_chain_identifiers() {
    let structure = read(SOURCE);
    let options = CifWriteOptions::new().with_block_id("chosen");
    let Ok(output) = write_canonical_with_options(&structure, &options) else {
        panic!("complete structure should write");
    };
    assert!(output.starts_with("data_chosen\n"), "{output}");
    assert!(output.contains("ATOM 44 C CA . GLY LONG_CHAIN . 9 ? 1.000 2.000 3.000"));
    assert!(output.ends_with(" 7\n#\n"), "{output}");
}

#[test]
fn absent_atom_site_identity_is_never_replaced_by_an_ordinal() {
    let structure = read(&SOURCE.replace("ATOM 44", "ATOM ?"));
    let options = CifWriteOptions::new().with_block_id("chosen");
    assert_eq!(
        write_canonical_with_options(&structure, &options),
        Err(CifWriteError::MissingAtomSiteId { atom: 0 })
    );
}

#[test]
fn bond_identifiers_require_explicit_generation_permission() {
    let source = format!(
        "{SOURCE}loop_\n_struct_conn.id\n_struct_conn.conn_type_id\n\
         _struct_conn.ptnr1_label_asym_id\n_struct_conn.ptnr1_label_seq_id\n\
         _struct_conn.ptnr1_label_comp_id\n_struct_conn.ptnr1_label_atom_id\n\
         _struct_conn.ptnr2_label_asym_id\n_struct_conn.ptnr2_label_seq_id\n\
         _struct_conn.ptnr2_label_comp_id\n_struct_conn.ptnr2_label_atom_id\n\
         c1 covale LONG_CHAIN 9 GLY CA LONG_CHAIN 9 GLY N\n"
    );
    let structure = read(&source);
    let options = CifWriteOptions::new().with_block_id("chosen");
    assert_eq!(
        write_canonical_with_options(&structure, &options),
        Err(CifWriteError::ConnectionIdsNotEnabled)
    );
}

#[derive(Default)]
struct CountingWriter {
    bytes: usize,
    writes: usize,
    largest_write: usize,
}

impl Write for CountingWriter {
    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
        self.bytes += bytes.len();
        self.writes += 1;
        self.largest_write = self.largest_write.max(bytes.len());
        Ok(bytes.len())
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

#[test]
fn canonical_writer_emits_bounded_fragments_without_a_full_file() {
    let structure = read(SOURCE);
    let options = CifWriteOptions::new().with_block_id("chosen");
    let expected = match write_canonical_with_options(&structure, &options) {
        Ok(output) => output,
        Err(error) => panic!("in-memory write failed: {error}"),
    };
    let mut writer = CountingWriter::default();
    if let Err(error) = write_canonical_to(&structure, &options, &mut writer) {
        panic!("stream write failed: {error}");
    }
    assert_eq!(writer.bytes, expected.len());
    assert!(writer.writes > 1);
    assert!(writer.largest_write < expected.len());
}

#[test]
fn canonical_preflight_refuses_before_touching_the_destination() {
    let structure = molframe_core::Structure::new(molframe_core::StructureData::empty());
    let mut writer = CountingWriter::default();
    assert!(
        matches!(
            write_canonical_to(&structure, &CifWriteOptions::new(), &mut writer),
            Err(CifWriteToError::Projection(CifWriteError::MissingBlockId))
        ),
        "missing identity must remain a projection error"
    );
    assert_eq!(writer.bytes, 0);
    assert_eq!(writer.writes, 0);
}

fn read(source: &str) -> molframe_core::Structure {
    let input = InputBuffer::from_bytes(source.as_bytes().to_vec());
    match crate::read(&input, &ReadOptions::new()) {
        Ok((structure, _)) => structure,
        Err(findings) => panic!("fixture failed: {findings:?}"),
    }
}