1use serde::{Deserialize, Serialize};
4
5use crate::error::AppError;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum BasisRole {
11 Geometry,
13 Mesh,
15 Material,
17 LoadCase,
19 Other,
20}
21
22impl BasisRole {
23 pub const ALL: [BasisRole; 5] = [
24 BasisRole::Geometry,
25 BasisRole::Mesh,
26 BasisRole::Material,
27 BasisRole::LoadCase,
28 BasisRole::Other,
29 ];
30
31 pub fn as_str(self) -> &'static str {
32 match self {
33 BasisRole::Geometry => "geometry",
34 BasisRole::Mesh => "mesh",
35 BasisRole::Material => "material",
36 BasisRole::LoadCase => "load_case",
37 BasisRole::Other => "other",
38 }
39 }
40
41 pub fn label(self) -> &'static str {
42 match self {
43 BasisRole::Geometry => "Geometry",
44 BasisRole::Mesh => "Mesh",
45 BasisRole::Material => "Material",
46 BasisRole::LoadCase => "Load case",
47 BasisRole::Other => "Other",
48 }
49 }
50
51 pub fn parse(s: &str) -> Option<BasisRole> {
53 match s
54 .trim()
55 .to_ascii_lowercase()
56 .replace(['-', ' '], "_")
57 .as_str()
58 {
59 "geometry" => Some(BasisRole::Geometry),
60 "mesh" => Some(BasisRole::Mesh),
61 "material" => Some(BasisRole::Material),
62 "load_case" => Some(BasisRole::LoadCase),
63 "other" => Some(BasisRole::Other),
64 _ => None,
65 }
66 }
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
71#[serde(rename_all = "snake_case")]
72pub enum ResultStatus {
73 Pass,
74 Fail,
75 #[serde(rename = "none")]
77 NoLimits,
78}
79
80impl ResultStatus {
81 pub fn as_str(self) -> &'static str {
82 match self {
83 ResultStatus::Pass => "pass",
84 ResultStatus::Fail => "fail",
85 ResultStatus::NoLimits => "none",
86 }
87 }
88}
89
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum ResultFilter {
94 Failed,
96 Passed,
98 AboveLimit,
100 BelowLimit,
102}
103
104impl ResultFilter {
105 pub const ALL: [ResultFilter; 4] = [
106 ResultFilter::Failed,
107 ResultFilter::Passed,
108 ResultFilter::AboveLimit,
109 ResultFilter::BelowLimit,
110 ];
111
112 pub fn as_str(self) -> &'static str {
113 match self {
114 ResultFilter::Failed => "failed",
115 ResultFilter::Passed => "passed",
116 ResultFilter::AboveLimit => "above_limit",
117 ResultFilter::BelowLimit => "below_limit",
118 }
119 }
120
121 pub fn label(self) -> &'static str {
122 match self {
123 ResultFilter::Failed => "A value fails its limits",
124 ResultFilter::Passed => "A value passes its limits",
125 ResultFilter::AboveLimit => "A value is above its limit",
126 ResultFilter::BelowLimit => "A value is below its limit",
127 }
128 }
129
130 pub fn parse(s: &str) -> Option<ResultFilter> {
131 ResultFilter::ALL
132 .into_iter()
133 .find(|f| f.as_str() == s.trim())
134 }
135
136 pub fn parse_field(s: Option<&str>) -> Result<Option<ResultFilter>, AppError> {
138 match s.map(str::trim).filter(|s| !s.is_empty()) {
139 None => Ok(None),
140 Some(s) => ResultFilter::parse(s).map(Some).ok_or_else(|| {
141 AppError::BadRequest(format!(
142 "Unknown result filter '{s}' (failed, passed, above_limit, below_limit)."
143 ))
144 }),
145 }
146 }
147}
148
149pub fn result_status(value: f64, lower: Option<f64>, upper: Option<f64>) -> ResultStatus {
152 if lower.is_none() && upper.is_none() {
153 return ResultStatus::NoLimits;
154 }
155 let above_lower = lower.is_none_or(|l| value >= l);
156 let below_upper = upper.is_none_or(|u| value <= u);
157 if value.is_finite() && above_lower && below_upper {
158 ResultStatus::Pass
159 } else {
160 ResultStatus::Fail
161 }
162}
163
164pub fn validate_result_numbers(
166 name: &str,
167 value: f64,
168 lower: Option<f64>,
169 upper: Option<f64>,
170) -> Result<(), AppError> {
171 if !value.is_finite() {
172 return Err(AppError::BadRequest(format!(
173 "{name}: the value must be a finite number."
174 )));
175 }
176 if lower.is_some_and(|v| !v.is_finite()) || upper.is_some_and(|v| !v.is_finite()) {
177 return Err(AppError::BadRequest(format!(
178 "{name}: the limits must be finite numbers."
179 )));
180 }
181 if let (Some(l), Some(u)) = (lower, upper) {
182 if l > u {
183 return Err(AppError::BadRequest(format!(
184 "{name}: the lower limit {l} is above the upper limit {u}."
185 )));
186 }
187 }
188 Ok(())
189}
190
191#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn basis_roles() {
199 let roles: Vec<(&str, &str)> = BasisRole::ALL
200 .iter()
201 .map(|r| (r.as_str(), r.label()))
202 .collect();
203 assert_eq!(
204 roles,
205 [
206 ("geometry", "Geometry"),
207 ("mesh", "Mesh"),
208 ("material", "Material"),
209 ("load_case", "Load case"),
210 ("other", "Other")
211 ]
212 );
213 }
214
215 #[test]
216 fn result_filters() {
217 let filters: Vec<&str> = ResultFilter::ALL.iter().map(|f| f.as_str()).collect();
218 assert_eq!(filters, ["failed", "passed", "above_limit", "below_limit"]);
219 let mut labels: Vec<&str> = ResultFilter::ALL.iter().map(|f| f.label()).collect();
220 assert_eq!(labels[0], "A value fails its limits");
221 labels.sort_unstable();
222 labels.dedup();
223 assert_eq!(labels.len(), ResultFilter::ALL.len());
224 for f in ResultFilter::ALL {
225 assert_eq!(ResultFilter::parse(&format!(" {} ", f.as_str())), Some(f));
226 assert_eq!(
227 ResultFilter::parse_field(Some(f.as_str())).unwrap(),
228 Some(f)
229 );
230 }
231 assert_eq!(ResultFilter::parse_field(None).unwrap(), None);
232 assert_eq!(ResultFilter::parse_field(Some(" ")).unwrap(), None);
233 assert!(ResultFilter::parse_field(Some("broken")).is_err());
234 }
235
236 #[test]
237 fn pass_and_fail() {
238 use ResultStatus::*;
239 assert_eq!(
240 (Pass.as_str(), Fail.as_str(), NoLimits.as_str()),
241 ("pass", "fail", "none")
242 );
243 assert_eq!(result_status(5.0, None, None), NoLimits);
244 assert_eq!(result_status(1.0, Some(1.0), Some(2.0)), Pass);
246 assert_eq!(result_status(2.0, Some(1.0), Some(2.0)), Pass);
247 assert_eq!(result_status(0.99, Some(1.0), Some(2.0)), Fail);
248 assert_eq!(result_status(2.01, Some(1.0), Some(2.0)), Fail);
249 assert_eq!(result_status(-1e9, None, Some(2.0)), Pass);
250 assert_eq!(result_status(1e9, Some(1.0), None), Pass);
251 assert_eq!(result_status(f64::NAN, Some(1.0), None), Fail);
252 assert_eq!(result_status(f64::INFINITY, None, Some(1e300)), Fail);
253 }
254}