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}
117
118impl Display for CifWriteError {
119    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
120        match self {
121            Self::MissingBlockId => formatter.write_str(
122                "canonical CIF needs `_entry.id` or an explicit `CifWriteOptions::with_block_id`",
123            ),
124            Self::InvalidBlockId(id) => write!(formatter, "`{id}` is not a valid CIF block id"),
125            Self::MissingModelNumber { model } => {
126                write!(
127                    formatter,
128                    "model at position {model} has no deposited number"
129                )
130            }
131            Self::ModelIndexOverflow { model } => {
132                write!(
133                    formatter,
134                    "model position {model} exceeds the supported index range"
135                )
136            }
137            Self::AtomRowCountOverflow => {
138                formatter.write_str("canonical atom-site row count exceeds the platform range")
139            }
140            Self::MissingAtomField { atom, field } => {
141                write!(formatter, "atom {atom} has no required `{field}`")
142            }
143            Self::MissingAtomSiteId { atom } => {
144                write!(formatter, "atom {atom} has no source `_atom_site.id`")
145            }
146            Self::InvalidBondAtom { bond, atom } => {
147                write!(formatter, "bond {bond} refers to absent atom {atom}")
148            }
149            Self::MissingBondField {
150                bond,
151                endpoint,
152                field,
153            } => write!(
154                formatter,
155                "bond {bond} endpoint {endpoint} has no required `{field}`"
156            ),
157            Self::ConnectionIdsNotEnabled => formatter.write_str(
158                "canonical connectivity requires explicit generated connection identifiers",
159            ),
160            Self::MissingConnectionTypeId => formatter.write_str(
161                "canonical connectivity requires an explicit `_struct_conn.conn_type_id`",
162            ),
163        }
164    }
165}
166
167impl std::error::Error for CifWriteError {}
168
169/// A canonical projection or output-stream failure.
170#[derive(Debug)]
171pub enum CifWriteToError {
172    /// The semantic structure cannot be represented canonically.
173    Projection(CifWriteError),
174    /// The destination stopped accepting bytes.
175    Output(io::Error),
176}
177
178impl Display for CifWriteToError {
179    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
180        match self {
181            Self::Projection(error) => Display::fmt(error, formatter),
182            Self::Output(error) => write!(formatter, "output stream failed: {error}"),
183        }
184    }
185}
186
187impl std::error::Error for CifWriteToError {
188    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
189        match self {
190            Self::Projection(error) => Some(error),
191            Self::Output(error) => Some(error),
192        }
193    }
194}
195
196impl From<CifWriteError> for CifWriteToError {
197    fn from(error: CifWriteError) -> Self {
198        Self::Projection(error)
199    }
200}
201
202pub(crate) fn valid_block_id(id: &str) -> bool {
203    !id.is_empty()
204        && id.bytes().all(|byte| {
205            byte.is_ascii_graphic() && !matches!(byte, b'#' | b'\'' | b'"' | b'[' | b']')
206        })
207}