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