Skip to main content

molframe_pdb/header/
records.rs

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