use std::collections::BTreeMap;
use rs_teststand::{Error, PropValType, PropertyObject, PropertyRepresentation};
const INSERT_IF_MISSING: i32 = 1;
const NO_OPTIONS: i32 = 0;
const RADIX_PREFIXES: [(&str, u32); 3] = [("0x", 16), ("0b", 2), ("0c", 8)];
const RADIX_CONVERSIONS: [char; 4] = ['x', 'X', 'o', 'b'];
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum PropertyValue {
Null,
Bool(bool),
Integer(i64),
Unsigned(u64),
Number(f64),
Text(String),
Array(Vec<Self>),
Container(BTreeMap<String, Self>),
}
impl PropertyValue {
fn creation_type(&self) -> PropValType {
match self {
Self::Bool(_) => PropValType::Boolean,
Self::Null | Self::Integer(_) | Self::Unsigned(_) | Self::Number(_) => {
PropValType::Number
}
Self::Text(_) => PropValType::String,
Self::Container(_) => PropValType::Container,
Self::Array(items) => items
.first()
.map_or(PropValType::Container, Self::creation_type),
}
}
}
fn column_major_offset(lengths: &[i32], indices: &[i32]) -> i32 {
let mut offset = 0;
let mut stride = 1;
for (length, index) in lengths.iter().zip(indices.iter()) {
offset += index * stride;
stride *= *length;
}
offset
}
fn parse_radix(text: &str) -> Option<f64> {
let trimmed = text.trim();
let (negative, digits) = trimmed
.strip_prefix('-')
.map_or((false, trimmed), |rest| (true, rest));
let lowered = digits.to_ascii_lowercase();
let (radix, body) = RADIX_PREFIXES
.into_iter()
.find_map(|(prefix, radix)| lowered.strip_prefix(prefix).map(|rest| (radix, rest)))?;
let magnitude = i64::from_str_radix(body, radix).ok()?;
#[allow(clippy::cast_precision_loss, reason = "engine numbers are f64 already")]
let value = magnitude as f64;
Some(if negative { -value } else { value })
}
fn is_radix_format(format: &str) -> bool {
let mut characters = format.chars();
while let Some(character) = characters.next() {
if character != '%' {
continue;
}
for following in characters.by_ref() {
if following.is_ascii_alphabetic() {
return RADIX_CONVERSIONS.contains(&following);
}
}
}
false
}
pub trait PropertyObjectValue {
fn to_value(&self) -> Result<PropertyValue, Error>;
fn apply_value(&self, value: &PropertyValue) -> Result<(), Error>;
}
impl PropertyObjectValue for PropertyObject {
fn to_value(&self) -> Result<PropertyValue, Error> {
let property_type = self.property_type()?;
let value_type = property_type.value_type()?;
if matches!(value_type, Ok(PropValType::Array)) {
let lengths = property_type.array_dimensions()?.lengths()?;
return array_to_value(self, &lengths);
}
let sub_properties = self.get_num_sub_properties("")?;
if matches!(value_type, Ok(PropValType::Container)) || sub_properties > 0 {
let mut members = BTreeMap::new();
for index in 0..sub_properties {
let name = self.get_nth_sub_property_name("", index, NO_OPTIONS)?;
let child = self.get_property_object(&name, NO_OPTIONS)?;
members.insert(name, child.to_value()?);
}
return Ok(PropertyValue::Container(members));
}
match value_type {
Ok(PropValType::Boolean) => Ok(PropertyValue::Bool(self.get_val_bool("", NO_OPTIONS)?)),
Ok(PropValType::Number) => number_to_value(self, &property_type),
Ok(PropValType::String) => {
Ok(PropertyValue::Text(self.get_val_string("", NO_OPTIONS)?))
}
_ => Ok(PropertyValue::Text(
self.get_val_string("", NO_OPTIONS)
.or_else(|_| self.get_formatted_value("", 0, "", true, ", "))?,
)),
}
}
fn apply_value(&self, value: &PropertyValue) -> Result<(), Error> {
let PropertyValue::Container(members) = value else {
return Err(Error::UnexpectedType {
expected: "Container",
actual: "scalar or array",
});
};
for (name, member) in members {
set_member(self, name, member)?;
}
Ok(())
}
}
fn array_to_value(object: &PropertyObject, lengths: &[i32]) -> Result<PropertyValue, Error> {
match lengths {
[] => Ok(PropertyValue::Array(Vec::new())),
[single] => {
let mut items = Vec::with_capacity((*single).max(0).try_into().unwrap_or(0));
for offset in 0..*single {
items.push(
object
.get_property_object_by_offset(offset, NO_OPTIONS)?
.to_value()?,
);
}
Ok(PropertyValue::Array(items))
}
_ => {
let mut indices = vec![0_i32; lengths.len()];
nest(object, lengths, 0, &mut indices)
}
}
}
fn nest(
object: &PropertyObject,
lengths: &[i32],
depth: usize,
indices: &mut Vec<i32>,
) -> Result<PropertyValue, Error> {
let Some(&length) = lengths.get(depth) else {
return Ok(PropertyValue::Array(Vec::new()));
};
let mut items = Vec::with_capacity(length.max(0).try_into().unwrap_or(0));
for index in 0..length {
if let Some(slot) = indices.get_mut(depth) {
*slot = index;
}
if depth + 1 == lengths.len() {
let offset = column_major_offset(lengths, indices);
items.push(
object
.get_property_object_by_offset(offset, NO_OPTIONS)?
.to_value()?,
);
} else {
items.push(nest(object, lengths, depth + 1, indices)?);
}
}
Ok(PropertyValue::Array(items))
}
fn number_to_value(
object: &PropertyObject,
property_type: &rs_teststand::PropertyObjectType,
) -> Result<PropertyValue, Error> {
match property_type.representation()? {
Ok(PropertyRepresentation::Int64) => {
return Ok(PropertyValue::Integer(
object.get_val_integer64("", NO_OPTIONS)?,
));
}
Ok(PropertyRepresentation::UInt64) => {
return Ok(PropertyValue::Unsigned(
object.get_val_unsigned_integer64("", NO_OPTIONS)?,
));
}
_ => {}
}
let number = object.get_val_number("", NO_OPTIONS)?;
if !number.is_finite() {
return Ok(PropertyValue::Null);
}
if is_radix_format(&object.numeric_format()?) {
return Ok(PropertyValue::Text(
object.get_formatted_value("", NO_OPTIONS, "", true, "")?,
));
}
Ok(PropertyValue::Number(number))
}
fn set_member(object: &PropertyObject, name: &str, value: &PropertyValue) -> Result<(), Error> {
match value {
PropertyValue::Null => object.set_val_number(name, INSERT_IF_MISSING, f64::NAN),
PropertyValue::Bool(flag) => object.set_val_bool(name, INSERT_IF_MISSING, *flag),
PropertyValue::Number(number) => object.set_val_number(name, INSERT_IF_MISSING, *number),
PropertyValue::Integer(number) => {
object.set_val_integer64(name, INSERT_IF_MISSING, *number)
}
PropertyValue::Unsigned(number) => {
object.set_val_unsigned_integer64(name, INSERT_IF_MISSING, *number)
}
PropertyValue::Text(text) => parse_radix(text).map_or_else(
|| object.set_val_string(name, INSERT_IF_MISSING, text),
|number| object.set_val_number(name, INSERT_IF_MISSING, number),
),
PropertyValue::Container(_) => {
if !object.exists(name, NO_OPTIONS)? {
object.new_sub_property(
name,
PropValType::Container,
false,
"",
INSERT_IF_MISSING,
)?;
}
object
.get_property_object(name, NO_OPTIONS)?
.apply_value(value)
}
PropertyValue::Array(items) => set_array_member(object, name, items),
}
}
fn set_array_member(
object: &PropertyObject,
name: &str,
items: &[PropertyValue],
) -> Result<(), Error> {
let element_type = items
.first()
.map_or(PropValType::Container, PropertyValue::creation_type);
if !object.exists(name, NO_OPTIONS)? {
object.new_sub_property(name, element_type, true, "", INSERT_IF_MISSING)?;
}
let array = object.get_property_object(name, NO_OPTIONS)?;
let count = i32::try_from(items.len()).map_err(|_| Error::UnexpectedType {
expected: "an array length within i32",
actual: "a longer array",
})?;
array.set_num_elements(count, NO_OPTIONS)?;
for (offset, item) in items.iter().enumerate() {
let offset = i32::try_from(offset).unwrap_or(i32::MAX);
let element = array.get_property_object_by_offset(offset, NO_OPTIONS)?;
match item {
PropertyValue::Container(_) => element.apply_value(item)?,
PropertyValue::Bool(flag) => element.set_val_bool("", NO_OPTIONS, *flag)?,
PropertyValue::Number(number) => element.set_val_number("", NO_OPTIONS, *number)?,
PropertyValue::Integer(number) => {
element.set_val_integer64("", NO_OPTIONS, *number)?;
}
PropertyValue::Unsigned(number) => {
element.set_val_unsigned_integer64("", NO_OPTIONS, *number)?;
}
PropertyValue::Null => element.set_val_number("", NO_OPTIONS, f64::NAN)?,
PropertyValue::Text(text) => match parse_radix(text) {
Some(number) => element.set_val_number("", NO_OPTIONS, number)?,
None => element.set_val_string("", NO_OPTIONS, text)?,
},
PropertyValue::Array(_) => {
return Err(Error::UnexpectedType {
expected: "a scalar or container array element",
actual: "a nested array",
});
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::PropertyValue;
type JsonResult<T> = Result<T, serde_json::Error>;
fn json(value: &PropertyValue) -> JsonResult<String> {
serde_json::to_string(value)
}
fn parse(text: &str) -> JsonResult<PropertyValue> {
serde_json::from_str(text)
}
#[test]
fn scalars_serialise_as_plain_json() -> JsonResult<()> {
assert_eq!(json(&PropertyValue::Bool(true))?, "true");
assert_eq!(
json(&PropertyValue::Text("SN-001".to_owned()))?,
"\"SN-001\""
);
assert_eq!(json(&PropertyValue::Number(1.5))?, "1.5");
assert_eq!(json(&PropertyValue::Integer(-7))?, "-7");
Ok(())
}
#[test]
fn an_integral_number_parses_as_integer_not_float() -> JsonResult<()> {
assert_eq!(parse("42")?, PropertyValue::Integer(42));
assert_eq!(parse("-42")?, PropertyValue::Integer(-42));
Ok(())
}
#[test]
fn a_value_beyond_i64_falls_through_to_unsigned() -> JsonResult<()> {
assert_eq!(
parse("18446744073709551615")?,
PropertyValue::Unsigned(u64::MAX)
);
Ok(())
}
#[test]
fn a_fractional_number_reaches_the_float_variant() -> JsonResult<()> {
assert_eq!(parse("1.5")?, PropertyValue::Number(1.5));
Ok(())
}
#[test]
fn the_64bit_extremes_survive_a_json_round_trip() -> JsonResult<()> {
for value in [
PropertyValue::Integer(i64::MIN),
PropertyValue::Integer(i64::MAX),
PropertyValue::Unsigned(u64::MAX),
] {
assert_eq!(parse(&json(&value)?)?, value, "lost {value:?}");
}
Ok(())
}
#[test]
fn a_container_round_trips_with_stable_key_order() -> JsonResult<()> {
let mut members = BTreeMap::new();
members.insert("Zebra".to_owned(), PropertyValue::Integer(1));
members.insert("Alpha".to_owned(), PropertyValue::Bool(false));
let value = PropertyValue::Container(members);
assert_eq!(json(&value)?, r#"{"Alpha":false,"Zebra":1}"#);
assert_eq!(parse(&json(&value)?)?, value);
Ok(())
}
#[test]
fn nested_arrays_and_containers_round_trip() -> JsonResult<()> {
let mut inner = BTreeMap::new();
inner.insert("Mode".to_owned(), PropertyValue::Text("Voltage".to_owned()));
let mut outer = BTreeMap::new();
outer.insert(
"Readings".to_owned(),
PropertyValue::Array(vec![PropertyValue::Number(1.5), PropertyValue::Number(2.5)]),
);
outer.insert("Instrument".to_owned(), PropertyValue::Container(inner));
let value = PropertyValue::Container(outer);
assert_eq!(parse(&json(&value)?)?, value);
Ok(())
}
}
#[cfg(test)]
mod representation_tests {
use super::PropertyValue;
#[test]
fn json_collapses_unsigned_into_signed_where_the_value_fits() -> Result<(), serde_json::Error> {
let unsigned = PropertyValue::Unsigned(0);
let text = serde_json::to_string(&unsigned)?;
let parsed: PropertyValue = serde_json::from_str(&text)?;
assert_eq!(parsed, PropertyValue::Integer(0));
assert_ne!(parsed, unsigned);
Ok(())
}
#[test]
fn a_value_above_i64_max_keeps_its_unsigned_identity() -> Result<(), serde_json::Error> {
let unsigned = PropertyValue::Unsigned(u64::MAX);
let parsed: PropertyValue = serde_json::from_str(&serde_json::to_string(&unsigned)?)?;
assert_eq!(parsed, unsigned);
Ok(())
}
}