Skip to main content

u_nesting_core/
error.rs

1//! Error types for U-Nesting.
2
3use thiserror::Error;
4
5/// Result type alias for U-Nesting operations.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// `value` inside `[min, max]` (a NaN is outside every range), or an
9/// [`Error::OutOfRange`] naming the field and the range.
10///
11/// # Errors
12///
13/// [`Error::OutOfRange`] with the closed bounds `min` and `max`.
14pub fn check_range(name: &str, value: f64, min: f64, max: f64) -> Result<()> {
15    if value >= min && value <= max {
16        Ok(())
17    } else {
18        Err(Error::OutOfRange {
19            parameter: name.to_string(),
20            id: None,
21            min: Some(min),
22            max: Some(max),
23            exclusive_min: None,
24            exclusive_max: None,
25            got: value,
26            range: format!("in [{min}, {max}]"),
27        })
28    }
29}
30
31/// `value` finite and at least `min`, or an [`Error::OutOfRange`] naming the
32/// field.
33///
34/// # Errors
35///
36/// [`Error::OutOfRange`] with the closed lower bound `min`.
37pub fn check_at_least(name: &str, value: f64, min: f64) -> Result<()> {
38    if value.is_finite() && value >= min {
39        Ok(())
40    } else {
41        Err(Error::OutOfRange {
42            parameter: name.to_string(),
43            id: None,
44            min: Some(min),
45            max: None,
46            exclusive_min: None,
47            exclusive_max: None,
48            got: value,
49            range: format!("a finite number >= {min}"),
50        })
51    }
52}
53
54/// `value` finite and strictly above `bound`, or an [`Error::OutOfRange`]
55/// naming the field with `bound` as its open lower bound (`exclusive_min`).
56///
57/// # Errors
58///
59/// [`Error::OutOfRange`] with the open lower bound `bound`.
60pub fn check_above(name: &str, value: f64, bound: f64) -> Result<()> {
61    if value.is_finite() && value > bound {
62        Ok(())
63    } else {
64        Err(Error::OutOfRange {
65            parameter: name.to_string(),
66            id: None,
67            min: None,
68            max: None,
69            exclusive_min: Some(bound),
70            exclusive_max: None,
71            got: value,
72            range: format!("a finite number > {bound}"),
73        })
74    }
75}
76
77/// An unsigned integer type a request's whole number is read into: a count,
78/// a time budget, a seed.
79pub trait WholeNumber: TryFrom<i128> + Copy {
80    /// The largest value the type holds.
81    const MAX: i128;
82}
83
84impl WholeNumber for u32 {
85    const MAX: i128 = u32::MAX as i128;
86}
87
88impl WholeNumber for u64 {
89    const MAX: i128 = u64::MAX as i128;
90}
91
92impl WholeNumber for usize {
93    const MAX: i128 = usize::MAX as i128;
94}
95
96/// `value` -- a whole number as a request gives it, negatives included -- as
97/// `T`, when it is at least `min` and `T` holds it; otherwise an
98/// [`Error::OutOfRange`] naming `parameter`.
99///
100/// A negative and a too-small positive value are refused alike, with the
101/// closed lower bound `min`; a value above what `T` holds also carries `max`.
102///
103/// # Errors
104///
105/// [`Error::OutOfRange`] when `value < min` or `value > T::MAX`.
106pub fn whole_at_least<T: WholeNumber>(parameter: &str, value: i128, min: i128) -> Result<T> {
107    match T::try_from(value) {
108        Ok(v) if value >= min => Ok(v),
109        _ => {
110            let above = value > T::MAX;
111            Err(Error::OutOfRange {
112                parameter: parameter.to_string(),
113                id: None,
114                min: Some(min as f64),
115                max: above.then_some(T::MAX as f64),
116                exclusive_min: None,
117                exclusive_max: None,
118                got: value as f64,
119                range: if above {
120                    format!("a whole number in [{min}, {}]", T::MAX)
121                } else {
122                    format!("a whole number >= {min}")
123                },
124            })
125        }
126    }
127}
128
129/// Why the engine refused a request or could not finish it. Each variant
130/// carries what a caller needs to point at the input to change -- the setting,
131/// the geometry id, the positions -- as fields, not only in the message;
132/// [`Error::code`] is the stable name of the reason.
133#[derive(Debug, Clone, PartialEq, Error)]
134#[non_exhaustive]
135pub enum Error {
136    /// A setting outside the values it accepts, and `range` says it in words.
137    /// `min` / `max` are the closed bounds (the value may equal them),
138    /// `exclusive_min` / `exclusive_max` the open ones (it must lie strictly
139    /// beyond them). A side has at most one of the two; `None` on both means
140    /// that side is unbounded. `id` names the geometry the setting belongs to,
141    /// when it belongs to one (a quantity).
142    #[error("{parameter} must be {range}, got {got}")]
143    OutOfRange {
144        parameter: String,
145        id: Option<String>,
146        min: Option<f64>,
147        max: Option<f64>,
148        exclusive_min: Option<f64>,
149        exclusive_max: Option<f64>,
150        got: f64,
151        range: String,
152    },
153
154    /// A list the request must not leave empty -- `parameter` names it
155    /// (`geometries`): there is nothing to solve.
156    #[error("{parameter} is empty; give at least one")]
157    EmptyInput { parameter: String },
158
159    /// A setting that cannot be used as given (a strategy this build or this
160    /// dimension does not offer, ...).
161    #[error("{parameter}: {message}")]
162    InvalidOption { parameter: String, message: String },
163
164    /// An option name the engine does not know: `got` is the name given and
165    /// `expected` the names it accepts.
166    #[error("unknown {parameter} '{got}'; expected one of {}", expected.join(", "))]
167    UnknownOption {
168        parameter: String,
169        got: String,
170        expected: Vec<String>,
171    },
172
173    /// A request of the wrong shape: not JSON, a missing or unknown key, a
174    /// value of the wrong type. `parameter` names the part when it is known.
175    #[error("{message}")]
176    MalformedInput {
177        parameter: Option<String>,
178        message: String,
179    },
180
181    /// A number that is not finite (NaN or an infinity), where a request
182    /// holds numbers -- a transport whose values can carry one (a Python
183    /// float) refuses it before the request is read. `parameter` is the path
184    /// of the value, or of the list it is in, and `index` its position in
185    /// that list; `id` names the geometry it belongs to.
186    #[error("{}: expected a finite number, got {got}", match index {
187        Some(i) => format!("{parameter}[{i}]"),
188        None => parameter.clone(),
189    })]
190    NotFinite {
191        parameter: String,
192        id: Option<String>,
193        index: Option<usize>,
194        got: f64,
195    },
196
197    /// The caller cancelled the run.
198    #[error("cancelled by the caller")]
199    Cancelled,
200
201    /// A geometry the engine cannot work with, named by its id when the check
202    /// that refused it knows the geometry.
203    #[error("Invalid geometry: {message}")]
204    InvalidGeometry { id: Option<String>, message: String },
205
206    /// Two geometries share an id, at positions `first` and `second` of the
207    /// request (counting from 0).
208    #[error(
209        "the id '{id}' is given twice, at positions {first} and {second} of geometries \
210         (counting from 0); placements name geometries by id, so every geometry needs \
211         its own"
212    )]
213    DuplicateId {
214        id: String,
215        first: usize,
216        second: usize,
217    },
218
219    /// A boundary the engine cannot place into; `parameter` names the field
220    /// when one field is at fault.
221    #[error("Invalid boundary: {message}")]
222    InvalidBoundary {
223        parameter: Option<String>,
224        message: String,
225    },
226
227    /// A failure inside the engine, not caused by the request.
228    #[error("Internal error: {0}")]
229    Internal(String),
230}
231
232impl Error {
233    /// One of each reason, every optional value present -- the published
234    /// vocabulary ([`crate::api_types::CodeCatalog`]) reads each code and the
235    /// fields that can come with it off these.
236    #[cfg(feature = "serde")]
237    pub(crate) fn examples() -> Vec<Self> {
238        let s = String::new;
239        vec![
240            Error::OutOfRange {
241                parameter: s(),
242                id: Some(s()),
243                min: Some(0.0),
244                max: Some(0.0),
245                exclusive_min: Some(0.0),
246                exclusive_max: Some(0.0),
247                got: 0.0,
248                range: s(),
249            },
250            Error::EmptyInput { parameter: s() },
251            Error::InvalidOption {
252                parameter: s(),
253                message: s(),
254            },
255            Error::UnknownOption {
256                parameter: s(),
257                got: s(),
258                expected: vec![],
259            },
260            Error::MalformedInput {
261                parameter: Some(s()),
262                message: s(),
263            },
264            Error::NotFinite {
265                parameter: s(),
266                id: Some(s()),
267                index: Some(0),
268                got: f64::NAN,
269            },
270            Error::Cancelled,
271            Error::InvalidGeometry {
272                id: Some(s()),
273                message: s(),
274            },
275            Error::DuplicateId {
276                id: s(),
277                first: 0,
278                second: 0,
279            },
280            Error::InvalidBoundary {
281                parameter: Some(s()),
282                message: s(),
283            },
284            Error::Internal(s()),
285        ]
286    }
287
288    /// The stable name of the reason: `parameter_out_of_range`,
289    /// `empty_input`, `invalid_option`, `unknown_option`, `malformed_input`,
290    /// `value_not_finite`, `invalid_geometry`, `duplicate_id`,
291    /// `invalid_boundary`, `cancelled` or `internal`.
292    pub fn code(&self) -> &'static str {
293        match self {
294            Error::OutOfRange { .. } => "parameter_out_of_range",
295            Error::EmptyInput { .. } => "empty_input",
296            Error::InvalidOption { .. } => "invalid_option",
297            Error::UnknownOption { .. } => "unknown_option",
298            Error::MalformedInput { .. } => "malformed_input",
299            Error::NotFinite { .. } => "value_not_finite",
300            Error::Cancelled => "cancelled",
301            Error::InvalidGeometry { .. } => "invalid_geometry",
302            Error::DuplicateId { .. } => "duplicate_id",
303            Error::InvalidBoundary { .. } => "invalid_boundary",
304            Error::Internal(_) => "internal",
305        }
306    }
307
308    /// The setting or request part the refusal names, when it names one.
309    pub fn parameter(&self) -> Option<&str> {
310        match self {
311            Error::OutOfRange { parameter, .. }
312            | Error::EmptyInput { parameter }
313            | Error::InvalidOption { parameter, .. }
314            | Error::UnknownOption { parameter, .. }
315            | Error::NotFinite { parameter, .. } => Some(parameter),
316            Error::MalformedInput { parameter, .. } | Error::InvalidBoundary { parameter, .. } => {
317                parameter.as_deref()
318            }
319            Error::DuplicateId { .. } => Some("geometries"),
320            _ => None,
321        }
322    }
323
324    /// This refusal with its `parameter` named by its path under `section`
325    /// when it is one of `fields` -- `spacing`, a field of the request's
326    /// `config`, becomes `config.spacing` -- and unchanged otherwise.
327    ///
328    /// The Rust API names a setting by its field, as the configuration type
329    /// the caller built has it; a request names it by where it sits in the
330    /// request. The wire layer, which knows the request, places it.
331    pub fn located(mut self, section: &str, fields: &[&str]) -> Self {
332        let parameter = match &mut self {
333            Error::OutOfRange { parameter, .. }
334            | Error::EmptyInput { parameter }
335            | Error::InvalidOption { parameter, .. }
336            | Error::UnknownOption { parameter, .. }
337            | Error::NotFinite { parameter, .. } => Some(parameter),
338            Error::MalformedInput { parameter, .. } | Error::InvalidBoundary { parameter, .. } => {
339                parameter.as_mut()
340            }
341            _ => None,
342        };
343        if let Some(parameter) = parameter {
344            if fields.contains(&parameter.as_str()) {
345                *parameter = format!("{section}.{parameter}");
346            }
347        }
348        self
349    }
350
351    /// A list the request must not leave empty.
352    pub fn empty_input(parameter: &str) -> Self {
353        Error::EmptyInput {
354            parameter: parameter.to_string(),
355        }
356    }
357
358    /// A setting that cannot be used as given.
359    pub fn invalid_option(parameter: &str, message: String) -> Self {
360        Error::InvalidOption {
361            parameter: parameter.to_string(),
362            message,
363        }
364    }
365
366    /// A geometry refused by a check that may or may not know which one.
367    pub fn invalid_geometry(id: Option<&str>, message: String) -> Self {
368        Error::InvalidGeometry {
369            id: id.map(str::to_string),
370            message,
371        }
372    }
373
374    /// A request of the wrong shape, naming the part when it is known.
375    pub fn malformed(parameter: Option<&str>, message: String) -> Self {
376        Error::MalformedInput {
377            parameter: parameter.map(str::to_string),
378            message,
379        }
380    }
381
382    /// A boundary refused, naming the field when one is at fault.
383    pub fn invalid_boundary(parameter: Option<&str>, message: String) -> Self {
384        Error::InvalidBoundary {
385            parameter: parameter.map(str::to_string),
386            message,
387        }
388    }
389}