Skip to main content

twine_thermo/model/coolprop/
error.rs

1use std::sync::PoisonError;
2
3use thiserror::Error;
4
5use crate::PropertyError;
6
7/// Errors returned by the [`CoolProp`](super::CoolProp) model.
8#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum CoolPropError {
11    #[error(transparent)]
12    Rfluids(#[from] rfluids::native::CoolPropError),
13    #[error("CoolProp abstract state mutex poisoned")]
14    Poisoned,
15}
16
17impl<T> From<PoisonError<T>> for CoolPropError {
18    fn from(_: PoisonError<T>) -> Self {
19        CoolPropError::Poisoned
20    }
21}
22
23impl From<CoolPropError> for PropertyError {
24    fn from(error: CoolPropError) -> Self {
25        match error {
26            CoolPropError::Rfluids(message) => map_error_message(&message.to_string()),
27            CoolPropError::Poisoned => PropertyError::Calculation {
28                context: "CoolProp abstract state mutex poisoned".to_string(),
29            },
30        }
31    }
32}
33
34/// The `rfluids::native::CoolPropError` type reports errors as strings,
35/// and the wording may change over time.
36/// This mapping aims to provide the most useful `PropertyError` variant.
37/// Unknown error strings default to `PropertyError::Calculation`.
38fn map_error_message(message: &str) -> PropertyError {
39    const UNDEFINED_MARKERS: &[&str] = &["not defined"];
40    const OUT_OF_DOMAIN_MARKERS: &[&str] = &[
41        "not in range",
42        "out of range",
43        "outside the range of validity",
44        "must be in range",
45        "must be between",
46        "quality must be",
47    ];
48    const INVALID_STATE_MARKERS: &[&str] =
49        &["not a valid number", "invalid state", "invalid number"];
50
51    let lowered = message.to_lowercase();
52    let context = message.to_string();
53
54    if contains_any(&lowered, UNDEFINED_MARKERS) {
55        PropertyError::Undefined { context }
56    } else if contains_any(&lowered, OUT_OF_DOMAIN_MARKERS) {
57        PropertyError::OutOfDomain { context }
58    } else if contains_any(&lowered, INVALID_STATE_MARKERS) {
59        PropertyError::InvalidState { context }
60    } else {
61        PropertyError::Calculation { context }
62    }
63}
64
65/// Returns `true` if any needle is found in the haystack.
66fn contains_any(haystack: &str, needles: &[&str]) -> bool {
67    needles.iter().any(|needle| haystack.contains(needle))
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn map_error_message_returns_undefined() {
76        let error = map_error_message("inputs are not defined for this region");
77        assert!(matches!(error, PropertyError::Undefined { .. }));
78    }
79
80    #[test]
81    fn map_error_message_returns_out_of_domain() {
82        let error = map_error_message("input 3.14 is out of range");
83        assert!(matches!(error, PropertyError::OutOfDomain { .. }));
84    }
85
86    #[test]
87    fn map_error_message_returns_invalid_state() {
88        let error = map_error_message("p is not a valid number");
89        assert!(matches!(error, PropertyError::InvalidState { .. }));
90    }
91
92    #[test]
93    fn map_error_message_returns_calculation_by_default() {
94        let error = map_error_message("some other failure");
95        assert!(matches!(error, PropertyError::Calculation { .. }));
96    }
97}