Skip to main content

molframe_pdb/header/
records.rs

1use molframe_core::Structure;
2
3/// Stable extension key for ordered legacy PDB metadata records.
4pub const PDB_HEADERS_EXTENSION: &str = "molframe.pdb.headers.v1";
5
6/// One fixed-column metadata record exactly as deposited.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct PdbHeaderRecord {
9    name: Box<str>,
10    line: Box<str>,
11}
12
13impl PdbHeaderRecord {
14    pub(crate) fn new(name: &str, line: &str) -> Self {
15        Self {
16            name: name.into(),
17            line: line.into(),
18        }
19    }
20
21    /// Six-column record name, trimmed.
22    #[must_use]
23    pub fn name(&self) -> &str {
24        &self.name
25    }
26
27    /// Original fixed-column line without its line ending.
28    #[must_use]
29    pub fn line(&self) -> &str {
30        &self.line
31    }
32}
33
34/// Every metadata record in archive order.
35#[derive(Clone, Debug, Default, PartialEq, Eq)]
36pub struct PdbHeaders {
37    records: Vec<PdbHeaderRecord>,
38}
39
40impl PdbHeaders {
41    pub(crate) fn push(&mut self, name: &str, line: &str) {
42        self.records.push(PdbHeaderRecord::new(name, line));
43    }
44
45    /// Whether no metadata records were deposited.
46    #[must_use]
47    pub fn is_empty(&self) -> bool {
48        self.records.is_empty()
49    }
50
51    /// All metadata records in archive order.
52    #[must_use]
53    pub fn records(&self) -> &[PdbHeaderRecord] {
54        &self.records
55    }
56
57    /// Records of one type, retaining their archive order.
58    pub fn named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a PdbHeaderRecord> {
59        self.records
60            .iter()
61            .filter(move |record| record.name.as_ref() == name)
62    }
63
64    /// Classification from columns 11–50 of `HEADER`.
65    #[must_use]
66    pub fn classification(&self) -> Option<&str> {
67        self.header_field(10, 50)
68    }
69
70    /// Deposition date from columns 51–59 of `HEADER`.
71    #[must_use]
72    pub fn deposition_date(&self) -> Option<&str> {
73        self.header_field(50, 59)
74    }
75
76    fn header_field(&self, start: usize, end: usize) -> Option<&str> {
77        let header = self.named("HEADER").next()?.line();
78        let value = header.get(start..end)?.trim();
79        (!value.is_empty()).then_some(value)
80    }
81}
82
83/// Access to preserved legacy metadata.
84pub trait PdbHeadersExt {
85    /// Metadata attached by the legacy PDB reader.
86    fn pdb_headers(&self) -> Option<&PdbHeaders>;
87}
88
89impl PdbHeadersExt for Structure {
90    fn pdb_headers(&self) -> Option<&PdbHeaders> {
91        self.extensions().get(PDB_HEADERS_EXTENSION)
92    }
93}
94
95/// Whether a record belongs to a metadata section of PDB format 3.3.
96pub(crate) fn is_metadata_record(name: &str) -> bool {
97    matches!(
98        name,
99        "HEADER"
100            | "OBSLTE"
101            | "TITLE"
102            | "SPLIT"
103            | "CAVEAT"
104            | "COMPND"
105            | "SOURCE"
106            | "KEYWDS"
107            | "EXPDTA"
108            | "NUMMDL"
109            | "MDLTYP"
110            | "AUTHOR"
111            | "REVDAT"
112            | "SPRSDE"
113            | "JRNL"
114            | "REMARK"
115            | "DBREF"
116            | "DBREF1"
117            | "DBREF2"
118            | "SEQADV"
119            | "SEQRES"
120            | "MODRES"
121            | "HET"
122            | "HETNAM"
123            | "HETSYN"
124            | "FORMUL"
125            | "HELIX"
126            | "SHEET"
127            | "SSBOND"
128            | "LINK"
129            | "CISPEP"
130            | "SITE"
131            | "CRYST1"
132            | "ORIGX1"
133            | "ORIGX2"
134            | "ORIGX3"
135            | "SCALE1"
136            | "SCALE2"
137            | "SCALE3"
138            | "MTRIX1"
139            | "MTRIX2"
140            | "MTRIX3"
141    )
142}