Skip to main content

molframe_cif/write/
options.rs

1//! Explicit decisions and failures for canonical CIF projection.
2
3use std::fmt::{Display, Formatter};
4use std::io;
5
6/// Decisions that cannot be recovered from a lowered structure.
7#[derive(Clone, Debug, Default, PartialEq, Eq)]
8pub struct CifWriteOptions {
9    block_id: Option<Box<str>>,
10    generate_connection_ids: bool,
11    extra_categories: Option<Box<str>>,
12}
13
14impl CifWriteOptions {
15    /// Starts with deterministic connection identifiers and no explicit block id.
16    ///
17    /// A connection's identifier is not retained by the bond graph, so a
18    /// canonical file numbers its connections in order. The numbering carries no
19    /// meaning of its own; [`Self::without_generated_connection_ids`] refuses
20    /// instead for a caller that wants nothing invented.
21    #[must_use]
22    pub const fn new() -> Self {
23        Self {
24            block_id: None,
25            generate_connection_ids: true,
26            extra_categories: None,
27        }
28    }
29
30    /// Refuses connectivity rather than numbering its connections.
31    #[must_use]
32    pub const fn without_generated_connection_ids(mut self) -> Self {
33        self.generate_connection_ids = false;
34        self
35    }
36
37    /// Appends already-rendered mmCIF categories to the output.
38    ///
39    /// For metadata the generic structure keeps in a typed extension (assemblies,
40    /// non-crystallographic operators, space group) that this crate cannot see.
41    /// The text must hold complete categories and no data-block header.
42    #[must_use]
43    pub fn with_extra_categories(mut self, text: impl Into<Box<str>>) -> Self {
44        let text = text.into();
45        self.extra_categories = (!text.is_empty()).then_some(text);
46        self
47    }
48
49    /// Rendered categories appended by the caller.
50    #[must_use]
51    pub fn extra_categories(&self) -> Option<&str> {
52        self.extra_categories.as_deref()
53    }
54
55    /// Uses this explicit data-block identifier instead of `_entry.id`.
56    #[must_use]
57    pub fn with_block_id(mut self, block_id: impl Into<Box<str>>) -> Self {
58        self.block_id = Some(block_id.into());
59        self
60    }
61
62    /// Allows deterministic `_struct_conn.id` values when connectivity exists.
63    ///
64    /// This is the default; it re-enables the choice after
65    /// [`Self::without_generated_connection_ids`].
66    /// The connection's own `conn_type_id` travels with the bond and is written
67    /// from it, so no class is chosen here.
68    #[must_use]
69    pub const fn with_generated_connection_ids(mut self) -> Self {
70        self.generate_connection_ids = true;
71        self
72    }
73
74    /// The explicit data-block identifier, when one was chosen.
75    #[must_use]
76    pub fn block_id(&self) -> Option<&str> {
77        self.block_id.as_deref()
78    }
79
80    pub(crate) const fn generates_connection_ids(&self) -> bool {
81        self.generate_connection_ids
82    }
83}
84
85/// A missing or invalid value that prevents canonical output.
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub enum CifWriteError {
88    /// Neither `_entry.id` nor an explicit block identifier is available.
89    MissingBlockId,
90    /// The explicit or stored data-block identifier is not a bare CIF token.
91    InvalidBlockId(String),
92    /// A model lacks its deposited model number.
93    MissingModelNumber {
94        /// Zero-based model position.
95        model: u32,
96    },
97    /// A model position exceeds the core model-index representation.
98    ModelIndexOverflow {
99        /// Zero-based model position.
100        model: usize,
101    },
102    /// The total number of atom-site rows exceeds the platform index space.
103    AtomRowCountOverflow,
104    /// A required atom-site item is unavailable.
105    MissingAtomField {
106        /// Zero-based atom position.
107        atom: u32,
108        /// Dictionary item name without the category prefix.
109        field: &'static str,
110    },
111    /// An atom-site identifier is zero, which records an absent source value.
112    MissingAtomSiteId {
113        /// Zero-based atom position.
114        atom: u32,
115    },
116    /// A bond points outside the atom table.
117    InvalidBondAtom {
118        /// Zero-based bond position.
119        bond: usize,
120        /// Referenced zero-based atom position.
121        atom: u32,
122    },
123    /// A bond endpoint cannot be represented by label identifiers.
124    MissingBondField {
125        /// Zero-based bond position.
126        bond: usize,
127        /// One-based endpoint number.
128        endpoint: u8,
129        /// Dictionary item name without the category prefix.
130        field: &'static str,
131    },
132    /// Connectivity needs identifiers that the lowered graph does not retain.
133    ConnectionIdsNotEnabled,
134    /// A secondary range endpoint lacks a required label identifier.
135    MissingSecondaryField {
136        /// Zero-based residue position.
137        residue: u32,
138        /// Missing dictionary item.
139        field: &'static str,
140    },
141    /// Secondary label identities are not unique in the projected topology.
142    AmbiguousSecondaryIdentity {
143        /// Zero-based residue position.
144        residue: u32,
145    },
146    /// Global secondary categories cannot encode independent model annotations.
147    ModelSpecificSecondary,
148}
149
150impl Display for CifWriteError {
151    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
152        match self {
153            Self::MissingBlockId => formatter.write_str(
154                "canonical CIF needs `_entry.id` or an explicit `CifWriteOptions::with_block_id`",
155            ),
156            Self::InvalidBlockId(id) => write!(formatter, "`{id}` is not a valid CIF block id"),
157            Self::MissingModelNumber { model } => {
158                write!(
159                    formatter,
160                    "model at position {model} has no deposited number"
161                )
162            }
163            Self::ModelIndexOverflow { model } => {
164                write!(
165                    formatter,
166                    "model position {model} exceeds the supported index range"
167                )
168            }
169            Self::AtomRowCountOverflow => {
170                formatter.write_str("canonical atom-site row count exceeds the platform range")
171            }
172            Self::MissingAtomField { atom, field } => {
173                write!(formatter, "atom {atom} has no required `{field}`")
174            }
175            Self::MissingAtomSiteId { atom } => {
176                write!(formatter, "atom {atom} has no source `_atom_site.id`")
177            }
178            Self::InvalidBondAtom { bond, atom } => {
179                write!(formatter, "bond {bond} refers to absent atom {atom}")
180            }
181            Self::MissingBondField {
182                bond,
183                endpoint,
184                field,
185            } => write!(
186                formatter,
187                "bond {bond} endpoint {endpoint} has no required `{field}`"
188            ),
189            Self::MissingSecondaryField { residue, field } => {
190                write!(
191                    formatter,
192                    "secondary residue {residue} has no required `{field}`"
193                )
194            }
195            Self::AmbiguousSecondaryIdentity { residue } => write!(
196                formatter,
197                "secondary residue {residue} has an ambiguous label identity"
198            ),
199            Self::ModelSpecificSecondary => formatter.write_str(
200                "canonical secondary categories cannot encode independent model annotations",
201            ),
202            Self::ConnectionIdsNotEnabled => formatter.write_str(
203                "canonical connectivity requires explicit generated connection identifiers",
204            ),
205        }
206    }
207}
208
209impl std::error::Error for CifWriteError {}
210
211/// A canonical projection or output-stream failure.
212#[derive(Debug)]
213pub enum CifWriteToError {
214    /// The semantic structure cannot be represented canonically.
215    Projection(CifWriteError),
216    /// The destination stopped accepting bytes.
217    Output(io::Error),
218}
219
220impl Display for CifWriteToError {
221    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
222        match self {
223            Self::Projection(error) => Display::fmt(error, formatter),
224            Self::Output(error) => write!(formatter, "output stream failed: {error}"),
225        }
226    }
227}
228
229impl std::error::Error for CifWriteToError {
230    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
231        match self {
232            Self::Projection(error) => Some(error),
233            Self::Output(error) => Some(error),
234        }
235    }
236}
237
238impl From<CifWriteError> for CifWriteToError {
239    fn from(error: CifWriteError) -> Self {
240        Self::Projection(error)
241    }
242}
243
244pub(crate) fn valid_block_id(id: &str) -> bool {
245    !id.is_empty()
246        && id.bytes().all(|byte| {
247            byte.is_ascii_graphic() && !matches!(byte, b'#' | b'\'' | b'"' | b'[' | b']')
248        })
249}