LCOV - code coverage report
Current view: top level - antares-api/src/temporal - filter.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 95.5 % 291 278
Test Date: 2026-11-02 13:21:27 Functions: 37.5 % 280 105

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! What a temporal read keeps of an Evolution: the rules per instance and the 5.7.4.4 window conditions.
       3              : 
       4              : use super::*;
       5              : 
       6              : /// Whether `rules` decide an instance on the Entity's state at its time (a
       7              : /// rule with a condition), which [`rules_per_instance`] rebuilds from the
       8              : /// whole history. A rule of types, Attributes and windows decides each
       9              : /// instance alone, so the store may prune to the query's window.
      10         2090 : pub(super) fn replays_state(rules: Option<&crate::policy::Filter>) -> bool {
      11         2090 :     rules.is_some_and(|r| r.rules.iter().any(crate::policy::Rule::is_conditional))
      12         2090 : }
      13              : 
      14              : /// Whether the store's per-Attribute cap keeps every instance `rules` would
      15              : /// show of the page: with no rules always; under rules only when no rule
      16              : /// reads state and every rule window stays open on the page's side: towards
      17              : /// now for lastN, and from no later than the query's `start` (its `after`
      18              : /// or `between` instant) oldest first, so the rules keep a leading run of
      19              : /// what the store kept. A window that closes before now under lastN, or
      20              : /// opens after the start oldest first, would have the store keep instances
      21              : /// the rules then drop. Rule windows judge observedAt, so
      22              : /// a window under a cap ordered by another `timeproperty` (a backfill: new
      23              : /// createdAt, old observedAt) keeps the cap in the API too.
      24         1502 : pub(super) fn cap_keeps_shown(
      25         1502 :     rules: Option<&crate::policy::Filter>,
      26         1502 :     page: &InstancePage,
      27         1502 :     timeprop: &str,
      28         1502 :     start: Option<&str>,
      29         1502 : ) -> bool {
      30              :     // an `Ago` bound resolved an hour late: the rules resolve theirs after
      31              :     // the store answered, by then a little later than now
      32         1502 :     let late = chrono::Utc::now()
      33         1502 :         .checked_add_signed(chrono::Duration::hours(1))
      34         1502 :         .unwrap_or(chrono::DateTime::<chrono::Utc>::MAX_UTC);
      35         1502 :     rules.is_none_or(|r| {
      36           82 :         !replays_state(Some(r))
      37           84 :             && r.rules.iter().all(|rule| {
      38           84 :                 rule.temporal.is_empty()
      39           58 :                     || (timeprop == "observedAt"
      40           54 :                         && rule.temporal.iter().all(|w| {
      41           54 :                             if page.desc {
      42            8 :                                 w.to.is_none()
      43              :                             } else {
      44           46 :                                 start.is_some_and(|s| w.opens_by(s, late))
      45              :                             }
      46           54 :                         }))
      47           84 :             })
      48           82 :     })
      49         1502 : }
      50              : 
      51              : /// The Attributes the store may qualify an Entity for a page through under
      52              : /// `rules`: `Some(None)` any, `Some(Some(list))` only these, `None`
      53              : /// where the rules' verdict on an Entity is not the store's, so it may not
      54              : /// page. It is where every rule names Entities by type alone (no condition,
      55              : /// id or idPattern) through the same windows, every type the rules name
      56              : /// shows the same Attributes (the union of its rules: a list longer than a
      57              : /// rule holds is split over several rules of the same types), no
      58              : /// type hides Attributes of its own, each requested type is named by a
      59              : /// rule, and the windows hold the whole request window (open towards now,
      60              : /// opened by its `after` or `between` instant on `observedAt`): an Entity
      61              : /// with an instance of a shown Attribute in the request window then shows
      62              : /// that instance, and one without shows none. `attrs`, the request's own,
      63              : /// narrow what is shown. Without it a view read every Entity of the type
      64              : /// for each page: 577 bike stations, past the 30 s statement timeout.
      65           48 : pub(super) fn page_attrs(
      66           48 :     rules: &crate::policy::Filter,
      67           48 :     types: Option<&[String]>,
      68           48 :     tq: Option<&TemporalQ>,
      69           48 :     attrs: Option<&[String]>,
      70           48 : ) -> Option<Option<Vec<String>>> {
      71           48 :     let first = rules.rules.first()?;
      72           48 :     let start = range_start(tq);
      73           48 :     let late = chrono::Utc::now()
      74           48 :         .checked_add_signed(chrono::Duration::hours(1))
      75           48 :         .unwrap_or(chrono::DateTime::<chrono::Utc>::MAX_UTC);
      76           48 :     let windows_hold = first.temporal.is_empty()
      77           42 :         || (tq.is_none_or(|t| t.timeproperty == "observedAt")
      78           40 :             && start.is_some_and(|s| {
      79           40 :                 first
      80           40 :                     .temporal
      81           40 :                     .iter()
      82           40 :                     .all(|w| w.to.is_none() && w.opens_by(s, late))
      83           40 :             }));
      84           48 :     let alike = rules
      85           48 :         .rules
      86           48 :         .iter()
      87           54 :         .all(|r| r.admits_by_type_alone() && r.temporal == first.temporal);
      88              :     // what an Entity of `t` shows: every Attribute, or the union of its rules' lists
      89           62 :     let shown = |t: &String| {
      90          184 :         let of = || rules.rules.iter().filter(|r| r.types.contains(t));
      91           62 :         let every = of().any(|r| r.every_attribute);
      92           62 :         let names: std::collections::BTreeSet<&String> = if every {
      93            0 :             Default::default()
      94              :         } else {
      95           62 :             of().flat_map(|r| &r.attributes).collect()
      96              :         };
      97           62 :         (every, names)
      98           62 :     };
      99           48 :     let mut typed = rules.rules.iter().flat_map(|r| &r.types).map(shown);
     100           48 :     let (every, names) = typed.next()?;
     101           48 :     let uniform = typed.all(|other| other == (every, names.clone()));
     102           48 :     let named = types.is_some_and(|ts| {
     103           48 :         ts.iter()
     104           52 :             .all(|t| rules.rules.iter().any(|r| r.types.contains(t)))
     105           48 :     });
     106           48 :     if !(windows_hold && alike && uniform && named && rules.omit_by_type.is_empty()) {
     107           26 :         return None;
     108           22 :     }
     109           22 :     Some(match (every, attrs) {
     110            0 :         (true, None) => None,
     111            0 :         (true, Some(a)) => Some(a.to_vec()),
     112           18 :         (false, None) => Some(names.into_iter().cloned().collect()),
     113            4 :         (false, Some(a)) => Some(a.iter().filter(|x| names.contains(x)).cloned().collect()),
     114              :     })
     115           48 : }
     116              : 
     117              : /// The instant a window read oldest first starts at: `timeAt` of `after`
     118              : /// and `between`; `before` and no window start at the beginning of time.
     119         1550 : pub(super) fn range_start(tq: Option<&TemporalQ>) -> Option<&str> {
     120         1550 :     tq.filter(|t| matches!(t.timerel.as_str(), "after" | "between"))
     121         1550 :         .map(|t| t.time_at.as_str())
     122         1550 : }
     123              : 
     124              : /// 5.7.4.4 S2/S3: the values filter and the geoquery are judged on the
     125              : /// Attribute instances WITHIN the temporal-query interval, so an
     126              : /// out-of-window instance must not satisfy either.
     127              : /// the narrowing rules, temporal per instance: an instance of an Attribute
     128              : /// is kept only where a rule showing the Attribute held at the instance's
     129              : /// time (`observedAt`, else `modifiedAt`). A rule without a condition holds
     130              : /// at every time; a conditional one is read on the Entity's state at that
     131              : /// time ([`state_at`]) and does not hold where a member its condition reads
     132              : /// has no instance yet, or where it reads `scope`, which keeps no history:
     133              : /// a state that cannot be rebuilt hides the instance (fail closed). `None`
     134              : /// when no rule names one of the Entity's types.
     135           88 : pub(crate) fn rules_per_instance(doc: &Value, rules: &crate::policy::Filter) -> Option<Value> {
     136           88 :     let ctx = crate::repr::policy_context();
     137           88 :     let types = crate::policy::entity_types(doc);
     138          126 :     let typed: Vec<&crate::policy::Rule> = rules.rules.iter().filter(|r| r.selects(doc)).collect();
     139           88 :     if !rules.rules.is_empty() && typed.is_empty() {
     140            4 :         return None;
     141           84 :     }
     142           84 :     let omitted: std::collections::BTreeSet<&str> = types
     143           84 :         .iter()
     144           84 :         .filter_map(|t| rules.omit_by_type.get(*t))
     145           84 :         .flatten()
     146           84 :         .map(String::as_str)
     147           84 :         .collect();
     148              :     // a rule with windows decides per instance too
     149          292 :     let per_instance = |r: &crate::policy::Rule| r.is_conditional() || !r.temporal.is_empty();
     150           84 :     let always: std::collections::BTreeSet<&str> = typed
     151           84 :         .iter()
     152           98 :         .filter(|r| !per_instance(r))
     153           84 :         .flat_map(|r| r.attributes.iter().map(String::as_str))
     154           84 :         .collect();
     155           84 :     let conditional: Vec<(&crate::policy::Rule, Vec<String>)> = typed
     156           84 :         .iter()
     157           98 :         .filter(|r| per_instance(r) && r.scope_q.is_none())
     158           84 :         .map(|r| (*r, r.reads(ctx)))
     159           84 :         .collect();
     160              :     // an unconditioned rule over every Attribute keeps every instance
     161           96 :     let every = typed.iter().any(|r| !per_instance(r) && r.every_attribute);
     162           84 :     let now = chrono::Utc::now();
     163           84 :     let Some(obj) = doc.as_object() else {
     164            0 :         return Some(doc.clone());
     165              :     };
     166           84 :     let mut out = Map::new();
     167          508 :     for (k, v) in obj {
     168          508 :         if is_meta(k) {
     169          336 :             out.insert(k.clone(), v.clone());
     170          336 :             continue;
     171          172 :         }
     172          172 :         if omitted.contains(k.as_str()) {
     173            0 :             continue;
     174          172 :         }
     175          172 :         if rules.rules.is_empty() || every || always.contains(k.as_str()) {
     176           24 :             out.insert(k.clone(), v.clone());
     177           24 :             continue;
     178          148 :         }
     179          148 :         let deciding: Vec<&(&crate::policy::Rule, Vec<String>)> = conditional
     180          148 :             .iter()
     181          152 :             .filter(|(r, _)| r.every_attribute || r.attributes.iter().any(|a| a == k))
     182          148 :             .collect();
     183          148 :         let Some(instances) = v.as_array().filter(|_| !deciding.is_empty()) else {
     184           68 :             continue;
     185              :         };
     186              :         // A condition rebuilds the state per instance, O(instances x
     187              :         // history); sort each Attribute's history once if long histories show
     188              :         // up under one. A rule without a condition reads no state: `typed`
     189              :         // already selected the Entity by its types and ids, which is all
     190              :         // `holds` reads of it, so only its windows decide.
     191           80 :         let reads_state = deciding.iter().any(|(r, _)| r.is_conditional());
     192           80 :         let kept: Vec<Value> = instances
     193           80 :             .iter()
     194         2464 :             .filter(|inst| {
     195         2464 :                 instant(inst).is_some_and(|t| {
     196         2464 :                     let state = reads_state.then(|| state_at(doc, t));
     197         2464 :                     let observed = inst.get("observedAt").and_then(Value::as_str);
     198         2464 :                     deciding.iter().any(|(r, reads)| {
     199         2464 :                         (!r.is_conditional()
     200          148 :                             || state.as_ref().is_some_and(|state| {
     201          148 :                                 reads.iter().all(|a| state.get(a).is_some()) && r.holds(state, ctx)
     202          148 :                             }))
     203         2434 :                             && r.temporal
     204         2434 :                                 .iter()
     205         2434 :                                 .all(|w| observed.is_some_and(|o| w.admits(o, now)))
     206         2464 :                     })
     207         2464 :                 })
     208         2464 :             })
     209           80 :             .cloned()
     210           80 :             .collect();
     211           80 :         if !kept.is_empty() {
     212           80 :             out.insert(k.clone(), Value::Array(kept));
     213           80 :         }
     214              :     }
     215           84 :     Some(Value::Object(out))
     216           88 : }
     217              : 
     218              : /// When an instance holds: its `observedAt`, else its `modifiedAt`.
     219         4896 : pub(super) fn instant(inst: &Value) -> Option<&str> {
     220         4896 :     inst.get("observedAt")
     221         4896 :         .or_else(|| inst.get("modifiedAt"))
     222         4896 :         .and_then(Value::as_str)
     223         4896 : }
     224              : 
     225              : /// The Entity as it stood at `t`, rebuilt from its history: the frame, and
     226              : /// for each Attribute the latest instance at or before `t`, if any.
     227          148 : pub(super) fn state_at(doc: &Value, t: &str) -> Value {
     228          148 :     let at = dt_key(t);
     229          148 :     let Some(obj) = doc.as_object() else {
     230            0 :         return Value::Null;
     231              :     };
     232          148 :     let mut out = Map::new();
     233          936 :     for (k, v) in obj {
     234          936 :         if is_meta(k) {
     235          592 :             if k == "id" || k == "type" {
     236          296 :                 out.insert(k.clone(), v.clone());
     237          296 :             }
     238          592 :             continue;
     239          344 :         }
     240          344 :         let latest = v
     241          344 :             .as_array()
     242          344 :             .into_iter()
     243          344 :             .flatten()
     244         2432 :             .filter_map(|inst| instant(inst).map(|i| (dt_key(i), inst)))
     245         2432 :             .filter(|(i, _)| *i <= at)
     246         1038 :             .max_by(|a, b| a.0.cmp(&b.0))
     247          344 :             .map(|(_, inst)| inst.clone());
     248          344 :         if let Some(inst) = latest {
     249          338 :             out.insert(k.clone(), Value::Array(vec![inst]));
     250          338 :         }
     251              :     }
     252          148 :     Value::Object(out)
     253          148 : }
     254              : 
     255         1026 : pub(super) fn window_conditions_match(
     256         1026 :     doc: &Value,
     257         1026 :     tq: Option<&TemporalQ>,
     258         1026 :     q_ast: Option<&antares_ql::QNode>,
     259         1026 :     geos: &[&antares_ql::geo::GeoQuery],
     260         1026 :     ctx: &Context,
     261         1026 : ) -> bool {
     262              :     // 5.7.4.4 S2/S3: the values filter and geoquery are checked against
     263              :     // the Attribute instances WITHIN the temporal-query interval — an
     264              :     // out-of-window instance must not satisfy them
     265         1026 :     let mut eval_doc = doc.clone();
     266         1026 :     if let (Some(tqv), Some(o)) = (tq, eval_doc.as_object_mut()) {
     267         5580 :         for (k, v) in o.iter_mut() {
     268         5580 :             if is_meta(k) {
     269         4072 :                 continue;
     270         1508 :             }
     271         1508 :             if let Some(arr) = v.as_array_mut() {
     272         1774 :                 arr.retain(|inst| tqv.instance_matches(inst));
     273            0 :             }
     274              :         }
     275            4 :     }
     276         1026 :     if let Some(ast) = q_ast {
     277          904 :         if !antares_ql::eval::eval_q(ast, &eval_doc, ctx, &|_| None) {
     278          666 :             return false;
     279          238 :         }
     280          122 :     }
     281          360 :     geos.iter().all(|g| g.matches(&eval_doc, ctx))
     282         1026 : }
     283              : 
     284              : /// 5.7.4.4 S9: of each Attribute `q` reads, keep the instances that meet
     285              : /// the query: an instance meets it when `q` holds on the Entity with that
     286              : /// Attribute narrowed to the one instance (the other Attributes in the
     287              : /// window), so a term on another Attribute still acts at Entity level.
     288          238 : pub(super) fn retain_q_instances(
     289          238 :     doc: &mut Value,
     290          238 :     tq: Option<&TemporalQ>,
     291          238 :     ast: &antares_ql::QNode,
     292          238 :     ctx: &Context,
     293          238 : ) {
     294          238 :     let mut heads: Vec<String> = ast
     295          238 :         .attribute_paths()
     296          238 :         .into_iter()
     297         2322 :         .filter_map(|p| p.split(['.', '[', '{']).next().map(|h| ctx.expand_key(h)))
     298          238 :         .collect();
     299          238 :     heads.sort();
     300          238 :     heads.dedup();
     301          238 :     let mut eval_doc = doc.clone();
     302          238 :     if let (Some(tqv), Some(o)) = (tq, eval_doc.as_object_mut()) {
     303         1390 :         for (k, v) in o.iter_mut() {
     304         1390 :             if !is_meta(k) {
     305          454 :                 if let Some(arr) = v.as_array_mut() {
     306          568 :                     arr.retain(|inst| tqv.instance_matches(inst));
     307            0 :                 }
     308          936 :             }
     309              :         }
     310            0 :     }
     311          238 :     let (Some(obj), Value::Object(eval)) = (doc.as_object_mut(), &mut eval_doc) else {
     312            0 :         return;
     313              :     };
     314          282 :     for head in heads {
     315          282 :         let Some(arr) = obj.get_mut(&head).and_then(Value::as_array_mut) else {
     316           20 :             continue;
     317              :         };
     318          262 :         let windowed = eval.get(&head).cloned();
     319          354 :         arr.retain(|inst| {
     320          354 :             eval.insert(head.clone(), Value::Array(vec![inst.clone()]));
     321              :             // `eval` is lent back as a document for the one evaluation
     322          354 :             let probe = Value::Object(std::mem::take(eval));
     323          354 :             let holds = antares_ql::eval::eval_q(ast, &probe, ctx, &|_| None);
     324          354 :             if let Value::Object(m) = probe {
     325          354 :                 *eval = m;
     326          354 :             }
     327          354 :             holds
     328          354 :         });
     329          262 :         if let Some(w) = windowed {
     330          262 :             eval.insert(head, w);
     331          262 :         }
     332              :     }
     333          238 : }
     334              : 
     335              : /// 5.7.4.4 S4 — and S7: the federation merge precedes this check, so
     336              : /// split and aggregated entities are re-filtered here too. The entity
     337              : /// qualifies if one of its scope's 4.18 validity intervals intersects
     338              : /// the query window; attribute instances outside every matching interval
     339              : /// are excluded (annex C.5.16).
     340          372 : pub(super) fn scope_window_retain(
     341          372 :     doc: &mut Value,
     342          372 :     sq: &str,
     343          372 :     tq: Option<&TemporalQ>,
     344          372 :     timeprop: &str,
     345          372 : ) -> bool {
     346              :     // every comparison on dt_key: raw strings put `…00.500Z` below `…00Z`
     347          372 :     let iv: Vec<(String, Option<String>)> = scope_match_intervals(doc, sq)
     348          372 :         .into_iter()
     349          372 :         .map(|(s, e)| (dt_key(&s), e.as_deref().map(dt_key)))
     350          372 :         .collect();
     351          372 :     let (wstart, wend) = match tq {
     352          368 :         Some(t) => match t.timerel.as_str() {
     353          368 :             "before" => (None, Some(dt_key(&t.time_at))),
     354          368 :             "after" => (Some(dt_key(&t.time_at)), None),
     355          362 :             "between" => (
     356          362 :                 Some(dt_key(&t.time_at)),
     357          362 :                 t.end_time_at.as_deref().map(dt_key),
     358          362 :             ),
     359            0 :             _ => (None, None),
     360              :         },
     361            4 :         None => (None, None),
     362              :     };
     363          372 :     let intersects = iv.iter().any(|(s, e)| {
     364          110 :         wend.as_deref().is_none_or(|w| s.as_str() < w)
     365          100 :             && e.as_deref()
     366          100 :                 .is_none_or(|e| wstart.as_deref().is_none_or(|w| e > w))
     367          110 :     });
     368          372 :     if !intersects {
     369          272 :         return false;
     370          100 :     }
     371          100 :     if let Some(o) = doc.as_object_mut() {
     372          592 :         for (k, v) in o.iter_mut() {
     373          592 :             if is_meta(k) {
     374          492 :                 continue;
     375          100 :             }
     376          100 :             if let Some(arr) = v.as_array_mut() {
     377          134 :                 arr.retain(|inst| {
     378          134 :                     inst.get(timeprop)
     379          134 :                         .and_then(Value::as_str)
     380          134 :                         .map(dt_key)
     381          134 :                         .is_some_and(|t| {
     382          134 :                             let t = t.as_str();
     383          134 :                             iv.iter()
     384          138 :                                 .any(|(s, e)| t >= s.as_str() && e.as_deref().is_none_or(|e| t < e))
     385          134 :                         })
     386          134 :                 });
     387            0 :             }
     388              :         }
     389            0 :     }
     390          100 :     true
     391          372 : }
        

Generated by: LCOV version 2.0-1