use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, Time};
const OBJ_ABBREVS: &[(&str, u32)] = &[
("ai", 0), ("ao", 1), ("av", 2), ("bi", 3), ("bo", 4), ("bv", 5), ("dev", 8), ("msi", 13), ("mso", 14), ("msv", 19), ("lp", 21), ("lsp", 21), ("sc", 17), ("cal", 6), ("nc", 15), ("trn", 20), ("acc", 23), ("pi", 24), ("lo", 12), ("prg", 16), ("cmd", 7), ];
const PROP_ABBREVS: &[(&str, u32)] = &[
("pv", 85), ("on", 77), ("ot", 79), ("desc", 28), ("sf", 111), ("es", 36), ("oos", 81), ("pa", 87), ("rd", 104), ("ol", 76), ("all", 8), ];
pub fn parse_object_type(s: &str) -> Result<ObjectType, String> {
let s = s.trim();
if let Ok(n) = s.parse::<u32>() {
return Ok(ObjectType::from_raw(n));
}
let lower = s.to_ascii_lowercase();
for &(abbr, raw) in OBJ_ABBREVS {
if lower == abbr {
return Ok(ObjectType::from_raw(raw));
}
}
let normalized = lower.replace('-', "_");
for &(name, val) in ObjectType::ALL_NAMED {
if name.eq_ignore_ascii_case(&normalized) {
return Ok(val);
}
}
Err(format!("unknown object type: '{s}'"))
}
pub fn parse_object_specifier(s: &str) -> Result<(ObjectType, u32), String> {
let (type_str, inst_str) = s
.rsplit_once(':')
.ok_or_else(|| format!("expected 'type:instance' format, got '{s}'"))?;
let obj_type = parse_object_type(type_str)?;
let instance = inst_str
.parse::<u32>()
.map_err(|_| format!("invalid instance number: '{inst_str}'"))?;
Ok((obj_type, instance))
}
pub fn parse_property(s: &str) -> Result<(PropertyIdentifier, Option<u32>), String> {
let s = s.trim();
let (prop_str, array_index) = if let Some(bracket_pos) = s.find('[') {
let end = s
.find(']')
.ok_or_else(|| format!("missing closing ']' in '{s}'"))?;
if end <= bracket_pos {
return Err(format!("malformed array index in '{s}'"));
}
let idx_str = &s[bracket_pos + 1..end];
let idx = idx_str
.parse::<u32>()
.map_err(|_| format!("invalid array index: '{idx_str}'"))?;
(&s[..bracket_pos], Some(idx))
} else {
(s, None)
};
let prop = parse_property_name(prop_str)?;
Ok((prop, array_index))
}
fn parse_property_name(s: &str) -> Result<PropertyIdentifier, String> {
if let Ok(n) = s.parse::<u32>() {
return Ok(PropertyIdentifier::from_raw(n));
}
let lower = s.to_ascii_lowercase();
for &(abbr, raw) in PROP_ABBREVS {
if lower == abbr {
return Ok(PropertyIdentifier::from_raw(raw));
}
}
let normalized = lower.replace('-', "_");
for &(name, val) in PropertyIdentifier::ALL_NAMED {
if name.eq_ignore_ascii_case(&normalized) {
return Ok(val);
}
}
Err(format!("unknown property: '{s}'"))
}
pub fn parse_value(s: &str) -> Result<PropertyValue, String> {
let s = s.trim();
if s.eq_ignore_ascii_case("null") {
return Ok(PropertyValue::Null);
}
if s.eq_ignore_ascii_case("true") {
return Ok(PropertyValue::Boolean(true));
}
if s.eq_ignore_ascii_case("false") {
return Ok(PropertyValue::Boolean(false));
}
if s.eq_ignore_ascii_case("active") {
return Ok(PropertyValue::Enumerated(1));
}
if s.eq_ignore_ascii_case("inactive") {
return Ok(PropertyValue::Enumerated(0));
}
if let Some(rest) = s.strip_prefix("date:") {
let parts: Vec<&str> = rest.split('-').collect();
if parts.len() != 3 {
return Err(format!(
"invalid date format: '{rest}', expected YYYY-MM-DD"
));
}
let year = if parts[0] == "*" {
0xFF
} else {
let y = parts[0]
.parse::<u16>()
.map_err(|_| format!("invalid year: '{}'", parts[0]))?;
if y < 1900 {
return Err(format!("year must be >= 1900, got {y}"));
}
(y - 1900) as u8
};
let month = if parts[1] == "*" {
0xFF
} else {
parts[1]
.parse::<u8>()
.map_err(|_| format!("invalid month: '{}'", parts[1]))?
};
let day = if parts[2] == "*" {
0xFF
} else {
parts[2]
.parse::<u8>()
.map_err(|_| format!("invalid day: '{}'", parts[2]))?
};
return Ok(PropertyValue::Date(Date {
year,
month,
day,
day_of_week: 0xFF,
}));
}
if let Some(rest) = s.strip_prefix("time:") {
let (time_part, hundredths_part) = if let Some((tp, hp)) = rest.rsplit_once('.') {
(tp, Some(hp))
} else {
(rest, None)
};
let parts: Vec<&str> = time_part.split(':').collect();
if parts.len() != 3 {
return Err(format!(
"invalid time format: '{rest}', expected HH:MM:SS or HH:MM:SS.hh"
));
}
let hour = if parts[0] == "*" {
0xFF
} else {
parts[0]
.parse::<u8>()
.map_err(|_| format!("invalid hour: '{}'", parts[0]))?
};
let minute = if parts[1] == "*" {
0xFF
} else {
parts[1]
.parse::<u8>()
.map_err(|_| format!("invalid minute: '{}'", parts[1]))?
};
let second = if parts[2] == "*" {
0xFF
} else {
parts[2]
.parse::<u8>()
.map_err(|_| format!("invalid second: '{}'", parts[2]))?
};
let hundredths = match hundredths_part {
Some("*") => 0xFF,
Some(h) => h
.parse::<u8>()
.map_err(|_| format!("invalid hundredths: '{h}'"))?,
None => 0,
};
return Ok(PropertyValue::Time(Time {
hour,
minute,
second,
hundredths,
}));
}
if let Some(rest) = s.strip_prefix("object:") {
let (obj_type, instance) = parse_object_specifier(rest)?;
let oid = ObjectIdentifier::new(obj_type, instance)
.map_err(|e| format!("invalid object identifier: {e}"))?;
return Ok(PropertyValue::ObjectIdentifier(oid));
}
if let Some(rest) = s.strip_prefix("enumerated:") {
let val = rest
.parse::<u32>()
.map_err(|_| format!("invalid enumerated value: '{rest}'"))?;
return Ok(PropertyValue::Enumerated(val));
}
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
let inner = &s[1..s.len() - 1];
return Ok(PropertyValue::CharacterString(inner.to_string()));
}
if s.contains('.') {
let val = s
.parse::<f32>()
.map_err(|_| format!("invalid float value: '{s}'"))?;
return Ok(PropertyValue::Real(val));
}
if (s.contains('e') || s.contains('E'))
&& !s.eq_ignore_ascii_case("enumerated")
&& !s.starts_with("enumerated:")
{
if let Ok(val) = s.parse::<f32>() {
return Ok(PropertyValue::Real(val));
}
}
if s.starts_with('-') {
let val = s
.parse::<i32>()
.map_err(|_| format!("invalid integer value: '{s}'"))?;
return Ok(PropertyValue::Signed(val));
}
if let Ok(val) = s.parse::<u64>() {
return Ok(PropertyValue::Unsigned(val));
}
Err(format!("cannot parse value: '{s}'"))
}
pub fn parse_value_with_priority(s: &str) -> Result<(PropertyValue, Option<u8>), String> {
let s = s.trim();
let at_pos = if let Some(stripped) = s.strip_prefix('"') {
if let Some(end_quote) = stripped.find('"') {
let after_quote = end_quote + 2; s[after_quote..].find('@').map(|p| p + after_quote)
} else {
None
}
} else {
s.rfind('@')
};
if let Some(pos) = at_pos {
let value_str = &s[..pos];
let priority_str = &s[pos + 1..];
let priority = priority_str
.parse::<u8>()
.map_err(|_| format!("invalid priority: '{priority_str}'"))?;
if !(1..=16).contains(&priority) {
return Err(format!("priority must be 1-16, got {priority}"));
}
let value = parse_value(value_str)?;
Ok((value, Some(priority)))
} else {
let value = parse_value(s)?;
Ok((value, None))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_object_type_full_name() {
assert_eq!(
parse_object_type("analog-input").unwrap().to_raw(),
ObjectType::ANALOG_INPUT.to_raw()
);
assert_eq!(
parse_object_type("ANALOG_INPUT").unwrap().to_raw(),
ObjectType::ANALOG_INPUT.to_raw()
);
assert_eq!(
parse_object_type("Binary-Value").unwrap().to_raw(),
ObjectType::BINARY_VALUE.to_raw()
);
}
#[test]
fn parse_object_type_abbreviation() {
assert_eq!(parse_object_type("ai").unwrap().to_raw(), 0);
assert_eq!(parse_object_type("bo").unwrap().to_raw(), 4);
assert_eq!(parse_object_type("dev").unwrap().to_raw(), 8);
assert_eq!(parse_object_type("msv").unwrap().to_raw(), 19);
assert_eq!(parse_object_type("AI").unwrap().to_raw(), 0);
}
#[test]
fn parse_object_type_numeric() {
assert_eq!(parse_object_type("0").unwrap().to_raw(), 0);
assert_eq!(parse_object_type("8").unwrap().to_raw(), 8);
assert_eq!(parse_object_type("42").unwrap().to_raw(), 42);
}
#[test]
fn parse_object_type_error() {
assert!(parse_object_type("nonexistent").is_err());
}
#[test]
fn parse_object_specifier_works() {
let (ot, inst) = parse_object_specifier("ai:1").unwrap();
assert_eq!(ot.to_raw(), 0);
assert_eq!(inst, 1);
let (ot, inst) = parse_object_specifier("analog-input:100").unwrap();
assert_eq!(ot.to_raw(), 0);
assert_eq!(inst, 100);
let (ot, inst) = parse_object_specifier("dev:1234").unwrap();
assert_eq!(ot.to_raw(), 8);
assert_eq!(inst, 1234);
}
#[test]
fn parse_object_specifier_errors() {
assert!(parse_object_specifier("ai").is_err()); assert!(parse_object_specifier("ai:abc").is_err()); }
#[test]
fn parse_property_full_name() {
let (p, idx) = parse_property("present-value").unwrap();
assert_eq!(p.to_raw(), 85);
assert_eq!(idx, None);
}
#[test]
fn parse_property_abbreviation() {
let (p, _) = parse_property("pv").unwrap();
assert_eq!(p.to_raw(), 85);
let (p, _) = parse_property("on").unwrap();
assert_eq!(p.to_raw(), 77);
let (p, _) = parse_property("all").unwrap();
assert_eq!(p.to_raw(), 8);
}
#[test]
fn parse_property_numeric() {
let (p, _) = parse_property("85").unwrap();
assert_eq!(p.to_raw(), 85);
}
#[test]
fn parse_property_array_index() {
let (p, idx) = parse_property("object-list[3]").unwrap();
assert_eq!(p.to_raw(), 76);
assert_eq!(idx, Some(3));
let (p, idx) = parse_property("ol[0]").unwrap();
assert_eq!(p.to_raw(), 76);
assert_eq!(idx, Some(0));
}
#[test]
fn parse_value_null() {
assert!(matches!(parse_value("null").unwrap(), PropertyValue::Null));
assert!(matches!(parse_value("NULL").unwrap(), PropertyValue::Null));
}
#[test]
fn parse_value_boolean() {
assert!(matches!(
parse_value("true").unwrap(),
PropertyValue::Boolean(true)
));
assert!(matches!(
parse_value("false").unwrap(),
PropertyValue::Boolean(false)
));
}
#[test]
fn parse_value_active_inactive() {
assert!(matches!(
parse_value("active").unwrap(),
PropertyValue::Enumerated(1)
));
assert!(matches!(
parse_value("inactive").unwrap(),
PropertyValue::Enumerated(0)
));
}
#[test]
fn parse_value_unsigned() {
match parse_value("42").unwrap() {
PropertyValue::Unsigned(v) => assert_eq!(v, 42),
other => panic!("expected Unsigned, got {other:?}"),
}
}
#[test]
fn parse_value_signed() {
match parse_value("-5").unwrap() {
PropertyValue::Signed(v) => assert_eq!(v, -5),
other => panic!("expected Signed, got {other:?}"),
}
}
#[test]
fn parse_value_real() {
match parse_value("72.5").unwrap() {
PropertyValue::Real(v) => assert!((v - 72.5).abs() < f32::EPSILON),
other => panic!("expected Real, got {other:?}"),
}
}
#[test]
fn parse_value_string() {
match parse_value("\"hello world\"").unwrap() {
PropertyValue::CharacterString(s) => assert_eq!(s, "hello world"),
other => panic!("expected CharacterString, got {other:?}"),
}
}
#[test]
fn parse_value_enumerated() {
match parse_value("enumerated:3").unwrap() {
PropertyValue::Enumerated(v) => assert_eq!(v, 3),
other => panic!("expected Enumerated, got {other:?}"),
}
}
#[test]
fn parse_value_with_priority_works() {
let (val, pri) = parse_value_with_priority("72.5@8").unwrap();
assert!(matches!(val, PropertyValue::Real(_)));
assert_eq!(pri, Some(8));
let (val, pri) = parse_value_with_priority("null@16").unwrap();
assert!(matches!(val, PropertyValue::Null));
assert_eq!(pri, Some(16));
let (val, pri) = parse_value_with_priority("42").unwrap();
assert!(matches!(val, PropertyValue::Unsigned(42)));
assert_eq!(pri, None);
}
#[test]
fn parse_value_with_priority_invalid() {
assert!(parse_value_with_priority("42@0").is_err()); assert!(parse_value_with_priority("42@17").is_err()); }
#[test]
fn lp_maps_to_life_safety_point() {
assert_eq!(parse_object_type("lp").unwrap().to_raw(), 21);
assert_eq!(parse_object_type("lsp").unwrap().to_raw(), 21);
assert_eq!(parse_object_type("cal").unwrap().to_raw(), 6);
}
#[test]
fn bracket_ordering_panic() {
assert!(parse_property("pv]3[").is_err());
}
#[test]
fn scientific_notation_real() {
match parse_value("1e10").unwrap() {
PropertyValue::Real(v) => assert!((v - 1e10).abs() < 1e5),
other => panic!("expected Real, got {other:?}"),
}
match parse_value("-2.5e3").unwrap() {
PropertyValue::Real(v) => assert!((v - (-2500.0)).abs() < f32::EPSILON),
other => panic!("expected Real, got {other:?}"),
}
}
#[test]
fn parse_date_value() {
match parse_value("date:2024-03-15").unwrap() {
PropertyValue::Date(d) => {
assert_eq!(d.year, 124); assert_eq!(d.month, 3);
assert_eq!(d.day, 15);
assert_eq!(d.day_of_week, 0xFF);
}
other => panic!("expected Date, got {other:?}"),
}
}
#[test]
fn parse_time_value() {
match parse_value("time:14:30:00").unwrap() {
PropertyValue::Time(t) => {
assert_eq!(t.hour, 14);
assert_eq!(t.minute, 30);
assert_eq!(t.second, 0);
assert_eq!(t.hundredths, 0);
}
other => panic!("expected Time, got {other:?}"),
}
}
#[test]
fn parse_object_identifier_value() {
match parse_value("object:ai:1").unwrap() {
PropertyValue::ObjectIdentifier(oid) => {
assert_eq!(oid.object_type().to_raw(), 0);
assert_eq!(oid.instance_number(), 1);
}
other => panic!("expected ObjectIdentifier, got {other:?}"),
}
}
#[test]
fn empty_string_with_priority() {
let (val, pri) = parse_value_with_priority("\"\"@8").unwrap();
match val {
PropertyValue::CharacterString(s) => assert_eq!(s, ""),
other => panic!("expected CharacterString, got {other:?}"),
}
assert_eq!(pri, Some(8));
}
}