use std::{
collections::{HashMap, HashSet},
num::TryFromIntError,
};
#[cfg(feature = "clock")]
use chrono::Utc;
use chrono::{DateTime, Duration, FixedOffset};
use miette::SourceSpan;
use pest::Parser as _;
use strumbra::SharedString;
use crate::{
ParseError,
enc_regex::EncodableRegex,
linker::{AlignFunction, ComputeFunction, GroupFunction, MapFunction},
parser::{self, MPLParser, ParseParamError, Rule},
tags::TagValue,
time::{Resolution, ResolutionError},
types::{BucketSpec, BucketType, Dataset, Metric, Parameterized},
};
mod fmt;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct MetricId {
pub dataset: Parameterized<Dataset>,
pub metric: Metric,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum TimeUnit {
Millisecond,
Second,
Minute,
Hour,
Day,
Week,
Month,
Year,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct RelativeTime {
pub value: u64,
pub unit: TimeUnit,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum Time {
Relative(RelativeTime),
Timestamp(i64),
RFC3339(#[cfg_attr(feature = "wasm", tsify(type = "string"))] DateTime<FixedOffset>),
Modifier(String),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct TimeRange {
pub start: Time,
pub end: Option<Time>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Source {
pub metric_id: MetricId,
pub time: Option<TimeRange>,
}
#[derive(Debug, thiserror::Error)]
pub enum ValueError {
#[error("Invalid Float")]
BadFloat,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum Cmp {
Eq(Parameterized<TagValue>),
Ne(Parameterized<TagValue>),
Gt(Parameterized<TagValue>),
Ge(Parameterized<TagValue>),
Lt(Parameterized<TagValue>),
Le(Parameterized<TagValue>),
RegEx(Parameterized<EncodableRegex>),
RegExNot(Parameterized<EncodableRegex>),
Is(TagType),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct As {
pub name: Metric,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum Filter {
And(Vec<Filter>),
Or(Vec<Filter>),
Not(Box<Filter>),
Cmp {
field: String,
rhs: Cmp,
},
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum FilterOrIfDef {
Filter(Filter),
Ifdef {
param: ParamDeclaration,
filter: Filter,
},
}
impl FilterOrIfDef {
#[cfg(test)]
pub(crate) fn filter(&self) -> &Filter {
match self {
FilterOrIfDef::Filter(filter) | FilterOrIfDef::Ifdef { filter, .. } => filter,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Mapping {
pub function: MapFunction,
pub arg: Option<f64>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Align {
pub function: AlignFunction,
pub time: Parameterized<RelativeTime>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct GroupBy {
#[cfg_attr(feature = "wasm", tsify(type = "{ offset: number, length: number }"))]
pub span: SourceSpan,
pub function: GroupFunction,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct BucketBy {
#[cfg_attr(feature = "wasm", tsify(type = "{ offset: number, length: number }"))]
pub span: SourceSpan,
pub function: BucketType,
pub time: Parameterized<RelativeTime>,
pub tags: Vec<String>,
pub spec: Vec<BucketSpec>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum Aggregate {
Map(Mapping),
Align(Align),
GroupBy(GroupBy),
Bucket(BucketBy),
As(As),
}
#[cfg_attr(feature = "wasm", tsify::declare)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum DirectiveValue {
Ident(String),
Int(i64),
Float(f64),
String(String),
Bool(bool),
None,
}
impl DirectiveValue {
#[must_use]
pub fn as_ident(&self) -> Option<&str> {
match self {
DirectiveValue::Ident(ident) => Some(ident),
_ => None,
}
}
#[must_use]
pub fn as_int(&self) -> Option<i64> {
match self {
DirectiveValue::Int(int) => Some(*int),
_ => None,
}
}
#[must_use]
pub fn as_float(&self) -> Option<f64> {
match self {
DirectiveValue::Float(float) => Some(*float),
_ => None,
}
}
#[must_use]
pub fn as_string(&self) -> Option<&str> {
match self {
DirectiveValue::String(string) => Some(string),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
DirectiveValue::Bool(bool) => Some(*bool),
_ => None,
}
}
#[must_use]
pub fn is_none(&self) -> bool {
matches!(self, DirectiveValue::None)
}
#[must_use]
pub fn is_some(&self) -> bool {
!self.is_none()
}
}
#[cfg_attr(feature = "wasm", tsify::declare)]
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
pub enum ParamType {
Terminal(TerminalParamType),
Optional(TerminalParamType),
}
impl ParamType {
fn is_optional(self) -> bool {
matches!(self, ParamType::Optional(_))
}
}
impl std::fmt::Display for ParamType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParamType::Terminal(t) => t.fmt(f),
ParamType::Optional(t) => write!(f, "Option<{t}>"),
}
}
}
#[cfg_attr(feature = "wasm", tsify::declare)]
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
pub enum TerminalParamType {
Duration,
Dataset,
Regex,
Tag(TagType),
}
impl std::fmt::Display for TerminalParamType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TerminalParamType::Dataset => write!(f, "Dataset"),
TerminalParamType::Duration => write!(f, "Duration"),
TerminalParamType::Regex => write!(f, "Regex"),
TerminalParamType::Tag(t) => t.fmt(f),
}
}
}
#[cfg_attr(feature = "wasm", tsify::declare)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[derive(Clone, Copy, Debug, Hash, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
pub enum TagType {
String,
Int,
Float,
Bool,
Null,
}
#[cfg(feature = "bincode")]
#[test]
fn test_renaming_none_to_null_has_no_bincode_side_effects() {
let enc = [4];
assert_eq!(
(TagType::Null, 1),
bincode::decode_from_slice(&enc, bincode::config::standard()).expect("it does ...")
);
}
impl std::fmt::Display for TagType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
TagType::String => "string",
TagType::Int => "int",
TagType::Float => "float",
TagType::Bool => "bool",
TagType::Null => "null",
}
)
}
}
#[cfg_attr(feature = "wasm", tsify::declare)]
pub type Directives = HashMap<String, DirectiveValue>;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
pub struct ParamDeclaration {
#[cfg_attr(feature = "wasm", tsify(type = "{ offset: number, length: number }"))]
pub span: SourceSpan,
pub name: String,
pub typ: ParamType,
}
impl ParamDeclaration {
pub(crate) fn typ(&self) -> TerminalParamType {
match self.typ {
ParamType::Terminal(terminal_param_type) | ParamType::Optional(terminal_param_type) => {
terminal_param_type
}
}
}
pub(crate) fn is_optional(&self) -> bool {
self.typ.is_optional()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ParamValue {
Dataset(Dataset),
Duration(RelativeTime),
String(String),
Int(i64),
Float(f64),
Bool(bool),
Regex(EncodableRegex),
}
impl ParamValue {
#[must_use]
pub fn typ(&self) -> TerminalParamType {
match self {
ParamValue::Dataset(_) => TerminalParamType::Dataset,
ParamValue::Duration(_) => TerminalParamType::Duration,
ParamValue::Regex(_) => TerminalParamType::Regex,
ParamValue::String(_) => TerminalParamType::Tag(TagType::String),
ParamValue::Int(_) => TerminalParamType::Tag(TagType::Int),
ParamValue::Float(_) => TerminalParamType::Tag(TagType::Float),
ParamValue::Bool(_) => TerminalParamType::Tag(TagType::Bool),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProvidedParam {
pub name: String,
pub value: ParamValue,
}
impl ProvidedParam {
pub fn new(name: impl Into<String>, value: ParamValue) -> Self {
Self {
name: name.into(),
value,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ProvidedParams {
inner: Vec<ProvidedParam>,
}
#[derive(Debug, thiserror::Error)]
pub enum ResolveError {
#[error("Param ${0} was not provided to the query")]
ParamNotProvided(String),
#[error(
"Param ${name} is defined as `{defined}`, but was used in a context that expected one of: {}",
expected.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
)]
InvalidType {
name: String,
defined: TerminalParamType,
expected: Vec<TerminalParamType>,
},
#[error("Shared string error: {0}")]
SharedString(#[from] strumbra::Error),
}
#[derive(Debug, thiserror::Error)]
pub enum ParseProvidedParamsError {
#[error("Failed to parse the value for ${param_name} as {expected_type}: {err}")]
ParseParam {
param_name: String,
expected_type: ParamType,
err: ParseParamError,
},
#[error("These params were provided more than once: {}", .0.join(", "))]
ParamsProvidedMoreThanOnce(Vec<String>),
#[error("The following params were declared but not provided: {}", .0.join(", "))]
ParamsDeclaredButNotProvided(Vec<String>),
#[error("The number of params provided exceeds the upper limit of {0}")]
TooManyParamsProvided(usize),
}
#[derive(Debug, Default)]
pub struct Warnings {
inner: Vec<String>,
}
impl Warnings {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, warning: impl Into<String>) {
self.inner.push(warning.into());
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[must_use]
pub fn as_slice(&self) -> &[String] {
&self.inner
}
#[must_use]
pub fn into_vec(self) -> Vec<String> {
self.inner
}
}
impl ProvidedParams {
#[must_use]
pub fn new(inner: Vec<ProvidedParam>) -> Self {
Self { inner }
}
pub fn parse_and_validate(
mpl_params: &Params,
query_params: &[(String, String)],
) -> Result<(Self, Warnings), ParseProvidedParamsError> {
const PREFIX: &str = "param__";
const PARAM_COUNT_LIMIT: usize = 128;
let mut warnings = Warnings::new();
let mut defined_more_than_once = HashSet::new();
let mut provided_but_not_declared = HashSet::new();
let mut seen = HashSet::new();
let params = query_params
.iter()
.filter_map(|(name, value)| {
if !name.starts_with(PREFIX) {
return None;
}
let name = name.trim_start_matches(PREFIX);
if name.is_empty() {
return None;
}
Some((name, value))
})
.take(PARAM_COUNT_LIMIT + 1)
.collect::<Vec<(&str, &String)>>();
if params.len() > PARAM_COUNT_LIMIT {
return Err(ParseProvidedParamsError::TooManyParamsProvided(
PARAM_COUNT_LIMIT,
));
}
let mut provided_params = Vec::new();
for (name, value) in params {
if seen.contains(name) {
defined_more_than_once.insert(name);
continue;
}
seen.insert(name);
let Some(mpl_param) = mpl_params.iter().find(|p| p.name == name) else {
provided_but_not_declared.insert(name);
continue;
};
let parsed = MPLParser::parse(Rule::param_value, value).map_err(|err| {
ParseProvidedParamsError::ParseParam {
param_name: name.to_string(),
expected_type: mpl_param.typ,
err: ParseParamError::Parse(ParseError::from(err)),
}
})?;
let value = parser::parse_param_value(mpl_param, parsed).map_err(|err| {
ParseProvidedParamsError::ParseParam {
param_name: name.to_string(),
expected_type: mpl_param.typ,
err,
}
})?;
provided_params.push(ProvidedParam {
name: name.to_string(),
value,
});
}
if !provided_but_not_declared.is_empty() {
let mut items = provided_but_not_declared
.into_iter()
.map(|p| format!("${p}"))
.collect::<Vec<String>>();
items.sort();
warnings.push(format!(
"These params were provided but not declared: {}",
items.join(", ")
));
}
if !defined_more_than_once.is_empty() {
let mut items = defined_more_than_once
.into_iter()
.map(String::from)
.collect::<Vec<String>>();
items.sort();
return Err(ParseProvidedParamsError::ParamsProvidedMoreThanOnce(items));
}
let declared_param_names = mpl_params
.iter()
.filter_map(|p| {
if p.typ.is_optional() {
None
} else {
Some(p.name.as_str())
}
})
.collect::<HashSet<&str>>();
let declared_but_not_provided = declared_param_names
.difference(&seen)
.collect::<Vec<&&str>>();
if !declared_but_not_provided.is_empty() {
let mut items = declared_but_not_provided
.into_iter()
.map(|s| String::from(*s))
.collect::<Vec<String>>();
items.sort();
return Err(ParseProvidedParamsError::ParamsDeclaredButNotProvided(
items,
));
}
Ok((ProvidedParams::new(provided_params), warnings))
}
#[must_use]
pub fn as_slice(&self) -> &[ProvidedParam] {
self.inner.as_slice()
}
fn get_param(&self, name: &str) -> Result<&ProvidedParam, ResolveError> {
self.inner
.iter()
.find(|p| p.name == name)
.ok_or(ResolveError::ParamNotProvided(name.to_string()))
}
pub fn resolve_tag_value(&self, pv: Parameterized<TagValue>) -> Result<TagValue, ResolveError> {
let param = match pv {
Parameterized::Concrete(val) => return Ok(val), Parameterized::Param { span: _, param } => param,
};
let provided_param = self.get_param(¶m.name)?;
match &provided_param.value {
ParamValue::String(val) => Ok(TagValue::String(SharedString::try_from(val)?)),
ParamValue::Int(val) => Ok(TagValue::Int(*val)),
ParamValue::Float(val) => Ok(TagValue::Float(*val)),
ParamValue::Bool(val) => Ok(TagValue::Bool(*val)),
val => Err(ResolveError::InvalidType {
name: param.name,
defined: val.typ(),
expected: vec![
TerminalParamType::Tag(TagType::String),
TerminalParamType::Tag(TagType::Int),
TerminalParamType::Tag(TagType::Float),
TerminalParamType::Tag(TagType::Bool),
],
}),
}
}
pub fn resolve_dataset(&self, pv: Parameterized<Dataset>) -> Result<Dataset, ResolveError> {
let param = match pv {
Parameterized::Concrete(val) => return Ok(val), Parameterized::Param { span: _, param } => param,
};
let provided_param = self.get_param(¶m.name)?;
match &provided_param.value {
ParamValue::Dataset(dataset) => Ok(dataset.clone()),
val => Err(ResolveError::InvalidType {
name: param.name,
defined: val.typ(),
expected: vec![TerminalParamType::Dataset],
}),
}
}
pub fn resolve_relative_time(
&self,
pv: Parameterized<RelativeTime>,
) -> Result<RelativeTime, ResolveError> {
let param = match pv {
Parameterized::Concrete(val) => return Ok(val), Parameterized::Param { span: _, param } => param,
};
let provided_param = self.get_param(¶m.name)?;
match &provided_param.value {
ParamValue::Duration(relative_time) => Ok(relative_time.clone()),
val => Err(ResolveError::InvalidType {
name: param.name,
defined: val.typ(),
expected: vec![TerminalParamType::Duration],
}),
}
}
pub fn resolve_regex(
&self,
pv: Parameterized<EncodableRegex>,
) -> Result<EncodableRegex, ResolveError> {
let param = match pv {
Parameterized::Concrete(val) => return Ok(val), Parameterized::Param { span: _, param } => param,
};
let provided_param = self.get_param(¶m.name)?;
match &provided_param.value {
ParamValue::Regex(re) => Ok(re.clone()),
val => Err(ResolveError::InvalidType {
name: param.name,
defined: val.typ(),
expected: vec![TerminalParamType::Regex],
}),
}
}
#[must_use]
pub fn contains(&self, param: &str) -> bool {
self.get_param(param).is_ok()
}
#[must_use]
pub fn active_filter<'a>(&self, filter: &'a FilterOrIfDef) -> Option<&'a Filter> {
match filter {
FilterOrIfDef::Filter(filter) => Some(filter),
FilterOrIfDef::Ifdef { param, filter } if self.contains(¶m.name) => Some(filter),
FilterOrIfDef::Ifdef { .. } => None,
}
}
#[must_use]
pub fn active_filters<'a>(&self, filters: &'a [FilterOrIfDef]) -> Vec<&'a Filter> {
filters
.iter()
.filter_map(|filter| self.active_filter(filter))
.collect()
}
}
#[cfg_attr(feature = "wasm", tsify::declare)]
pub type Params = Vec<ParamDeclaration>;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum Query {
Simple {
source: Source,
filters: Vec<FilterOrIfDef>,
aggregates: Vec<Aggregate>,
directives: Directives,
params: Params,
sample: Option<f64>,
},
Compute {
left: Box<Query>,
right: Box<Query>,
name: Metric,
op: ComputeFunction,
aggregates: Vec<Aggregate>,
directives: Directives,
params: Params,
},
}
impl Query {
#[must_use]
pub fn params(&self) -> &Params {
match self {
Query::Simple { params, .. } | Query::Compute { params, .. } => params,
}
}
#[must_use]
pub fn directives(&self) -> &Directives {
match self {
Query::Simple { directives, .. } | Query::Compute { directives, .. } => directives,
}
}
}
impl RelativeTime {
pub fn to_duration(&self) -> Result<Duration, TimeError> {
let v = i64::try_from(self.value).map_err(TimeError::InvalidDuration)?;
Ok(match self.unit {
TimeUnit::Millisecond => Duration::milliseconds(v),
TimeUnit::Second => Duration::seconds(v),
TimeUnit::Minute => Duration::minutes(v),
TimeUnit::Hour => Duration::hours(v),
TimeUnit::Day => Duration::days(v),
TimeUnit::Week => Duration::weeks(v),
TimeUnit::Month => Duration::days(v.saturating_mul(30)),
TimeUnit::Year => Duration::days(v.saturating_mul(365)),
})
}
pub fn to_resolution(&self) -> Result<Resolution, ResolutionError> {
match self.unit {
TimeUnit::Millisecond => Resolution::secs(self.value / 1000),
TimeUnit::Second => Resolution::secs(self.value),
TimeUnit::Minute => Resolution::secs(self.value.saturating_mul(60)),
TimeUnit::Hour => Resolution::secs(self.value.saturating_mul(60 * 60)),
TimeUnit::Day => Resolution::secs(self.value.saturating_mul(60 * 60 * 24)),
TimeUnit::Week => Resolution::secs(self.value.saturating_mul(60 * 60 * 24 * 7)),
TimeUnit::Month => Resolution::secs(self.value.saturating_mul(60 * 60 * 24 * 30)),
TimeUnit::Year => Resolution::secs(self.value.saturating_mul(60 * 60 * 24 * 365)),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum TimeError {
#[error("Invalid timestamp {0}, could not be converted to a UTC datetime")]
InvalidTimestamp(i64),
#[error(
"Invalid duration {0}, could not be converted to Duration as it exceeds the maximum i64"
)]
InvalidDuration(TryFromIntError),
}
#[cfg(feature = "clock")]
impl Time {
fn to_datetime(&self) -> Result<DateTime<Utc>, TimeError> {
Ok(match self {
Time::Relative(t) => Utc::now() - t.to_duration()?,
Time::Timestamp(ts) => {
DateTime::<Utc>::from_timestamp(*ts, 0).ok_or(TimeError::InvalidTimestamp(*ts))?
}
Time::RFC3339(t) => t.with_timezone(&Utc),
Time::Modifier(_) => todo!(),
})
}
}
#[cfg(feature = "clock")]
impl TimeRange {
pub fn to_start_end(&self) -> Result<(DateTime<Utc>, DateTime<Utc>), TimeError> {
let start = self.start.to_datetime()?;
let end = self
.end
.as_ref()
.map_or_else(|| Ok(Utc::now()), Time::to_datetime)?;
Ok((start, end))
}
}