Skip to main content

molframe_core/io/format/
options.rs

1//! Shared parsing options and reader/writer traits.
2
3use super::super::input::{InputBuffer, Limits};
4use super::detect::Format;
5use crate::diagnostic::Diagnostic;
6use crate::diagnostic::{Diagnostics, Strictness};
7use crate::structure::Structure;
8
9/// How much irregularity a read tolerates, and what it does about it.
10#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
11#[non_exhaustive]
12pub enum ParseMode {
13    /// Anything violating the specification fails the read.
14    Strict,
15    /// Produce a structure and report what was wrong with it.
16    #[default]
17    Permissive,
18    /// Continue past local errors, marking the regions they affected.
19    Recover,
20}
21
22impl ParseMode {
23    /// The strictness this mode implies.
24    #[must_use]
25    pub const fn strictness(self) -> Strictness {
26        match self {
27            Self::Strict => Strictness::Strict,
28            Self::Permissive => Strictness::Medium,
29            Self::Recover => Strictness::Loose,
30        }
31    }
32}
33
34/// Whether a read records a digest of the bytes it parsed.
35#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
36pub enum InputDigest {
37    /// Record nothing: hashing is work on every read, and the read path is measured on
38    /// throughput.
39    #[default]
40    Skip,
41    /// Record the SHA-256 of the bytes parsed, after decompression.
42    Sha256,
43}
44
45/// Policy for atom rows whose format-specific element field is absent or invalid.
46#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
47pub enum MissingElementPolicy {
48    /// Preserve the missing value as [`Element::UNKNOWN`](crate::Element::UNKNOWN).
49    #[default]
50    PreserveUnknown,
51    /// Explicitly apply the naming convention of the selected input format.
52    InferFromAtomName,
53}
54
55/// Policy for adjacent mmCIF atom groups whose residue identifiers are identical.
56#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
57pub enum AmbiguousResidueBoundaryPolicy {
58    /// Refuse to invent a residue boundary that the deposited identifiers do not define.
59    #[default]
60    Reject,
61    /// Explicitly split when an atom name repeats in the same alternate location.
62    InferFromFileOrder,
63}
64
65/// mmCIF/BinaryCIF categories every structure read needs, whatever the filter.
66pub const REQUIRED_CIF_CATEGORIES: [&str; 5] = [
67    "atom_site",
68    "entity",
69    "entity_poly",
70    "entity_poly_seq",
71    "struct_asym",
72];
73
74/// PDB records every structure read needs, whatever the filter.
75pub const REQUIRED_PDB_RECORDS: [&str; 6] = ["ATOM", "HETATM", "MODEL", "ENDMDL", "TER", "END"];
76
77/// Which optional categories (mmCIF/BinaryCIF) or records (PDB) a read keeps.
78///
79/// Names are compared ASCII case-insensitively. Required names (see
80/// [`REQUIRED_CIF_CATEGORIES`], [`REQUIRED_PDB_RECORDS`]) are kept by
81/// [`CategoryFilter::keeps_category`] and [`CategoryFilter::keeps_record`]
82/// regardless of the filter.
83#[derive(Clone, Debug, Default, PartialEq, Eq)]
84pub enum CategoryFilter {
85    /// Keep everything.
86    #[default]
87    All,
88    /// Keep only the named names, plus the required ones.
89    Only(Vec<Box<str>>),
90    /// Keep everything except the named names, unless required.
91    Except(Vec<Box<str>>),
92}
93
94impl CategoryFilter {
95    /// Keeps only `names` (and the required names).
96    #[must_use]
97    pub fn only<I, S>(names: I) -> Self
98    where
99        I: IntoIterator<Item = S>,
100        S: Into<Box<str>>,
101    {
102        Self::Only(names.into_iter().map(Into::into).collect())
103    }
104
105    /// Drops `names`, except those that are required.
106    #[must_use]
107    pub fn except<I, S>(names: I) -> Self
108    where
109        I: IntoIterator<Item = S>,
110        S: Into<Box<str>>,
111    {
112        Self::Except(names.into_iter().map(Into::into).collect())
113    }
114
115    /// Whether the filter itself admits `name`, ignoring what is required.
116    #[must_use]
117    pub fn admits(&self, name: &str) -> bool {
118        let listed = |names: &[Box<str>]| names.iter().any(|item| item.eq_ignore_ascii_case(name));
119        match self {
120            Self::All => true,
121            Self::Only(names) => listed(names),
122            Self::Except(names) => !listed(names),
123        }
124    }
125
126    /// Whether a CIF category must be read: required, or admitted.
127    #[must_use]
128    pub fn keeps_category(&self, name: &str) -> bool {
129        self.admits(name)
130            || REQUIRED_CIF_CATEGORIES
131                .iter()
132                .any(|required| required.eq_ignore_ascii_case(name))
133    }
134
135    /// Whether a PDB record must be read: required, or admitted.
136    #[must_use]
137    pub fn keeps_record(&self, name: &str) -> bool {
138        self.admits(name)
139            || REQUIRED_PDB_RECORDS
140                .iter()
141                .any(|required| required.eq_ignore_ascii_case(name))
142    }
143}
144
145/// What a read should and should not bother doing.
146#[derive(Clone, Debug, Default)]
147pub struct ReadOptions {
148    /// Which format, or automatic detection.
149    pub format: Format,
150    /// How much irregularity to tolerate.
151    pub mode: ParseMode,
152    /// Read only the first model.
153    pub only_first_model: bool,
154    /// Read coordinates and skip everything else.
155    pub only_atomic_coords: bool,
156    /// Drop hydrogens while reading.
157    pub discard_hydrogens: bool,
158    /// What to do when a row does not declare a valid element.
159    pub missing_element_policy: MissingElementPolicy,
160    /// What to do when mmCIF residue identifiers do not define a boundary.
161    pub ambiguous_residue_boundary_policy: AmbiguousResidueBoundaryPolicy,
162    /// Ceilings that apply while reading.
163    pub limits: Limits,
164    /// Which optional categories or records to read.
165    pub categories: CategoryFilter,
166    /// Whether to record the SHA-256 of the bytes parsed in the structure's entry
167    /// metadata; only a result certified as computed from exactly these bytes needs it.
168    pub input_digest: InputDigest,
169}
170
171impl ReadOptions {
172    /// Options that read everything, tolerating what the archive contains.
173    #[must_use]
174    pub fn new() -> Self {
175        Self::default()
176    }
177
178    /// Sets the format.
179    #[must_use]
180    pub const fn format(mut self, format: Format) -> Self {
181        self.format = format;
182        self
183    }
184
185    /// Sets how much irregularity to tolerate.
186    #[must_use]
187    pub const fn mode(mut self, mode: ParseMode) -> Self {
188        self.mode = mode;
189        self
190    }
191
192    /// Reads only the first model.
193    #[must_use]
194    pub const fn only_first_model(mut self, only: bool) -> Self {
195        self.only_first_model = only;
196        self
197    }
198
199    /// Reads coordinates and skips everything else.
200    #[must_use]
201    pub const fn only_atomic_coords(mut self, only: bool) -> Self {
202        self.only_atomic_coords = only;
203        self
204    }
205
206    /// Selects an explicit policy for missing or invalid element fields.
207    #[must_use]
208    pub const fn missing_element_policy(mut self, policy: MissingElementPolicy) -> Self {
209        self.missing_element_policy = policy;
210        self
211    }
212
213    /// Selects an explicit policy for ambiguous mmCIF residue boundaries.
214    #[must_use]
215    pub const fn ambiguous_residue_boundary_policy(
216        mut self,
217        policy: AmbiguousResidueBoundaryPolicy,
218    ) -> Self {
219        self.ambiguous_residue_boundary_policy = policy;
220        self
221    }
222
223    /// Drops hydrogens while reading.
224    #[must_use]
225    pub const fn discard_hydrogens(mut self, discard: bool) -> Self {
226        self.discard_hydrogens = discard;
227        self
228    }
229
230    /// Restricts which optional categories (mmCIF/BinaryCIF) or records (PDB)
231    /// are read. Names the structure cannot be built without are always read.
232    #[must_use]
233    pub fn categories(mut self, categories: CategoryFilter) -> Self {
234        self.categories = categories;
235        self
236    }
237
238    /// Records the SHA-256 of the bytes parsed, so a result can be tied to them.
239    #[must_use]
240    pub const fn digest_input(mut self, digest: bool) -> Self {
241        self.input_digest = if digest {
242            InputDigest::Sha256
243        } else {
244            InputDigest::Skip
245        };
246        self
247    }
248
249    /// Sets the ceilings that apply while reading.
250    #[must_use]
251    pub const fn limits(mut self, limits: Limits) -> Self {
252        self.limits = limits;
253        self
254    }
255
256    /// Finishes a read under this option set.
257    ///
258    /// # Errors
259    ///
260    /// Returns the ordered findings when at least one finding is an error under
261    /// the selected parse mode.
262    pub fn finish(
263        &self,
264        structure: Structure,
265        findings: impl IntoIterator<Item = Diagnostic>,
266    ) -> ReadResult {
267        let mut ordered = Diagnostics::new();
268        ordered.extend(findings);
269        let is_error = ordered.has_error(self.mode.strictness());
270        let findings = ordered.finish();
271        if is_error {
272            Err(findings)
273        } else {
274            Ok((structure, findings))
275        }
276    }
277}
278
279/// A structure and everything that was wrong with the file it came from.
280pub type ReadResult = Result<(Structure, Vec<Diagnostic>), Vec<Diagnostic>>;
281
282/// A reader for one format.
283pub trait Reader {
284    /// The format this reads.
285    const FORMAT: Format;
286
287    /// Reads a structure.
288    ///
289    /// # Errors
290    ///
291    /// Returns the findings that stopped the read.
292    fn read(input: &InputBuffer, options: &ReadOptions) -> ReadResult;
293}
294
295/// Which parts of a structure a write should include.
296pub trait Select {
297    /// Whether to write this model.
298    fn accept_model(&self, _model: crate::index::ModelIndex) -> bool {
299        true
300    }
301
302    /// Whether to write this chain.
303    fn accept_chain(&self, _chain: crate::index::ChainIndex) -> bool {
304        true
305    }
306
307    /// Whether to write this residue.
308    fn accept_residue(&self, _residue: crate::index::ResidueIndex) -> bool {
309        true
310    }
311
312    /// Whether to write this atom.
313    fn accept_atom(&self, _atom: crate::index::AtomIndex) -> bool {
314        true
315    }
316}
317
318/// A filter that writes everything.
319#[derive(Clone, Copy, Debug, Default)]
320pub struct SelectAll;
321
322impl Select for SelectAll {}
323
324#[cfg(test)]
325#[path = "options_tests.rs"]
326mod tests;