use bacnet_client::log_reader::LogCursor;
use bacnet_services::read_range::RangeSpec;
use bacnet_types::calendar::SpecificDate;
use bacnet_types::primitives::{Date, Time};
const DEFAULT_COUNT: i16 = 100;
#[derive(Debug, Clone, Default, PartialEq, Eq, clap::Args)]
pub struct RangeOptions {
#[arg(long, conflicts_with_all = ["sequence", "time"])]
pub position: Option<u64>,
#[arg(long, conflicts_with = "time")]
pub sequence: Option<u64>,
#[arg(long, value_parser = parse_reference_time)]
pub time: Option<(Date, Time)>,
#[arg(long, allow_negative_numbers = true)]
pub count: Option<i16>,
#[arg(long)]
pub all: bool,
#[arg(long, conflicts_with = "all")]
pub strict: bool,
}
impl RangeOptions {
fn count(&self) -> Result<i16, String> {
match self.count.unwrap_or(DEFAULT_COUNT) {
0 => Err("--count may not be zero".into()),
count => Ok(count),
}
}
pub fn spec(&self) -> Result<Option<RangeSpec>, String> {
let count = || self.count().map(i32::from);
Ok(match (self.position, self.sequence, self.time) {
(Some(reference_index), _, _) => Some(RangeSpec::ByPosition {
reference_index,
count: count()?,
}),
(_, Some(reference_seq), _) => Some(RangeSpec::BySequenceNumber {
reference_seq,
count: count()?,
}),
(_, _, Some(reference_time)) => Some(RangeSpec::ByTime {
reference_time,
count: count()?,
}),
(None, None, None) if self.count.is_some() => {
return Err("--count needs --position, --sequence, --time or --all".into())
}
(None, None, None) => None,
})
}
pub fn pages(&self) -> Result<(LogCursor, u16), String> {
let cursor = match (self.position, self.sequence, self.time) {
(Some(position), _, _) => LogCursor::Position(position),
(_, Some(sequence), _) => LogCursor::Sequence(sequence),
(_, _, Some((date, time))) => LogCursor::Time(date, time),
(None, None, None) => LogCursor::Oldest,
};
let page_size = u16::try_from(self.count()?)
.map_err(|_| "--all reads forward: --count must be positive".to_string())?;
Ok((cursor, page_size))
}
pub fn from_words(words: &[String]) -> Result<(Self, Vec<String>), String> {
let mut options = Self::default();
let mut rest = Vec::new();
let mut words = words.iter();
while let Some(word) = words.next() {
let mut value = |flag: &str| {
words
.next()
.cloned()
.ok_or_else(|| format!("{flag} needs a value"))
};
let reference = |flag: &str, text: String| {
text.parse::<u64>()
.map_err(|_| format!("{flag} needs a number, got '{text}'"))
};
match word.as_str() {
"--position" => {
options.position = Some(reference("--position", value("--position")?)?)
}
"--sequence" => {
options.sequence = Some(reference("--sequence", value("--sequence")?)?)
}
"--time" => options.time = Some(parse_reference_time(&value("--time")?)?),
"--count" => {
let text = value("--count")?;
options.count = Some(
text.parse()
.map_err(|_| format!("--count needs -32768..=32767, got '{text}'"))?,
);
}
"--all" => options.all = true,
"--strict" => options.strict = true,
flag if flag.starts_with("--") => return Err(format!("unknown option {flag}")),
_ => rest.push(word.clone()),
}
}
let starts = [
options.position.is_some(),
options.sequence.is_some(),
options.time.is_some(),
];
if starts.iter().filter(|start| **start).count() > 1 {
return Err("give one of --position, --sequence and --time".into());
}
if options.strict && options.all {
return Err("--strict takes one range; --all reads leniently".into());
}
Ok((options, rest))
}
}
fn number<T: std::str::FromStr>(text: &str, what: &str) -> Result<T, String> {
text.parse()
.map_err(|_| format!("invalid {what} '{text}' in --time"))
}
pub fn parse_reference_time(text: &str) -> Result<(Date, Time), String> {
let usage = || format!("--time must be YYYY-MM-DDTHH:MM[:SS[.hh]], got '{text}'");
let (date, time) = text.split_once(['T', ' ']).ok_or_else(usage)?;
let [year, month, day] =
<[&str; 3]>::try_from(date.split('-').collect::<Vec<_>>()).map_err(|_| usage())?;
let day = SpecificDate::new(
number(year, "year")?,
number(month, "month")?,
number(day, "day")?,
)
.ok_or_else(|| format!("--time date '{date}' is not a day in 1900..=2154"))?;
let (clock, hundredths) = match time.split_once('.') {
Some((clock, fraction)) if fraction.len() == 2 => (clock, number(fraction, "hundredths")?),
Some(_) => return Err(usage()),
None => (time, 0),
};
let parts: Vec<&str> = clock.split(':').collect();
let (hour, minute, second) = match parts.as_slice() {
[hour, minute] => (number(hour, "hour")?, number(minute, "minute")?, 0),
[hour, minute, second] => (
number(hour, "hour")?,
number(minute, "minute")?,
number(second, "second")?,
),
_ => return Err(usage()),
};
let time = Time {
hour,
minute,
second,
hundredths,
};
if !time.is_specific() {
return Err(format!("--time '{text}' is not a time of day"));
}
Ok((day.to_date(), time))
}
#[cfg(test)]
#[path = "read_range_options_tests.rs"]
mod tests;