1use std::{
3 collections::{HashMap, HashSet},
4 num::TryFromIntError,
5};
6
7#[cfg(feature = "clock")]
8use chrono::Utc;
9use chrono::{DateTime, Duration, FixedOffset};
10use miette::SourceSpan;
11use pest::Parser as _;
12use strumbra::SharedString;
13
14use crate::{
15 ParseError,
16 enc_regex::EncodableRegex,
17 linker::{AlignFunction, ComputeFunction, GroupFunction, MapFunction},
18 parser::{self, MPLParser, ParseParamError, Rule},
19 tags::TagValue,
20 time::{Resolution, ResolutionError},
21 types::{BucketSpec, BucketType, Dataset, Metric, Parameterized},
22};
23
24mod fmt;
25#[cfg(test)]
26mod tests;
27
28#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
30#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
31#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
32pub struct MetricId {
33 pub dataset: Parameterized<Dataset>,
35 pub metric: Metric,
37}
38
39#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
41#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
42#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
43pub enum TimeUnit {
44 Millisecond,
46 Second,
48 Minute,
50 Hour,
52 Day,
54 Week,
56 Month,
58 Year,
60}
61
62#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
63#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
65#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
66pub struct RelativeTime {
67 pub value: u64,
69 pub unit: TimeUnit,
71}
72
73#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
75#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
76#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
77pub enum Time {
78 Relative(RelativeTime),
80 Timestamp(i64),
82 RFC3339(#[cfg_attr(feature = "wasm", tsify(type = "string"))] DateTime<FixedOffset>),
84 Modifier(String),
86}
87
88#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
90#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
91#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
92pub struct TimeRange {
93 pub start: Time,
95 pub end: Option<Time>,
97}
98
99#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
101#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
102#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
103pub struct Source {
104 pub metric_id: MetricId,
106 pub time: Option<TimeRange>,
108}
109
110#[derive(Debug, thiserror::Error)]
112pub enum ValueError {
113 #[error("Invalid Float")]
115 BadFloat,
116}
117
118#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
120#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
121#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
122pub enum Cmp {
123 Eq(Parameterized<TagValue>),
125 Ne(Parameterized<TagValue>),
127 Gt(Parameterized<TagValue>),
129 Ge(Parameterized<TagValue>),
131 Lt(Parameterized<TagValue>),
133 Le(Parameterized<TagValue>),
135 RegEx(Parameterized<EncodableRegex>),
137 RegExNot(Parameterized<EncodableRegex>),
139 Is(TagType),
141}
142
143#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
145#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
146#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
147pub struct As {
148 pub name: Metric,
150}
151
152#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
154#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
155#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
156pub enum Filter {
157 And(Vec<Filter>),
159 Or(Vec<Filter>),
161 Not(Box<Filter>),
163 Cmp {
165 field: String,
167 rhs: Cmp,
169 },
170}
171
172#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
174#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
175#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
176pub struct Mapping {
177 pub function: MapFunction,
179 pub arg: Option<f64>,
181}
182
183#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
185#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
186#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
187pub struct Align {
188 pub function: AlignFunction,
190 pub time: Parameterized<RelativeTime>,
192}
193
194#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
196#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
197#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
198pub struct GroupBy {
199 #[cfg_attr(feature = "wasm", tsify(type = "{ offset: number, length: number }"))]
201 pub span: SourceSpan,
202 pub function: GroupFunction,
204 pub tags: Vec<String>,
206}
207
208#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
210#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
211#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
212pub struct BucketBy {
213 #[cfg_attr(feature = "wasm", tsify(type = "{ offset: number, length: number }"))]
215 pub span: SourceSpan,
216 pub function: BucketType,
218 pub time: Parameterized<RelativeTime>,
220 pub tags: Vec<String>,
222 pub spec: Vec<BucketSpec>,
224}
225
226#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
228#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
229#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
230pub enum Aggregate {
231 Map(Mapping),
233 Align(Align),
235 GroupBy(GroupBy),
237 Bucket(BucketBy),
239 As(As),
241}
242
243#[cfg_attr(feature = "wasm", tsify::declare)]
245#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
246#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
247pub enum DirectiveValue {
248 Ident(String),
250 Int(i64),
252 Float(f64),
254 String(String),
256 Bool(bool),
258 None,
260}
261
262impl DirectiveValue {
263 #[must_use]
265 pub fn as_ident(&self) -> Option<&str> {
266 match self {
267 DirectiveValue::Ident(ident) => Some(ident),
268 _ => None,
269 }
270 }
271 #[must_use]
273 pub fn as_int(&self) -> Option<i64> {
274 match self {
275 DirectiveValue::Int(int) => Some(*int),
276 _ => None,
277 }
278 }
279 #[must_use]
281 pub fn as_float(&self) -> Option<f64> {
282 match self {
283 DirectiveValue::Float(float) => Some(*float),
284 _ => None,
285 }
286 }
287 #[must_use]
289 pub fn as_string(&self) -> Option<&str> {
290 match self {
291 DirectiveValue::String(string) => Some(string),
292 _ => None,
293 }
294 }
295 #[must_use]
297 pub fn as_bool(&self) -> Option<bool> {
298 match self {
299 DirectiveValue::Bool(bool) => Some(*bool),
300 _ => None,
301 }
302 }
303 #[must_use]
305 pub fn is_none(&self) -> bool {
306 matches!(self, DirectiveValue::None)
307 }
308 #[must_use]
310 pub fn is_some(&self) -> bool {
311 !self.is_none()
312 }
313}
314
315#[cfg_attr(feature = "wasm", tsify::declare)]
317#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
318pub enum ParamType {
319 Duration,
321 Dataset,
323 Regex,
325 Tag(TagType),
327}
328impl std::fmt::Display for ParamType {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 match self {
331 ParamType::Dataset => write!(f, "dataset"),
332 ParamType::Duration => write!(f, "duration"),
333 ParamType::Regex => write!(f, "regex"),
334 ParamType::Tag(t) => t.fmt(f),
335 }
336 }
337}
338
339#[cfg_attr(feature = "wasm", tsify::declare)]
341#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
342#[derive(Clone, Copy, Debug, Hash, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
343pub enum TagType {
344 String,
346 Int,
348 Float,
350 Bool,
352 None,
354}
355
356impl std::fmt::Display for TagType {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 write!(
359 f,
360 "{}",
361 match self {
362 TagType::String => "string",
363 TagType::Int => "int",
364 TagType::Float => "float",
365 TagType::Bool => "bool",
366 TagType::None => "null",
367 }
368 )
369 }
370}
371
372#[cfg_attr(feature = "wasm", tsify::declare)]
374pub type Directives = HashMap<String, DirectiveValue>;
375
376#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
378#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
379pub struct Param {
380 #[cfg_attr(feature = "wasm", tsify(type = "{ offset: number, length: number }"))]
382 pub span: SourceSpan,
383 pub name: String,
385 pub typ: ParamType,
387}
388
389#[derive(Debug, Clone, PartialEq)]
391pub enum ParamValue {
392 Dataset(Dataset),
394 Duration(RelativeTime),
396 String(String),
398 Int(i64),
400 Float(f64),
402 Bool(bool),
404 Regex(EncodableRegex),
406}
407
408impl ParamValue {
409 #[must_use]
411 pub fn typ(&self) -> ParamType {
412 match self {
413 ParamValue::Dataset(_) => ParamType::Dataset,
414 ParamValue::Duration(_) => ParamType::Duration,
415 ParamValue::Regex(_) => ParamType::Regex,
416 ParamValue::String(_) => ParamType::Tag(TagType::String),
417 ParamValue::Int(_) => ParamType::Tag(TagType::Int),
418 ParamValue::Float(_) => ParamType::Tag(TagType::Float),
419 ParamValue::Bool(_) => ParamType::Tag(TagType::Bool),
420 }
421 }
422}
423
424#[derive(Debug, Clone, PartialEq)]
426pub struct ProvidedParam {
427 pub name: String,
429 pub value: ParamValue,
431}
432
433impl ProvidedParam {
434 pub fn new(name: impl Into<String>, value: ParamValue) -> Self {
436 Self {
437 name: name.into(),
438 value,
439 }
440 }
441}
442
443#[derive(Debug, Clone, Default)]
445pub struct ProvidedParams {
446 inner: Vec<ProvidedParam>,
447}
448
449#[derive(Debug, thiserror::Error)]
451pub enum ResolveError {
452 #[error("Param ${0} was not provided to the query")]
454 ParamNotProvided(String),
455 #[error(
457 "Param ${name} is defined as `{defined}`, but was used in a context that expected one of: {}",
458 expected.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
459 )]
460 InvalidType {
461 name: String,
463 defined: ParamType,
465 expected: Vec<ParamType>,
467 },
468 #[error("Shared string error: {0}")]
470 SharedString(#[from] strumbra::Error),
471}
472
473#[derive(Debug, thiserror::Error)]
475pub enum ParseProvidedParamsError {
476 #[error("Failed to parse the value for ${param_name} as {expected_type}: {err}")]
478 ParseParam {
479 param_name: String,
481 expected_type: ParamType,
483 err: ParseParamError,
485 },
486 #[error("These params were provided more than once: {}", .0.join(", "))]
488 ParamsProvidedMoreThanOnce(Vec<String>),
489 #[error("The following params were declared but not provided: {}", .0.join(", "))]
491 ParamsDeclaredButNotProvided(Vec<String>),
492 #[error("The number of params provided exceeds the upper limit of {0}")]
494 TooManyParamsProvided(usize),
495}
496
497#[derive(Debug, Default)]
499pub struct Warnings {
500 inner: Vec<String>,
501}
502
503impl Warnings {
504 #[must_use]
506 pub fn new() -> Self {
507 Self::default()
508 }
509
510 pub fn push(&mut self, warning: impl Into<String>) {
512 self.inner.push(warning.into());
513 }
514
515 #[must_use]
517 pub fn is_empty(&self) -> bool {
518 self.inner.is_empty()
519 }
520
521 #[must_use]
523 pub fn as_slice(&self) -> &[String] {
524 &self.inner
525 }
526
527 #[must_use]
529 pub fn into_vec(self) -> Vec<String> {
530 self.inner
531 }
532}
533
534impl ProvidedParams {
535 #[must_use]
537 pub fn new(inner: Vec<ProvidedParam>) -> Self {
538 Self { inner }
539 }
540
541 pub fn parse_and_validate(
545 mpl_params: &Params,
546 query_params: &[(String, String)],
547 ) -> Result<(Self, Warnings), ParseProvidedParamsError> {
548 const PREFIX: &str = "param__";
549 const PARAM_COUNT_LIMIT: usize = 128;
550
551 let mut warnings = Warnings::new();
552 let mut defined_more_than_once = HashSet::new();
553 let mut provided_but_not_declared = HashSet::new();
554 let mut seen = HashSet::new();
555
556 let params = query_params
557 .iter()
558 .filter_map(|(name, value)| {
559 if !name.starts_with(PREFIX) {
560 return None;
561 }
562 let name = name.trim_start_matches(PREFIX);
563 if name.is_empty() {
564 return None;
565 }
566
567 Some((name, value))
568 })
569 .take(PARAM_COUNT_LIMIT + 1)
570 .collect::<Vec<(&str, &String)>>();
571
572 if params.len() > PARAM_COUNT_LIMIT {
574 return Err(ParseProvidedParamsError::TooManyParamsProvided(
575 PARAM_COUNT_LIMIT,
576 ));
577 }
578
579 let mut provided_params = Vec::new();
580 for (name, value) in params {
581 if seen.contains(name) {
582 defined_more_than_once.insert(name);
584 continue;
585 }
586 seen.insert(name);
587
588 let Some(mpl_param) = mpl_params.iter().find(|p| p.name == name) else {
590 provided_but_not_declared.insert(name);
591 continue;
592 };
593
594 let parsed = MPLParser::parse(Rule::param_value, value).map_err(|err| {
596 ParseProvidedParamsError::ParseParam {
597 param_name: name.to_string(),
598 expected_type: mpl_param.typ,
599 err: ParseParamError::Parse(ParseError::from(err)),
600 }
601 })?;
602
603 let value = parser::parse_param_value(mpl_param, parsed).map_err(|err| {
605 ParseProvidedParamsError::ParseParam {
606 param_name: name.to_string(),
607 expected_type: mpl_param.typ,
608 err,
609 }
610 })?;
611
612 provided_params.push(ProvidedParam {
613 name: name.to_string(),
614 value,
615 });
616 }
617
618 if !provided_but_not_declared.is_empty() {
619 let mut items = provided_but_not_declared
621 .into_iter()
622 .map(|p| format!("${p}"))
623 .collect::<Vec<String>>();
624 items.sort();
625
626 warnings.push(format!(
628 "These params were provided but not declared: {}",
629 items.join(", ")
630 ));
631 }
632
633 if !defined_more_than_once.is_empty() {
634 let mut items = defined_more_than_once
636 .into_iter()
637 .map(String::from)
638 .collect::<Vec<String>>();
639 items.sort();
640
641 return Err(ParseProvidedParamsError::ParamsProvidedMoreThanOnce(items));
642 }
643
644 let declared_param_names = mpl_params
645 .iter()
646 .map(|p| p.name.as_str())
647 .collect::<HashSet<&str>>();
648 let declared_but_not_provided = declared_param_names
649 .difference(&seen)
650 .collect::<Vec<&&str>>();
651 if !declared_but_not_provided.is_empty() {
652 let mut items = declared_but_not_provided
654 .into_iter()
655 .map(|s| String::from(*s))
656 .collect::<Vec<String>>();
657 items.sort();
658
659 return Err(ParseProvidedParamsError::ParamsDeclaredButNotProvided(
660 items,
661 ));
662 }
663
664 Ok((ProvidedParams::new(provided_params), warnings))
665 }
666
667 #[must_use]
669 pub fn as_slice(&self) -> &[ProvidedParam] {
670 self.inner.as_slice()
671 }
672
673 fn get_param(&self, name: &str) -> Result<&ProvidedParam, ResolveError> {
674 self.inner
675 .iter()
676 .find(|p| p.name == name)
677 .ok_or(ResolveError::ParamNotProvided(name.to_string()))
678 }
679
680 pub fn resolve_tag_value(&self, pv: Parameterized<TagValue>) -> Result<TagValue, ResolveError> {
682 let param = match pv {
683 Parameterized::Concrete(val) => return Ok(val), Parameterized::Param { span: _, param } => param,
685 };
686
687 let provided_param = self.get_param(¶m.name)?;
688 match &provided_param.value {
689 ParamValue::String(val) => Ok(TagValue::String(SharedString::try_from(val)?)),
690 ParamValue::Int(val) => Ok(TagValue::Int(*val)),
691 ParamValue::Float(val) => Ok(TagValue::Float(*val)),
692 ParamValue::Bool(val) => Ok(TagValue::Bool(*val)),
693 val => Err(ResolveError::InvalidType {
694 name: param.name,
695 defined: val.typ(),
696 expected: vec![
697 ParamType::Tag(TagType::String),
698 ParamType::Tag(TagType::Int),
699 ParamType::Tag(TagType::Float),
700 ParamType::Tag(TagType::Bool),
701 ],
702 }),
703 }
704 }
705
706 pub fn resolve_dataset(&self, pv: Parameterized<Dataset>) -> Result<Dataset, ResolveError> {
708 let param = match pv {
709 Parameterized::Concrete(val) => return Ok(val), Parameterized::Param { span: _, param } => param,
711 };
712
713 let provided_param = self.get_param(¶m.name)?;
714 match &provided_param.value {
715 ParamValue::Dataset(dataset) => Ok(dataset.clone()),
716 val => Err(ResolveError::InvalidType {
717 name: param.name,
718 defined: val.typ(),
719 expected: vec![ParamType::Dataset],
720 }),
721 }
722 }
723
724 pub fn resolve_relative_time(
726 &self,
727 pv: Parameterized<RelativeTime>,
728 ) -> Result<RelativeTime, ResolveError> {
729 let param = match pv {
730 Parameterized::Concrete(val) => return Ok(val), Parameterized::Param { span: _, param } => param,
732 };
733
734 let provided_param = self.get_param(¶m.name)?;
735 match &provided_param.value {
736 ParamValue::Duration(relative_time) => Ok(relative_time.clone()),
737 val => Err(ResolveError::InvalidType {
738 name: param.name,
739 defined: val.typ(),
740 expected: vec![ParamType::Duration],
741 }),
742 }
743 }
744
745 pub fn resolve_regex(
747 &self,
748 pv: Parameterized<EncodableRegex>,
749 ) -> Result<EncodableRegex, ResolveError> {
750 let param = match pv {
751 Parameterized::Concrete(val) => return Ok(val), Parameterized::Param { span: _, param } => param,
753 };
754
755 let provided_param = self.get_param(¶m.name)?;
756 match &provided_param.value {
757 ParamValue::Regex(re) => Ok(re.clone()),
758 val => Err(ResolveError::InvalidType {
759 name: param.name,
760 defined: val.typ(),
761 expected: vec![ParamType::Regex],
762 }),
763 }
764 }
765}
766
767#[cfg_attr(feature = "wasm", tsify::declare)]
769pub type Params = Vec<Param>;
770
771#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
773#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
774#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
775pub enum Query {
776 Simple {
778 source: Source,
780 filters: Vec<Filter>,
782 aggregates: Vec<Aggregate>,
784 directives: Directives,
786 params: Params,
788 sample: Option<f64>,
790 },
791 Compute {
793 left: Box<Query>,
795 right: Box<Query>,
797 name: Metric,
799 op: ComputeFunction,
801 aggregates: Vec<Aggregate>,
803 directives: Directives,
805 params: Params,
807 },
808}
809
810impl Query {
811 #[must_use]
813 pub fn params(&self) -> &Params {
814 match self {
815 Query::Simple { params, .. } | Query::Compute { params, .. } => params,
816 }
817 }
818 #[must_use]
820 pub fn directives(&self) -> &Directives {
821 match self {
822 Query::Simple { directives, .. } | Query::Compute { directives, .. } => directives,
823 }
824 }
825}
826
827impl RelativeTime {
828 pub fn to_duration(&self) -> Result<Duration, TimeError> {
830 let v = i64::try_from(self.value).map_err(TimeError::InvalidDuration)?;
831 Ok(match self.unit {
832 TimeUnit::Millisecond => Duration::milliseconds(v),
833 TimeUnit::Second => Duration::seconds(v),
834 TimeUnit::Minute => Duration::minutes(v),
835 TimeUnit::Hour => Duration::hours(v),
836 TimeUnit::Day => Duration::days(v),
837 TimeUnit::Week => Duration::weeks(v),
838 TimeUnit::Month => Duration::days(v.saturating_mul(30)),
839 TimeUnit::Year => Duration::days(v.saturating_mul(365)),
840 })
841 }
842
843 pub fn to_resolution(&self) -> Result<Resolution, ResolutionError> {
845 match self.unit {
846 TimeUnit::Millisecond => Resolution::secs(self.value / 1000),
847 TimeUnit::Second => Resolution::secs(self.value),
848 TimeUnit::Minute => Resolution::secs(self.value.saturating_mul(60)),
849 TimeUnit::Hour => Resolution::secs(self.value.saturating_mul(60 * 60)),
850 TimeUnit::Day => Resolution::secs(self.value.saturating_mul(60 * 60 * 24)),
851 TimeUnit::Week => Resolution::secs(self.value.saturating_mul(60 * 60 * 24 * 7)),
852 TimeUnit::Month => Resolution::secs(self.value.saturating_mul(60 * 60 * 24 * 30)),
853 TimeUnit::Year => Resolution::secs(self.value.saturating_mul(60 * 60 * 24 * 365)),
854 }
855 }
856}
857
858#[derive(Debug, thiserror::Error)]
860pub enum TimeError {
861 #[error("Invalid timestamp {0}, could not be converted to a UTC datetime")]
863 InvalidTimestamp(i64),
864 #[error(
866 "Invalid duration {0}, could not be converted to Duration as it exceeds the maximum i64"
867 )]
868 InvalidDuration(TryFromIntError),
869}
870#[cfg(feature = "clock")]
871impl Time {
872 fn to_datetime(&self) -> Result<DateTime<Utc>, TimeError> {
873 Ok(match self {
874 Time::Relative(t) => Utc::now() - t.to_duration()?,
875 Time::Timestamp(ts) => {
876 DateTime::<Utc>::from_timestamp(*ts, 0).ok_or(TimeError::InvalidTimestamp(*ts))?
877 }
878 Time::RFC3339(t) => t.with_timezone(&Utc),
879 Time::Modifier(_) => todo!(),
880 })
881 }
882}
883
884#[cfg(feature = "clock")]
885impl TimeRange {
886 pub fn to_start_end(&self) -> Result<(DateTime<Utc>, DateTime<Utc>), TimeError> {
888 let start = self.start.to_datetime()?;
889 let end = self
890 .end
891 .as_ref()
892 .map_or_else(|| Ok(Utc::now()), Time::to_datetime)?;
893 Ok((start, end))
894 }
895}