mod datetime;
mod json;
mod numeric;
mod typed;
pub use datetime::{parse_date, parse_date_time, parse_time, parse_timestamp};
pub use typed::{parse_value_for, parse_value_for_with_priority};
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
#[cfg(test)]
mod datetime_tests;
#[cfg(test)]
mod json_tests;
#[cfg(test)]
mod typed_more_tests;
#[cfg(test)]
mod typed_tests;
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:") {
return parse_date(rest).map(PropertyValue::Date);
}
if let Some(rest) = s.strip_prefix("time:") {
return parse_time(rest).map(PropertyValue::Time);
}
if s.eq_ignore_ascii_case("any time") || datetime::looks_like_time(s) {
return parse_time(s).map(PropertyValue::Time);
}
let lower = s.to_ascii_lowercase();
if datetime::looks_like_date(s) || lower.starts_with("any date") || lower == "unspecified" {
return match parse_date_time(s)? {
(date, None) => Ok(PropertyValue::Date(date)),
(date, Some(time)) => Ok(PropertyValue::List(vec![
PropertyValue::Date(date),
PropertyValue::Time(time),
])),
};
}
if let Some(bits) = s.strip_prefix("bits(").and_then(|b| b.strip_suffix(')')) {
if !bits.bytes().all(|b| b == b'0' || b == b'1') {
return Err(format!(
"invalid bits '{bits}': 0/1 digits, first bit first"
));
}
let set: Vec<bool> = bits.bytes().map(|b| b == b'1').collect();
return Ok(typed::bit_string(&set));
}
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 numeric::finite(PropertyValue::Real(val)).map_err(|e| format!("'{s}': {e}"));
}
if (s.contains('e') || s.contains('E'))
&& !s.eq_ignore_ascii_case("enumerated")
&& !s.starts_with("enumerated:")
&& let Ok(val) = s.parse::<f32>()
{
return numeric::finite(PropertyValue::Real(val)).map_err(|e| format!("'{s}': {e}"));
}
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}'"))
}
fn split_priority(s: &str) -> Result<(&str, 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('@')
};
let Some(pos) = at_pos else {
return Ok((s, None));
};
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}"));
}
Ok((&s[..pos], Some(priority)))
}
pub fn parse_value_with_priority(s: &str) -> Result<(PropertyValue, Option<u8>), String> {
let (value, priority) = split_priority(s)?;
Ok((parse_value(value)?, priority))
}
#[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));
}
}