1use super::{
11 AlignmentPolicy, AltlocPolicy, AssemblyChoice, ContactDefinition, EquivalencePolicy,
12 HydrogenPolicy, MissingPolicy, ModelChoice, Namespace, PeriodicPolicy, Precision, RadiiSet,
13 SymmetryPolicy,
14};
15use std::fmt;
16use std::str::FromStr;
17
18#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
21#[error("invalid policy value for {field}: {value} (expected {expected})")]
22pub struct PolicyParseError {
23 pub field: &'static str,
25 pub value: String,
27 pub expected: String,
29}
30
31crate::diagnostic_from!(
32 PolicyParseError,
33 |_error| crate::Code::E6104,
34 |diagnostic, error| diagnostic
35 .about_field(error.field)
36 .with_context("value", error.value.clone())
37 .with_context("expected", error.expected.clone())
38);
39
40impl PolicyParseError {
41 #[must_use]
44 pub fn new(field: &'static str, value: &str, expected: &str) -> Self {
45 Self {
46 field,
47 value: value.to_owned(),
48 expected: expected.to_owned(),
49 }
50 }
51}
52
53fn refused(field: &'static str, value: &str, expected: &str) -> PolicyParseError {
54 PolicyParseError::new(field, value, expected)
55}
56
57#[must_use]
60pub fn canonical_spelling(value: &str) -> String {
61 value.replace('_', "-")
62}
63
64fn canonical(value: &str) -> String {
65 canonical_spelling(value)
66}
67
68fn payload<'a>(
70 field: &'static str,
71 value: &'a str,
72 prefix: &str,
73) -> Result<Option<&'a str>, PolicyParseError> {
74 match value.split_once(':') {
75 Some((head, rest)) if canonical(head) == prefix => {
76 if rest.is_empty() {
77 Err(refused(field, value, &format!("a value after '{prefix}:'")))
78 } else {
79 Ok(Some(rest))
80 }
81 }
82 _ => Ok(None),
83 }
84}
85
86fn finite_float(
87 field: &'static str,
88 value: &str,
89 text: &str,
90 allow_zero: bool,
91) -> Result<f32, PolicyParseError> {
92 let expected = if allow_zero {
93 "a finite non-negative number"
94 } else {
95 "a finite positive number"
96 };
97 let parsed: f32 = text.parse().map_err(|_| refused(field, value, expected))?;
98 if parsed.is_finite() && (parsed > 0.0 || (allow_zero && parsed == 0.0)) {
99 Ok(parsed)
100 } else {
101 Err(refused(field, value, expected))
102 }
103}
104
105macro_rules! closed_vocabulary {
110 ($type:ty, $field:literal, [$(($variant:path, $name:literal)),+ $(,)?]) => {
111 impl $type {
112 pub const NAMES: &'static [&'static str] = &[$($name),+];
114
115 #[must_use]
117 pub const fn name(&self) -> &'static str {
118 match self {
119 $($variant => $name,)+
120 #[allow(unreachable_patterns)]
121 _ => "custom",
122 }
123 }
124 }
125
126 impl ::std::fmt::Display for $type {
127 fn fmt(&self, formatter: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
128 formatter.write_str(self.name())
129 }
130 }
131
132 impl ::std::str::FromStr for $type {
133 type Err = $crate::contract::PolicyParseError;
134
135 fn from_str(value: &str) -> Result<Self, Self::Err> {
136 match $crate::contract::canonical_spelling(value).as_str() {
137 $($name => Ok($variant),)+
138 _ => Err($crate::contract::PolicyParseError::new(
139 $field,
140 value,
141 &Self::NAMES.join(", "),
142 )),
143 }
144 }
145 }
146 };
147}
148
149pub(crate) use closed_vocabulary;
150
151closed_vocabulary!(
152 Namespace,
153 "identifiers",
154 [
155 (Namespace::Label, "label"),
156 (Namespace::Auth, "auth"),
157 (Namespace::Explicit, "explicit"),
158 ]
159);
160closed_vocabulary!(
161 MissingPolicy,
162 "missing_atoms",
163 [
164 (MissingPolicy::Ignore, "ignore"),
165 (MissingPolicy::Report, "report"),
166 (MissingPolicy::Indeterminate, "indeterminate"),
167 (MissingPolicy::Fail, "fail"),
168 ]
169);
170closed_vocabulary!(
171 HydrogenPolicy,
172 "hydrogens",
173 [
174 (HydrogenPolicy::ExplicitOnly, "explicit-only"),
175 (HydrogenPolicy::IncludeInferred, "include-inferred"),
176 (HydrogenPolicy::Exclude, "exclude"),
177 ]
178);
179closed_vocabulary!(
180 EquivalencePolicy,
181 "atom_equivalence",
182 [
183 (EquivalencePolicy::None, "none"),
184 (EquivalencePolicy::Ccd, "ccd"),
185 (EquivalencePolicy::Explicit, "explicit"),
186 ]
187);
188closed_vocabulary!(
189 SymmetryPolicy,
190 "symmetry",
191 [
192 (SymmetryPolicy::None, "none"),
193 (SymmetryPolicy::Crystallographic, "crystallographic"),
194 (SymmetryPolicy::BiologicalAssembly, "biological-assembly"),
195 ]
196);
197closed_vocabulary!(
198 Precision,
199 "precision",
200 [(Precision::F32, "f32"), (Precision::F64, "f64"),]
201);
202closed_vocabulary!(
203 PeriodicPolicy,
204 "periodic",
205 [
206 (PeriodicPolicy::None, "none"),
207 (PeriodicPolicy::Pbc, "pbc"),
208 (PeriodicPolicy::MinimumImage, "minimum-image"),
209 ]
210);
211closed_vocabulary!(
212 RadiiSet,
213 "vdw_radii",
214 [
215 (RadiiSet::Bondi, "bondi"),
216 (RadiiSet::AmberUnited, "amber-united"),
217 (RadiiSet::Charmm, "charmm"),
218 (RadiiSet::Alvarez, "alvarez"),
219 ]
220);
221
222impl FromStr for AltlocPolicy {
223 type Err = PolicyParseError;
224
225 fn from_str(value: &str) -> Result<Self, Self::Err> {
226 if let Some(label) = payload("altloc", value, "label")? {
227 return Ok(Self::Label(label.into()));
228 }
229 match canonical(value).as_str() {
230 "keep-all" => Ok(Self::KeepAll),
231 "conformer-consistent" => Ok(Self::ConformerConsistent),
232 "first" => Ok(Self::First),
233 "highest-occupancy-per-residue" => Ok(Self::HighestOccupancyPerResidue),
234 "highest-occupancy-per-atom" => Ok(Self::HighestOccupancyPerAtom),
235 _ => Err(refused(
236 "altloc",
237 value,
238 "keep-all, conformer-consistent, first, highest-occupancy-per-residue, \
239 highest-occupancy-per-atom or label:<id>",
240 )),
241 }
242 }
243}
244
245impl fmt::Display for AltlocPolicy {
246 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
247 match self {
248 Self::KeepAll => formatter.write_str("keep-all"),
249 Self::ConformerConsistent => formatter.write_str("conformer-consistent"),
250 Self::First => formatter.write_str("first"),
251 Self::HighestOccupancyPerResidue => {
252 formatter.write_str("highest-occupancy-per-residue")
253 }
254 Self::HighestOccupancyPerAtom => formatter.write_str("highest-occupancy-per-atom"),
255 Self::Label(label) => write!(formatter, "label:{label}"),
256 }
257 }
258}
259
260impl FromStr for AssemblyChoice {
261 type Err = PolicyParseError;
262
263 fn from_str(value: &str) -> Result<Self, Self::Err> {
264 if let Some(id) = payload("assembly", value, "biological")? {
265 return Ok(Self::Biological(id.into()));
266 }
267 if let Some(radius) = payload("assembly", value, "crystal")? {
268 return Ok(Self::Crystal {
269 radius: finite_float("assembly", value, radius, false)?,
270 });
271 }
272 match canonical(value).as_str() {
273 "asymmetric-unit" => Ok(Self::AsymmetricUnit),
274 _ => Err(refused(
275 "assembly",
276 value,
277 "asymmetric-unit, biological:<id> or crystal:<radius>",
278 )),
279 }
280 }
281}
282
283impl fmt::Display for AssemblyChoice {
284 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
285 match self {
286 Self::AsymmetricUnit => formatter.write_str("asymmetric-unit"),
287 Self::Biological(id) => write!(formatter, "biological:{id}"),
288 Self::Crystal { radius } => write!(formatter, "crystal:{radius}"),
289 }
290 }
291}
292
293impl FromStr for ModelChoice {
294 type Err = PolicyParseError;
295
296 fn from_str(value: &str) -> Result<Self, Self::Err> {
297 if let Some(index) = payload("model", value, "index")? {
298 return index
299 .parse()
300 .map(Self::Index)
301 .map_err(|_| refused("model", value, "index:<non-negative integer>"));
302 }
303 match canonical(value).as_str() {
304 "first" => Ok(Self::First),
305 "all" => Ok(Self::All),
306 "ensemble" => Ok(Self::Ensemble),
307 _ => Err(refused("model", value, "first, all, ensemble or index:<n>")),
308 }
309 }
310}
311
312impl fmt::Display for ModelChoice {
313 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
314 match self {
315 Self::First => formatter.write_str("first"),
316 Self::Index(index) => write!(formatter, "index:{index}"),
317 Self::All => formatter.write_str("all"),
318 Self::Ensemble => formatter.write_str("ensemble"),
319 }
320 }
321}
322
323impl FromStr for AlignmentPolicy {
324 type Err = PolicyParseError;
325
326 fn from_str(value: &str) -> Result<Self, Self::Err> {
327 if let Some(selection) = payload("alignment", value, "explicit")? {
328 return Ok(Self::Explicit(selection.into()));
329 }
330 match canonical(value).as_str() {
331 "none" => Ok(Self::None),
332 "global" => Ok(Self::Global),
333 "local" => Ok(Self::Local),
334 _ => Err(refused(
335 "alignment",
336 value,
337 "none, global, local or explicit:<selection>",
338 )),
339 }
340 }
341}
342
343impl fmt::Display for AlignmentPolicy {
344 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
345 match self {
346 Self::None => formatter.write_str("none"),
347 Self::Explicit(selection) => write!(formatter, "explicit:{selection}"),
348 Self::Global => formatter.write_str("global"),
349 Self::Local => formatter.write_str("local"),
350 }
351 }
352}
353
354impl FromStr for ContactDefinition {
355 type Err = PolicyParseError;
356
357 fn from_str(value: &str) -> Result<Self, Self::Err> {
358 if let Some(tolerance) = payload("contact_def", value, "distance")? {
359 return Ok(Self::DistanceCutoff {
360 tolerance: finite_float("contact_def", value, tolerance, true)?,
361 });
362 }
363 if let Some(probe) = payload("contact_def", value, "surface")? {
364 return Ok(Self::SurfaceBased {
365 probe: finite_float("contact_def", value, probe, false)?,
366 });
367 }
368 Err(refused(
369 "contact_def",
370 value,
371 "distance:<tolerance> or surface:<probe>",
372 ))
373 }
374}
375
376impl fmt::Display for ContactDefinition {
377 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
378 match self {
379 Self::DistanceCutoff { tolerance } => write!(formatter, "distance:{tolerance}"),
380 Self::SurfaceBased { probe } => write!(formatter, "surface:{probe}"),
381 }
382 }
383}
384
385#[cfg(test)]
386#[path = "vocabulary_tests.rs"]
387mod tests;