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 }
121 }
122 }
123
124 impl ::std::fmt::Display for $type {
125 fn fmt(&self, formatter: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
126 formatter.write_str(self.name())
127 }
128 }
129
130 impl ::std::str::FromStr for $type {
131 type Err = $crate::contract::PolicyParseError;
132
133 fn from_str(value: &str) -> Result<Self, Self::Err> {
134 match $crate::contract::canonical_spelling(value).as_str() {
135 $($name => Ok($variant),)+
136 _ => Err($crate::contract::PolicyParseError::new(
137 $field,
138 value,
139 &Self::NAMES.join(", "),
140 )),
141 }
142 }
143 }
144 };
145}
146
147pub(crate) use closed_vocabulary;
148
149closed_vocabulary!(
150 Namespace,
151 "identifiers",
152 [
153 (Namespace::Label, "label"),
154 (Namespace::Auth, "auth"),
155 (Namespace::Explicit, "explicit"),
156 ]
157);
158closed_vocabulary!(
159 MissingPolicy,
160 "missing_atoms",
161 [
162 (MissingPolicy::Ignore, "ignore"),
163 (MissingPolicy::Report, "report"),
164 (MissingPolicy::Indeterminate, "indeterminate"),
165 (MissingPolicy::Fail, "fail"),
166 ]
167);
168closed_vocabulary!(
169 HydrogenPolicy,
170 "hydrogens",
171 [
172 (HydrogenPolicy::ExplicitOnly, "explicit-only"),
173 (HydrogenPolicy::IncludeInferred, "include-inferred"),
174 (HydrogenPolicy::Exclude, "exclude"),
175 ]
176);
177closed_vocabulary!(
178 EquivalencePolicy,
179 "atom_equivalence",
180 [
181 (EquivalencePolicy::None, "none"),
182 (EquivalencePolicy::Ccd, "ccd"),
183 (EquivalencePolicy::Explicit, "explicit"),
184 ]
185);
186closed_vocabulary!(
187 SymmetryPolicy,
188 "symmetry",
189 [
190 (SymmetryPolicy::None, "none"),
191 (SymmetryPolicy::Crystallographic, "crystallographic"),
192 (SymmetryPolicy::BiologicalAssembly, "biological-assembly"),
193 ]
194);
195closed_vocabulary!(
196 Precision,
197 "precision",
198 [(Precision::F32, "f32"), (Precision::F64, "f64"),]
199);
200closed_vocabulary!(
201 PeriodicPolicy,
202 "periodic",
203 [
204 (PeriodicPolicy::None, "none"),
205 (PeriodicPolicy::Pbc, "pbc"),
206 (PeriodicPolicy::MinimumImage, "minimum-image"),
207 ]
208);
209closed_vocabulary!(
210 RadiiSet,
211 "vdw_radii",
212 [
213 (RadiiSet::Bondi, "bondi"),
214 (RadiiSet::AmberUnited, "amber-united"),
215 (RadiiSet::Charmm, "charmm"),
216 (RadiiSet::Alvarez, "alvarez"),
217 ]
218);
219
220impl FromStr for AltlocPolicy {
221 type Err = PolicyParseError;
222
223 fn from_str(value: &str) -> Result<Self, Self::Err> {
224 if let Some(label) = payload("altloc", value, "label")? {
225 return Ok(Self::Label(label.into()));
226 }
227 match canonical(value).as_str() {
228 "keep-all" => Ok(Self::KeepAll),
229 "conformer-consistent" => Ok(Self::ConformerConsistent),
230 "first" => Ok(Self::First),
231 "highest-occupancy-per-residue" => Ok(Self::HighestOccupancyPerResidue),
232 "highest-occupancy-per-atom" => Ok(Self::HighestOccupancyPerAtom),
233 _ => Err(refused(
234 "altloc",
235 value,
236 "keep-all, conformer-consistent, first, highest-occupancy-per-residue, \
237 highest-occupancy-per-atom or label:<id>",
238 )),
239 }
240 }
241}
242
243impl fmt::Display for AltlocPolicy {
244 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
245 match self {
246 Self::KeepAll => formatter.write_str("keep-all"),
247 Self::ConformerConsistent => formatter.write_str("conformer-consistent"),
248 Self::First => formatter.write_str("first"),
249 Self::HighestOccupancyPerResidue => {
250 formatter.write_str("highest-occupancy-per-residue")
251 }
252 Self::HighestOccupancyPerAtom => formatter.write_str("highest-occupancy-per-atom"),
253 Self::Label(label) => write!(formatter, "label:{label}"),
254 }
255 }
256}
257
258impl FromStr for AssemblyChoice {
259 type Err = PolicyParseError;
260
261 fn from_str(value: &str) -> Result<Self, Self::Err> {
262 if let Some(id) = payload("assembly", value, "biological")? {
263 return Ok(Self::Biological(id.into()));
264 }
265 if let Some(radius) = payload("assembly", value, "crystal")? {
266 return Ok(Self::Crystal {
267 radius: finite_float("assembly", value, radius, false)?,
268 });
269 }
270 match canonical(value).as_str() {
271 "asymmetric-unit" => Ok(Self::AsymmetricUnit),
272 _ => Err(refused(
273 "assembly",
274 value,
275 "asymmetric-unit, biological:<id> or crystal:<radius>",
276 )),
277 }
278 }
279}
280
281impl fmt::Display for AssemblyChoice {
282 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
283 match self {
284 Self::AsymmetricUnit => formatter.write_str("asymmetric-unit"),
285 Self::Biological(id) => write!(formatter, "biological:{id}"),
286 Self::Crystal { radius } => write!(formatter, "crystal:{radius}"),
287 }
288 }
289}
290
291impl FromStr for ModelChoice {
292 type Err = PolicyParseError;
293
294 fn from_str(value: &str) -> Result<Self, Self::Err> {
295 if let Some(index) = payload("model", value, "index")? {
296 return index
297 .parse()
298 .map(Self::Index)
299 .map_err(|_| refused("model", value, "index:<non-negative integer>"));
300 }
301 match canonical(value).as_str() {
302 "first" => Ok(Self::First),
303 "all" => Ok(Self::All),
304 "ensemble" => Ok(Self::Ensemble),
305 _ => Err(refused("model", value, "first, all, ensemble or index:<n>")),
306 }
307 }
308}
309
310impl fmt::Display for ModelChoice {
311 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
312 match self {
313 Self::First => formatter.write_str("first"),
314 Self::Index(index) => write!(formatter, "index:{index}"),
315 Self::All => formatter.write_str("all"),
316 Self::Ensemble => formatter.write_str("ensemble"),
317 }
318 }
319}
320
321impl FromStr for AlignmentPolicy {
322 type Err = PolicyParseError;
323
324 fn from_str(value: &str) -> Result<Self, Self::Err> {
325 if let Some(selection) = payload("alignment", value, "explicit")? {
326 return Ok(Self::Explicit(selection.into()));
327 }
328 match canonical(value).as_str() {
329 "none" => Ok(Self::None),
330 "global" => Ok(Self::Global),
331 "local" => Ok(Self::Local),
332 _ => Err(refused(
333 "alignment",
334 value,
335 "none, global, local or explicit:<selection>",
336 )),
337 }
338 }
339}
340
341impl fmt::Display for AlignmentPolicy {
342 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
343 match self {
344 Self::None => formatter.write_str("none"),
345 Self::Explicit(selection) => write!(formatter, "explicit:{selection}"),
346 Self::Global => formatter.write_str("global"),
347 Self::Local => formatter.write_str("local"),
348 }
349 }
350}
351
352impl FromStr for ContactDefinition {
353 type Err = PolicyParseError;
354
355 fn from_str(value: &str) -> Result<Self, Self::Err> {
356 if let Some(tolerance) = payload("contact_def", value, "distance")? {
357 return Ok(Self::DistanceCutoff {
358 tolerance: finite_float("contact_def", value, tolerance, true)?,
359 });
360 }
361 if let Some(probe) = payload("contact_def", value, "surface")? {
362 return Ok(Self::SurfaceBased {
363 probe: finite_float("contact_def", value, probe, false)?,
364 });
365 }
366 Err(refused(
367 "contact_def",
368 value,
369 "distance:<tolerance> or surface:<probe>",
370 ))
371 }
372}
373
374impl fmt::Display for ContactDefinition {
375 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
376 match self {
377 Self::DistanceCutoff { tolerance } => write!(formatter, "distance:{tolerance}"),
378 Self::SurfaceBased { probe } => write!(formatter, "surface:{probe}"),
379 }
380 }
381}
382
383#[cfg(test)]
384#[path = "vocabulary_tests.rs"]
385mod tests;