LCOV - code coverage report
Current view: top level - antares-api/src/temporal - mod.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 97.7 % 43 42
Test Date: 2026-11-02 13:21:27 Functions: 72.9 % 48 35

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! /temporal/entities (5.6.11–5.6.16, 5.7.3/5.7.4; resources 6.18–6.22).
       3              : 
       4              : use crate::negotiate::*;
       5              : use crate::state::{now_iso, AppState};
       6              : use crate::temporalq::TemporalQ;
       7              : use antares_jsonld::compact::compact_instance;
       8              : use antares_jsonld::{expand_entity, Context, ExpandOpts};
       9              : use antares_model::{dt_key, NgsiError, TenantId};
      10              : use antares_ql::parse_q;
      11              : use antares_store::TemporalDriverExt as _;
      12              : use axum::body::Bytes;
      13              : use axum::extract::{Path, State};
      14              : use axum::http::{HeaderMap, StatusCode};
      15              : use axum::response::{IntoResponse, Response};
      16              : use serde_json::{json, Map, Value};
      17              : use std::collections::HashMap;
      18              : 
      19              : use crate::negotiate::CleanParams;
      20              : 
      21              : use antares_model::is_meta;
      22              : 
      23              : mod aggregate;
      24              : mod filter;
      25              : mod query;
      26              : mod render;
      27              : mod retrieve;
      28              : #[cfg(test)]
      29              : mod tests;
      30              : mod write;
      31              : 
      32              : use aggregate::*;
      33              : pub(crate) use filter::*;
      34              : pub use query::*;
      35              : pub(crate) use render::*;
      36              : pub use retrieve::*;
      37              : pub use write::*;
      38              : 
      39              : // ---------- temporal query params (4.11) ----------
      40              : 
      41              : /// Windowed per-entity temporal data: filtered+ordered instances per attr.
      42              : struct Windowed {
      43              :     attrs: std::collections::BTreeMap<String, Vec<Value>>,
      44              :     max_per_attr: usize,
      45              :     ts_min: Option<String>,
      46              :     ts_max: Option<String>,
      47              :     truncated: bool,
      48              :     /// some Attribute holds instances past this page in its order: the
      49              :     /// response links to the next page (6.4.7.3)
      50              :     more: bool,
      51              :     /// the fewest instances an Attribute the ceiling cut shortened still
      52              :     /// served (`truncate`): how far the next offsetN page may move
      53              :     cut_served: Option<usize>,
      54              :     /// Attributes their type records `onChange`: each value
      55              :     /// holds until the next row, so a window starts at the level carried in
      56              :     /// and an aggregation weighs a value by the time it held
      57              :     held: std::collections::HashSet<String>,
      58              : }
      59              : 
      60              : /// TS 104 176 6.4.7.3: which instances of each Attribute one response
      61              : /// carries. firstN pages oldest first, lastN newest first, neither oldest
      62              : /// first; offsetN skips that many in the chosen order.
      63              : #[derive(Default, Clone, Copy, Debug, PartialEq)]
      64              : pub(crate) struct InstancePage {
      65              :     /// lastN: newest first
      66              :     desc: bool,
      67              :     /// the firstN or lastN asked for
      68              :     n: Option<usize>,
      69              :     /// offsetN
      70              :     offset: usize,
      71              : }
      72              : 
      73              : impl InstancePage {
      74              :     /// firstN, lastN and offsetN, validated: N positive (5.2.21), offsetN
      75              :     /// not negative (Table 6.4.7.3-2), both at most i64::MAX (bound as a
      76              :     /// `bigint` they would wrap negative and silently select nothing).
      77              :     /// firstN asks for ascending order and lastN for descending; one page
      78              :     /// cannot be both, so the pair is BadRequestData.
      79         2128 :     fn parse(params: &HashMap<String, String>) -> Result<Self, NgsiError> {
      80         6362 :         let count = |name: &str, min: usize| -> Result<Option<usize>, NgsiError> {
      81         6362 :             params
      82         6362 :                 .get(name)
      83         6362 :                 .map(|v| {
      84          448 :                     v.parse::<usize>()
      85          448 :                         .ok()
      86          448 :                         .filter(|n| *n >= min && *n <= i64::MAX as usize)
      87          448 :                         .ok_or_else(|| NgsiError::BadRequestData(format!("invalid {name} {v:?}")))
      88          448 :                 })
      89         6362 :                 .transpose()
      90         6362 :         };
      91         2128 :         let (first, last) = (count("firstN", 1)?, count("lastN", 1)?);
      92         2116 :         if first.is_some() && last.is_some() {
      93            6 :             return Err(NgsiError::BadRequestData(
      94            6 :                 "firstN pages ascending and lastN descending: give one of them (6.4.7.3)".into(),
      95            6 :             ));
      96         2110 :         }
      97              :         Ok(Self {
      98         2110 :             desc: last.is_some(),
      99         2110 :             n: first.or(last),
     100         2110 :             offset: count("offsetN", 0)?.unwrap_or(0),
     101              :         })
     102         2128 :     }
     103              : 
     104              :     /// The most instances of one Attribute a page holds: N, or the broker's
     105              :     /// ceiling when N is above it (6.4.7.3 "whichever is smaller"); with no
     106              :     /// N, the broker's default, at most the ceiling.
     107         3872 :     fn size(&self, default: usize, limit: usize) -> usize {
     108         3872 :         self.n.map_or(default.min(limit), |n| n.min(limit))
     109         3872 :     }
     110              : 
     111              :     /// How far one page moves offsetN: its size, or N itself for an
     112              :     /// aggregated representation, which the ceiling never cuts.
     113         1890 :     fn step(&self, aggregated: bool, default: usize, limit: usize) -> usize {
     114         1890 :         match (aggregated, self.n) {
     115            0 :             (true, Some(n)) => n,
     116         1890 :             _ => self.size(default, limit),
     117              :         }
     118         1890 :     }
     119              : 
     120              :     /// The per-Attribute cap a store may apply before the window: every
     121              :     /// instance up to the end of this page in its order, and one more, so
     122              :     /// the window still sees whether a next page exists. Without N the page
     123              :     /// is `default` instances, the broker's default page, when the caller
     124              :     /// pages instances at all (`None` for an aggregation, which reads the
     125              :     /// whole window): a chart's plain window read paged by offsetN used to
     126              :     /// rebuild every instance of the window per page.
     127         2020 :     fn store_cap(&self, default: Option<usize>) -> Option<i64> {
     128         2020 :         self.n.or(default).map(|n| {
     129         1878 :             n.saturating_add(self.offset)
     130         1878 :                 .saturating_add(1)
     131         1878 :                 .min(i64::MAX as usize) as i64
     132         1878 :         })
     133         2020 :     }
     134              : }
     135              : 
     136              : /// The page a read without firstN/lastN serves per Attribute, for the
     137              : /// store's cap: none for an aggregation (it reads the whole window) or a
     138              : /// tenant recording `onChange`, whose window starts at the level carried in.
     139         2008 : fn default_page(st: &AppState, tenant: &TenantId, trepr: &TRepr) -> Option<usize> {
     140         2008 :     (!trepr.aggregated && !crate::history::records_on_change(st, tenant)).then(|| {
     141         1782 :         st.temporal_default_instances
     142         1782 :             .min(st.temporal_instance_limit)
     143         1782 :     })
     144         2008 : }
     145              : 
     146              : /// Parsed temporal representation params (options/format/lastN/pick/omit/
     147              : /// datasetId/aggregation), fully validated up front.
     148              : #[derive(Default, Clone)]
     149              : pub(crate) struct TRepr {
     150              :     temporal_values: bool,
     151              :     aggregated: bool,
     152              :     sys: bool,
     153              :     page: InstancePage,
     154              :     pick: Option<Vec<crate::repr::ProjNode>>,
     155              :     omit: Option<Vec<crate::repr::ProjNode>>,
     156              :     dataset_id: Option<Vec<String>>,
     157              :     attrs: Option<Vec<String>>,
     158              :     aggr_methods: Vec<String>,
     159              :     aggr_period: AggrPeriod,
     160              :     lang: Option<String>,
     161              : }
     162              : 
     163              : #[derive(Clone, Copy, Debug, Default, PartialEq)]
     164              : pub(crate) enum AggrPeriod {
     165              :     /// PT0S / absent: one bucket over the whole range
     166              :     #[default]
     167              :     Whole,
     168              :     Seconds(i64),
     169              :     /// 4.5.19.1: a period may mix date and time elements
     170              :     /// ("P3Y6M4DT12H30M5S"), so a month step carries the leftover seconds —
     171              :     /// months are not a fixed number of seconds and cannot be folded in.
     172              :     Months(u32, i64),
     173              : }
     174              : 
     175              : /// 5.6.13.4-5.6.15.4 shared forwarding: proxy registrations without the
     176              : /// operation's support are an error of type Conflict and are never
     177              : /// contacted; supporting registrations receive the forwarded request. None
     178              : /// = no matching registrations (the operation stays purely local).
     179              : /// `path_suffix` arrives with its client-controlled segments already
     180              : /// percent-encoded (RFC 3986 clause 3.3); the entity id is encoded here.
     181              : /// What the local `mutate` did, in the words the answer needs. 5.6.13,
     182              : /// 5.6.14, 5.6.15 and 5.6.16 all answer the same three ways: 204 when the
     183              : /// target was there, ResourceNotFound naming the Entity when the Entity was
     184              : /// not, and ResourceNotFound naming the Attribute or the instance when the
     185              : /// Entity was there but the target inside it was not.
     186              : struct LocalWrite {
     187              :     /// `None` when the Temporal Evolution itself is absent.
     188              :     res: Option<Result<(), NgsiError>>,
     189              :     /// whether the operation's target inside it was there.
     190              :     found: bool,
     191              :     /// what a 404 names once the Entity itself was found.
     192              :     missing: String,
     193              :     /// what the 204 Part reports was done here.
     194              :     applied: &'static str,
     195              : }
        

Generated by: LCOV version 2.0-1