Line data Source code
1 : // SPDX-License-Identifier: EUPL-1.2
2 : //! The temporal query of a request: `TemporalQ` parsed from the
3 : //! timerel/timeAt/endTimeAt/timeproperty parameters (5.2.21, Table
4 : //! 5.2.21-1) and the instance match it defines (4.11).
5 :
6 : use antares_jsonld::parse_datetime;
7 : use antares_model::{dt_key, NgsiError};
8 : use serde_json::Value;
9 : use std::collections::HashMap;
10 :
11 : #[derive(Clone)]
12 : pub struct TemporalQ {
13 : pub timerel: String,
14 : pub time_at: String,
15 : pub end_time_at: Option<String>,
16 : pub timeproperty: String,
17 : }
18 :
19 : impl TemporalQ {
20 : /// The 4.11 Temporal Query from its request parameters: `timerel` decides
21 : /// which of `timeAt`/`endTimeAt` are required, and `required` says
22 : /// whether the operation demands one at all (5.7.4 does, 5.7.3 does not).
23 : /// `GeoQuery::from_params` is the same convention for a different
24 : /// parameter family, not the same parser.
25 1714 : pub fn from_params(
26 1714 : params: &HashMap<String, String>,
27 1714 : required: bool,
28 1714 : ) -> Result<Option<Self>, NgsiError> {
29 1714 : let bad = NgsiError::BadRequestData;
30 1714 : let Some(timerel) = params.get("timerel") else {
31 344 : if required {
32 4 : return Err(bad("temporal query requires timerel (5.7.4)".into()));
33 340 : }
34 340 : if params.contains_key("timeAt") || params.contains_key("endTimeAt") {
35 4 : return Err(bad("timeAt given without timerel".into()));
36 336 : }
37 : // bare timeproperty: representation keyed on it; instances that
38 : // lack it are excluded (retrieval-by-deletedAt, 020_17/18)
39 336 : if let Some(tp) = params.get("timeproperty") {
40 8 : if !["observedAt", "createdAt", "modifiedAt", "deletedAt"].contains(&tp.as_str()) {
41 0 : return Err(bad(format!("invalid timeproperty {tp:?}")));
42 8 : }
43 8 : return Ok(Some(Self {
44 8 : timerel: "any".into(),
45 8 : time_at: String::new(),
46 8 : end_time_at: None,
47 8 : timeproperty: tp.clone(),
48 8 : }));
49 328 : }
50 328 : return Ok(None);
51 : };
52 1370 : if !["before", "after", "between"].contains(&timerel.as_str()) {
53 16 : return Err(bad(format!("invalid timerel {timerel:?}")));
54 1354 : }
55 1354 : let time_at = params
56 1354 : .get("timeAt")
57 1354 : .filter(|s| parse_datetime(s))
58 1354 : .ok_or_else(|| bad("timeAt must be a valid ISO 8601 DateTime (4.11)".into()))?
59 1334 : .clone();
60 1334 : let end_time_at = match params.get("endTimeAt") {
61 592 : Some(s) if parse_datetime(s) => Some(s.clone()),
62 0 : Some(_) => return Err(bad("endTimeAt must be a valid ISO 8601 DateTime".into())),
63 742 : None => None,
64 : };
65 1334 : if timerel == "between" && end_time_at.is_none() {
66 12 : return Err(bad("timerel=between requires endTimeAt (4.11)".into()));
67 1322 : }
68 1322 : let timeproperty = params
69 1322 : .get("timeproperty")
70 1322 : .cloned()
71 1322 : .unwrap_or_else(|| "observedAt".into());
72 1322 : if !["observedAt", "createdAt", "modifiedAt", "deletedAt"].contains(&timeproperty.as_str())
73 : {
74 4 : return Err(bad(format!("invalid timeproperty {timeproperty:?}")));
75 1318 : }
76 1318 : Ok(Some(Self {
77 1318 : timerel: timerel.clone(),
78 1318 : time_at,
79 1318 : end_time_at,
80 1318 : timeproperty,
81 1318 : }))
82 1714 : }
83 :
84 3188 : pub(crate) fn instance_matches(&self, inst: &Value) -> bool {
85 3188 : let Some(t) = inst.get(&self.timeproperty).and_then(Value::as_str) else {
86 30 : return false;
87 : };
88 : // 4.11: before = exclusive bound, after = inclusive bound, between =
89 : // inclusive lower / exclusive upper. Compared on the canonical key so
90 : // equal instants written with different 4.6.3 fraction forms
91 : // ("…00Z" / "…00.000Z" / "…00,5Z") hit the bounds exactly.
92 3158 : let t = dt_key(t);
93 3158 : match self.timerel.as_str() {
94 3158 : "any" => true, // bare timeproperty: presence is the filter
95 3154 : "before" => t < dt_key(&self.time_at),
96 3096 : "after" => t >= dt_key(&self.time_at),
97 2476 : "between" => {
98 2476 : t >= dt_key(&self.time_at)
99 2234 : && self.end_time_at.as_deref().is_some_and(|e| t < dt_key(e))
100 : }
101 0 : _ => false,
102 : }
103 3188 : }
104 : }
105 :
106 : #[cfg(test)]
107 : mod tests {
108 : use super::*;
109 : use serde_json::json;
110 :
111 20 : fn tq(timerel: &str, time_at: &str, end: Option<&str>) -> TemporalQ {
112 20 : let mut p = HashMap::new();
113 20 : p.insert("timerel".to_owned(), timerel.to_owned());
114 20 : p.insert("timeAt".to_owned(), time_at.to_owned());
115 20 : if let Some(e) = end {
116 4 : p.insert("endTimeAt".to_owned(), e.to_owned());
117 16 : }
118 20 : TemporalQ::from_params(&p, true).unwrap().unwrap()
119 20 : }
120 :
121 48 : fn inst(observed_at: &str) -> Value {
122 48 : json!({"observedAt": observed_at, "value": 1})
123 48 : }
124 :
125 : /// 4.11 after: "The specified value is used as an INCLUSIVE bound" — an
126 : /// instance at exactly timeAt matches, regardless of the equal instant
127 : /// being written with or without a seconds fraction (4.6.3 allows both).
128 : #[test]
129 4 : fn after_is_inclusive_across_fraction_forms() {
130 4 : let q = tq("after", "2017-12-13T14:20:00Z", None);
131 4 : assert!(q.instance_matches(&inst("2017-12-13T14:20:00Z")));
132 4 : assert!(
133 4 : q.instance_matches(&inst("2017-12-13T14:20:00.000Z")),
134 : "same instant with a fraction must be included"
135 : );
136 4 : assert!(!q.instance_matches(&inst("2017-12-13T14:19:59.999999Z")));
137 4 : }
138 :
139 : /// 4.11 before: "The specified value is used as an EXCLUSIVE bound" — an
140 : /// instance at exactly timeAt does not match, in any equal spelling.
141 : #[test]
142 4 : fn before_is_exclusive_across_fraction_forms() {
143 4 : let q = tq("before", "2017-12-13T14:20:00Z", None);
144 4 : assert!(!q.instance_matches(&inst("2017-12-13T14:20:00Z")));
145 4 : assert!(
146 4 : !q.instance_matches(&inst("2017-12-13T14:20:00.000Z")),
147 : "same instant with a fraction must stay excluded"
148 : );
149 4 : assert!(q.instance_matches(&inst("2017-12-13T14:19:59.999999Z")));
150 4 : }
151 :
152 : /// 4.11 between: "the lower bound of the range is inclusive and ... the
153 : /// upper bound of the range is exclusive."
154 : #[test]
155 4 : fn between_bounds_inclusive_lower_exclusive_upper() {
156 4 : let q = tq(
157 4 : "between",
158 4 : "2017-12-13T14:20:00Z",
159 4 : Some("2017-12-13T14:40:00Z"),
160 : );
161 4 : assert!(
162 4 : q.instance_matches(&inst("2017-12-13T14:20:00.000Z")),
163 : "lower incl"
164 : );
165 4 : assert!(q.instance_matches(&inst("2017-12-13T14:30:00Z")));
166 4 : assert!(
167 4 : !q.instance_matches(&inst("2017-12-13T14:40:00.000Z")),
168 : "upper excl in any spelling"
169 : );
170 4 : assert!(!q.instance_matches(&inst("2017-12-13T14:19:59Z")));
171 4 : }
172 :
173 : /// 4.6.3: "a comma instead of a decimal point may be used" in requests —
174 : /// the comma form must compare as the same instant.
175 : #[test]
176 4 : fn comma_fraction_compares_as_the_same_instant() {
177 4 : let q = tq("after", "2017-12-13T14:20:00,500000Z", None);
178 4 : assert!(q.instance_matches(&inst("2017-12-13T14:20:00.5Z")));
179 4 : assert!(!q.instance_matches(&inst("2017-12-13T14:20:00.499999Z")));
180 4 : }
181 :
182 : /// 4.11: "Entities which do not convey the target Temporal Property of
183 : /// the query shall be considered as non-matching" + timeproperty
184 : /// defaults to observedAt.
185 : #[test]
186 4 : fn missing_timeproperty_is_a_nonmatch_and_default_is_observed_at() {
187 4 : let q = tq("after", "1970-01-01T00:00:00Z", None);
188 4 : assert_eq!(q.timeproperty, "observedAt");
189 4 : assert!(!q.instance_matches(&json!({"modifiedAt": "2020-01-01T00:00:00Z"})));
190 4 : }
191 :
192 : /// 4.11 grammar: only before/after/between; timeAt mandatory and a
193 : /// DateTime; between requires endTimeAt.
194 : #[test]
195 4 : fn grammar_rejections() {
196 20 : let mk = |pairs: &[(&str, &str)]| {
197 20 : let mut p = HashMap::new();
198 32 : for (k, v) in pairs {
199 32 : p.insert((*k).to_owned(), (*v).to_owned());
200 32 : }
201 20 : TemporalQ::from_params(&p, false)
202 20 : };
203 4 : assert!(mk(&[("timerel", "during"), ("timeAt", "2020-01-01T00:00:00Z")]).is_err());
204 4 : assert!(mk(&[("timerel", "before")]).is_err(), "timeAt mandatory");
205 4 : assert!(
206 4 : mk(&[("timerel", "before"), ("timeAt", "2020-01-01")]).is_err(),
207 : "Date is not a DateTime"
208 : );
209 4 : assert!(
210 4 : mk(&[("timerel", "between"), ("timeAt", "2020-01-01T00:00:00Z")]).is_err(),
211 : "between requires endTimeAt"
212 : );
213 4 : assert!(
214 4 : mk(&[("timeAt", "2020-01-01T00:00:00Z")]).is_err(),
215 : "timeAt without timerel"
216 : );
217 4 : }
218 : }
|