use crate::Error;
use crate::property::{PropertyObject, PropertyOptions};
const STEP_NAME: &str = "TS.StepName";
const STEP_TYPE: &str = "TS.StepType";
const INDEX: &str = "TS.Index";
const BLOCK_LEVEL: &str = "TS.BlockLevel";
const TOTAL_TIME: &str = "TS.TotalTime";
const REPORT_TEXT: &str = "ReportText";
const ERROR_OCCURRED: &str = "Error.Occurred";
const ERROR_CODE: &str = "Error.Code";
const ERROR_MESSAGE: &str = "Error.Msg";
const STATUS: &str = "Status";
const NUMERIC: &str = "Numeric";
const STRING: &str = "String";
const UNITS: &str = "Units";
const LIMIT_LOW: &str = "RawLimits.Low";
const LIMIT_HIGH: &str = "RawLimits.High";
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct StepResult {
pub name: String,
pub step_type: String,
pub status: String,
pub value: Option<ResultValue>,
pub index: i32,
pub block_level: i32,
pub total_time: f64,
pub report_text: String,
pub error: ResultError,
pub units: Option<String>,
pub limits: Option<Limits>,
}
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct ResultError {
pub occurred: bool,
pub code: i32,
pub message: String,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Limits {
pub low: f64,
pub high: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ResultValue {
Number(f64),
Text(String),
}
#[derive(Debug)]
pub struct ResultList {
results: PropertyObject,
}
impl ResultList {
pub fn from_result_object(result_object: &PropertyObject) -> Result<Self, Error> {
let none = PropertyOptions::NONE.bits();
if !result_object.exists("ResultList", none)? {
return Err(Error::UnexpectedType {
expected: "an object carrying a ResultList",
actual: "no ResultList property",
});
}
Ok(Self {
results: result_object.get_property_object("ResultList", none)?,
})
}
pub fn len(&self) -> Result<i32, Error> {
self.results.get_num_elements()
}
pub fn is_empty(&self) -> Result<bool, Error> {
Ok(self.len()? == 0)
}
pub fn parse(&self) -> Result<Vec<StepResult>, Error> {
self.parse_from(0)
}
pub fn parse_from(&self, start: i32) -> Result<Vec<StepResult>, Error> {
let none = PropertyOptions::NONE.bits();
let mut parsed = Vec::new();
for index in start.max(0)..self.len()? {
let entry = self.results.get_property_object_by_offset(index, none)?;
let text = |path: &str| entry.get_val_string(path, none).unwrap_or_default();
let value = entry
.get_val_number(NUMERIC, none)
.ok()
.map(ResultValue::Number)
.or_else(|| {
entry
.get_val_string(STRING, none)
.ok()
.filter(|found| !found.is_empty())
.map(ResultValue::Text)
});
let limits = match (
entry.get_val_number(LIMIT_LOW, none),
entry.get_val_number(LIMIT_HIGH, none),
) {
(Ok(low), Ok(high)) => Some(Limits { low, high }),
_ => None,
};
#[expect(
clippy::cast_possible_truncation,
reason = "counts are small whole numbers and the cast saturates"
)]
let count = |path: &str| {
entry
.get_val_number(path, none)
.ok()
.map_or(0, |found| found as i32)
};
parsed.push(StepResult {
name: text(STEP_NAME),
step_type: text(STEP_TYPE),
status: text(STATUS),
value,
index: count(INDEX),
block_level: count(BLOCK_LEVEL),
total_time: entry.get_val_number(TOTAL_TIME, none).unwrap_or_default(),
report_text: text(REPORT_TEXT),
error: ResultError {
occurred: entry
.get_val_boolean(ERROR_OCCURRED, none)
.unwrap_or_default(),
code: count(ERROR_CODE),
message: text(ERROR_MESSAGE),
},
units: entry
.get_val_string(UNITS, none)
.ok()
.filter(|found| !found.is_empty()),
limits,
});
}
Ok(parsed)
}
#[must_use]
pub const fn as_property_object(&self) -> &PropertyObject {
&self.results
}
}
#[cfg(test)]
mod tests {
use super::{ResultError, ResultValue, StepResult};
#[test]
fn a_result_without_a_measurement_is_distinguishable_from_a_zero() {
let action = StepResult {
name: "Initialize".to_owned(),
step_type: "Action".to_owned(),
status: "Done".to_owned(),
value: None,
index: 0,
block_level: 0,
total_time: 0.0,
report_text: String::new(),
error: ResultError::default(),
units: None,
limits: None,
};
let measured = StepResult {
value: Some(ResultValue::Number(0.0)),
..action.clone()
};
assert_ne!(action.value, measured.value);
assert!(action.value.is_none());
}
#[test]
fn a_text_measurement_is_kept_as_text() {
assert_eq!(
ResultValue::Text("SN-001".to_owned()),
ResultValue::Text("SN-001".to_owned())
);
assert_ne!(
ResultValue::Text("1.5".to_owned()),
ResultValue::Number(1.5)
);
}
}