pub mod elements;
pub mod props;
use crate::error::InvalidArgument;
use crate::TimestampExt;
use crate::{
error::{get_rrd_error, RrdError, RrdResult},
ops::{
graph::{
elements::GraphElement,
props::{GraphProps, ImageFormat},
},
info::{self, InfoValue},
},
util::ArrayOfStrings,
Timestamp,
};
use log::debug;
use nom::Parser;
use nom::{bytes, character::complete, combinator, sequence, Finish};
use std::{collections, ffi::CString, fmt::Write as _};
pub fn graph(
image_format: ImageFormat,
props: &GraphProps,
elements: &[GraphElement],
) -> RrdResult<(Vec<u8>, GraphMetadata)> {
let mut args = vec!["graphv".to_string(), "-".to_string()];
args.extend(graph_args(Some(image_format), props, elements)?);
debug!("Graph: args={args:?}");
let args = args
.into_iter()
.map(CString::new)
.collect::<Result<ArrayOfStrings, _>>()?;
let argc = args.len().try_into().map_err(|_| {
RrdError::InvalidArgument("too many graph arguments for librrd".to_string())
})?;
let info_ptr = unsafe {
rrd_sys::rrd_graph_v(
argc,
args.as_ptr().cast(),
)
};
if info_ptr.is_null() {
return Err(get_rrd_error().unwrap_or_else(|| {
RrdError::Internal("No graph data produced, but no librrd error".to_string())
}));
}
let mut info = info::build_info_map(info_ptr)?;
let image = extract_info_value(&mut info, "image", info::InfoValue::into_blob)?;
debug!("Graph output: {info:?}");
let graph_left = extract_info_value(&mut info, "graph_left", info::InfoValue::into_count)?;
let graph_top = extract_info_value(&mut info, "graph_top", info::InfoValue::into_count)?;
let graph_width = extract_info_value(&mut info, "graph_width", info::InfoValue::into_count)?;
let graph_height = extract_info_value(&mut info, "graph_height", info::InfoValue::into_count)?;
let image_width = extract_info_value(&mut info, "image_width", info::InfoValue::into_count)?;
let image_height = extract_info_value(&mut info, "image_height", info::InfoValue::into_count)?;
let graph_start = timestamp_from_count(extract_info_value(
&mut info,
"graph_start",
info::InfoValue::into_count,
)?)?;
let graph_end = timestamp_from_count(extract_info_value(
&mut info,
"graph_end",
info::InfoValue::into_count,
)?)?;
let value_min = extract_info_value(&mut info, "value_min", info::InfoValue::into_value)?;
let value_max = extract_info_value(&mut info, "value_max", info::InfoValue::into_value)?;
Ok((
image,
GraphMetadata {
graph_left,
graph_top,
graph_width,
graph_height,
graph_start,
graph_end,
image_width,
image_height,
value_min,
value_max,
extra_info: info,
},
))
}
pub fn graph_args(
image_format: Option<ImageFormat>,
props: &GraphProps,
elements: &[GraphElement],
) -> RrdResult<Vec<String>> {
if !elements.iter().any(|c| matches!(c, GraphElement::Def(_))) {
return Err(RrdError::InvalidArgument(
"Must have at least one Def element".to_string(),
));
}
if !elements.iter().any(|c| {
matches!(
c,
GraphElement::Print(_)
| GraphElement::GPrint(_)
| GraphElement::Line(_)
| GraphElement::Area(_)
)
}) {
return Err(RrdError::InvalidArgument(
"Must have at least one Line, Area, GPrint, or Print element".to_string(),
));
}
let mut args = Vec::new();
if let Some(image_format) = image_format {
image_format.append_to(&mut args)?;
}
props.append_to(&mut args)?;
for c in elements {
c.append_to(&mut args)?;
}
Ok(args)
}
#[derive(Clone, Debug, PartialEq)]
pub struct GraphMetadata {
pub graph_left: u64,
pub graph_top: u64,
pub graph_width: u64,
pub graph_height: u64,
pub graph_start: Timestamp,
pub graph_end: Timestamp,
pub image_width: u64,
pub image_height: u64,
pub value_min: f64,
pub value_max: f64,
pub extra_info: collections::HashMap<String, InfoValue>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(missing_docs)]
pub struct Color {
pub red: u8,
pub green: u8,
pub blue: u8,
pub alpha: Option<u8>,
}
impl Color {
fn append_to(self, s: &mut String) {
match self.alpha {
None => write!(s, "#{:02X}{:02X}{:02X}", self.red, self.green, self.blue,),
Some(alpha) => write!(
s,
"#{:02X}{:02X}{:02X}{:02X}",
self.red, self.green, self.blue, alpha
),
}
.unwrap();
}
}
impl std::str::FromStr for Color {
type Err = InvalidArgument;
fn from_str(s: &str) -> Result<Self, Self::Err> {
combinator::map(
combinator::all_consuming(sequence::preceded(
bytes::complete::tag("#"),
(
parse_hex_byte,
parse_hex_byte,
parse_hex_byte,
combinator::opt(parse_hex_byte),
),
)),
|(red, green, blue, alpha)| Color {
red,
green,
blue,
alpha,
},
)
.parse_complete(s)
.finish()
.map_err(|_| InvalidArgument("Invalid color"))
.map(|(_rem, c)| c)
}
}
trait AppendArgs {
fn append_to(&self, args: &mut Vec<String>) -> RrdResult<()>;
}
fn extract_info_value<T>(
info: &mut collections::HashMap<String, InfoValue>,
key: &str,
transform: impl FnOnce(InfoValue) -> Option<T>,
) -> RrdResult<T> {
let value = info
.remove(key)
.ok_or_else(|| RrdError::Internal(format!("Graph info: no {key}")))?;
transform(value)
.ok_or_else(|| RrdError::Internal(format!("Graph info: unexpected {key} value type")))
}
fn timestamp_from_count(count: u64) -> RrdResult<Timestamp> {
let time_t = count
.try_into()
.map_err(|_| RrdError::Internal(format!("Graph timestamp {count} overflows time_t")))?;
Timestamp::try_from_time_t(time_t)
}
fn parse_hex_byte(input: &str) -> nom::IResult<&str, u8> {
combinator::map_opt(
sequence::pair(complete::anychar, complete::anychar),
|(hi, lo)| {
let hi = u8::try_from(hi.to_digit(16)?).ok()?;
let lo = u8::try_from(lo.to_digit(16)?).ok()?;
Some((hi << 4) | lo)
},
)
.parse_complete(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_color_no_alpha() {
assert_eq!(
Color {
red: 0x01,
green: 0x23,
blue: 0x45,
alpha: None,
},
"#012345".parse().unwrap()
)
}
#[test]
fn parse_color_with_alpha() {
assert_eq!(
Color {
red: 0x01,
green: 0x23,
blue: 0x45,
alpha: Some(0x67),
},
"#01234567".parse().unwrap()
)
}
#[test]
fn parse_color_err_invalid_hex() {
assert!("#0000ZZ".parse::<Color>().is_err());
}
#[test]
fn parse_color_err_no_prefix() {
assert!("FFFFFF".parse::<Color>().is_err());
}
#[test]
fn parse_color_err_wrong_length() {
assert!("#FFFFF".parse::<Color>().is_err());
assert!("#FFFFFFF".parse::<Color>().is_err());
assert!("#FFFFFFFFF".parse::<Color>().is_err());
}
#[test]
fn cgi_template() {
let var_name: elements::VarName = "myspeed".try_into().unwrap();
let rrd_path = std::path::PathBuf::from("data.rrd");
let args = graph_args(
Some(props::ImageFormat::Png),
&props::GraphProps::default(),
&[
elements::Def {
var_name: var_name.clone(),
rrd: rrd_path,
ds_name: "speed".to_string(),
consolidation_fn: crate::ConsolidationFn::Avg,
step: None,
start: None,
end: None,
reduce: None,
}
.into(),
elements::Line {
width: 2.0,
value: var_name,
color: Some(elements::ColorWithLegend {
color: Color {
red: 0xff,
green: 0x00,
blue: 0x00,
alpha: None,
},
legend: None,
}),
stack: false,
skip_scale: false,
dashes: None,
}
.into(),
],
)
.unwrap();
let expected_args = vec![
"--imgformat".to_string(),
"PNG".to_string(),
"DEF:myspeed=data.rrd:speed:AVERAGE".to_string(),
"LINE2:myspeed#FF0000".to_string(),
];
assert_eq!(args, expected_args);
}
}