LCOV - code coverage report
Current view: top level - antares-api/src/temporal - retrieve.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 98.2 % 334 328
Test Date: 2026-11-02 13:21:27 Functions: 56.6 % 136 77

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! Retrieve temporal evolution of one Entity (5.7.3).
       3              : 
       4              : use super::*;
       5              : 
       6              : // ---------- GET /temporal/entities/{id} (5.7.3) ----------
       7              : 
       8         1040 : pub async fn retrieve_temporal(
       9         1040 :     State(st): State<AppState>,
      10         1040 :     Path(id): Path<String>,
      11         1040 :     CleanParams(params): CleanParams,
      12         1040 :     headers: HeaderMap,
      13         1040 : ) -> Response {
      14         1040 :     match retrieve_temporal_outer(&st, &id, &params, &headers).await {
      15          890 :         Ok(r) => r,
      16          150 :         Err(e) => e.into_response(),
      17              :     }
      18         1040 : }
      19              : 
      20              : /// 5.7.3.4 Retrieve Temporal Evolution: the EntityMap half of the clause is
      21              : /// the shared rule (`entity_maps::retrieve_with_map`); this is the retrieve
      22              : /// it wraps.
      23         1040 : pub(super) async fn retrieve_temporal_outer(
      24         1040 :     st: &AppState,
      25         1040 :     id: &str,
      26         1040 :     params: &HashMap<String, String>,
      27         1040 :     headers: &HeaderMap,
      28         1040 : ) -> ApiResult<Response> {
      29         1040 :     crate::entity_map::retrieve_with_map(st, id, params, headers, true, |map| async move {
      30         1040 :         retrieve_temporal_inner(st, id, params, headers, map.as_ref()).await
      31         2080 :     })
      32         1040 :     .await
      33         1040 : }
      34              : 
      35              : /// Vanilla: `check_params` already refused `versions` as unknown.
      36         1040 : pub(super) fn versions_asked(
      37         1040 :     _st: &AppState,
      38         1040 :     _params: &HashMap<String, String>,
      39         1040 :     _headers: &HeaderMap,
      40         1040 : ) -> ApiResult<bool> {
      41         1040 :     Ok(false)
      42         1040 : }
      43              : 
      44         1040 : pub(super) async fn retrieve_temporal_inner(
      45         1040 :     st: &AppState,
      46         1040 :     id: &str,
      47         1040 :     params: &HashMap<String, String>,
      48         1040 :     headers: &HeaderMap,
      49         1040 :     map: Option<&Value>,
      50         1040 : ) -> ApiResult<Response> {
      51         1040 :     let tenant = tenant_from(headers)?;
      52         1040 :     check_params(params, RETRIEVE_PARAMS)?;
      53         1040 :     let versions = versions_asked(st, params, headers)?;
      54         1040 :     let accept = parse_accept(headers)?;
      55         1040 :     let ctx = request_context(&st.loader, headers).await?;
      56         1040 :     let filter = gate!(st, &tenant, headers, "5.7.3", ids: &[id]).await?;
      57              :     // Every reader below takes the window clamped to the grant
      58         1040 :     let windowed = filter.narrow_window(params, false)?;
      59         1040 :     let params = &windowed;
      60         1040 :     let tq = TemporalQ::from_params(params, false)?;
      61         1028 :     let mut trepr = parse_trepr(params, &ctx)?;
      62         1000 :     crate::repr::narrow_projection(&mut trepr.pick, &mut trepr.omit, &filter, &ctx)?;
      63              :     // 5.7.3.4: "If projection attributes are present and indicate the
      64              :     // use of Linked Entity retrieval, an error of type BadRequestData
      65              :     // shall be raised" — unconditional, temporal defines no join.
      66         1000 :     reject_linked_projection(&trepr, "5.7.3.4")?;
      67          988 :     antares_model::EntityId::new(id)?;
      68          984 :     let types_rule = crate::repr::type_rules(&filter);
      69          984 :     let timeprop = tq
      70          984 :         .as_ref()
      71          984 :         .map_or("observedAt", |t| t.timeproperty.as_str())
      72          984 :         .to_owned();
      73          984 :     let req = RetrieveReq {
      74          984 :         st,
      75          984 :         tenant: &tenant,
      76          984 :         headers,
      77          984 :         ctx: &ctx,
      78          984 :         id,
      79          984 :         params,
      80          984 :         map,
      81          984 :     };
      82          984 :     let tf = retrieve_filter(
      83          984 :         tq.as_ref(),
      84          984 :         trepr.page,
      85          984 :         default_page(st, &tenant, &trepr),
      86          984 :         types_rule.as_deref(),
      87          984 :         &timeprop,
      88              :     );
      89          984 :     let mut warnings: Vec<String> = Vec::new();
      90          984 :     let doc = load_evolution(&req, &tf, &timeprop, &mut warnings).await?;
      91          922 :     let not_found = || NgsiError::ResourceNotFound(format!("temporal entity {id} not found"));
      92              :     // ADR-0020: an Entity outside the
      93              :     // narrowing now is the 404 of an absent one
      94          922 :     if !member_now(st, &tenant, &ctx, &filter, id, Some(&doc)).await? {
      95            4 :         return Err(not_found().into());
      96          918 :     }
      97              :     // the history its rules show, before anything
      98              :     // reads it; no rule for its types is the 404 of an absent Entity
      99          918 :     let doc = match types_rule.as_deref() {
     100           28 :         Some(r) => rules_per_instance(&doc, r).ok_or_else(not_found)?,
     101          890 :         None => doc,
     102              :     };
     103          916 :     let attrs_filter = selection(&trepr);
     104          916 :     check_selection(&doc, &trepr, attrs_filter.as_ref(), id)?;
     105          906 :     let mut w = window(
     106          906 :         &doc,
     107          906 :         tq.as_ref(),
     108          906 :         trepr.page,
     109          906 :         attrs_filter.as_ref(),
     110          906 :         trepr.omit.as_ref(),
     111          906 :         trepr.dataset_id.as_ref(),
     112          906 :         &timeprop,
     113          906 :         crate::history::held_attrs(st, &tenant, &doc),
     114              :     );
     115          906 :     if !trepr.aggregated {
     116          852 :         let page_n = trepr.page;
     117          852 :         truncate(
     118          852 :             &mut w,
     119          852 :             &timeprop,
     120          852 :             page_n.desc,
     121          852 :             page_n.size(st.temporal_default_instances, st.temporal_instance_limit),
     122              :         );
     123              :         // after the cut: lastN counts measurements, never versions
     124          852 :         if versions {
     125            0 :             with_versions(&mut w, &st.temporal.versions(&tenant, id).await?);
     126          852 :         }
     127           54 :     }
     128          906 :     let payload = present_temporal(&doc, &w, &ctx, &trepr, tq.as_ref(), &timeprop)?;
     129          894 :     if (trepr.pick.is_some() || trepr.omit.is_some())
     130           34 :         && payload.as_object().is_some_and(|o| o.is_empty())
     131              :     {
     132            4 :         return Err(NgsiError::ResourceNotFound(format!(
     133            4 :             "projection matches nothing on temporal entity {id}"
     134            4 :         ))
     135            4 :         .into());
     136          890 :     }
     137          890 :     let mut resp = respond_retrieved(&req, accept, &w, &trepr, tq.as_ref(), payload);
     138          890 :     crate::paging::attach_warnings(&mut resp, &warnings);
     139          890 :     filter.mark(&st.host_alias, resp.headers_mut());
     140          890 :     Ok(resp)
     141         1040 : }
     142              : 
     143              : /// The parameters 5.7.3 accepts (6.19.3.1 Table 6.19.3.1-1).
     144              : const RETRIEVE_PARAMS: &[&str] = &[
     145              :     "attrs",
     146              :     "timerel",
     147              :     "timeAt",
     148              :     "endTimeAt",
     149              :     "timeproperty",
     150              :     "lastN",
     151              :     "firstN",
     152              :     "offsetN",
     153              :     "aggrMethods",
     154              :     "aggrPeriodDuration",
     155              :     "options",
     156              :     "format",
     157              :     "lang",
     158              :     "local",
     159              :     "pick",
     160              :     "omit",
     161              :     "datasetId",
     162              :     "entityMap",
     163              :     "entityMapLifetime",
     164              : ];
     165              : 
     166              : /// One 5.7.3 request as the stages after the gate read it; `params` is
     167              : /// the window clamped to the grant.
     168              : struct RetrieveReq<'a> {
     169              :     st: &'a AppState,
     170              :     tenant: &'a TenantId,
     171              :     headers: &'a HeaderMap,
     172              :     ctx: &'a Context,
     173              :     id: &'a str,
     174              :     params: &'a HashMap<String, String>,
     175              :     map: Option<&'a Value>,
     176              : }
     177              : 
     178              : /// Instance pruning pushed into the store (no q=/geo on retrieve, so it
     179              : /// is always safe here); window() stays the arbiter. Under the narrowing rules, a
     180              : /// condition rebuilds each instance's state from the whole history, so
     181              : /// the store prunes no instance under one; rules without one read only
     182              : /// the window.
     183          984 : fn retrieve_filter<'a>(
     184          984 :     tq: Option<&'a TemporalQ>,
     185          984 :     page_n: InstancePage,
     186          984 :     default: Option<usize>,
     187          984 :     types_rule: Option<&crate::policy::Filter>,
     188          984 :     timeprop: &'a str,
     189          984 : ) -> antares_store::filter::TemporalFilter<'a> {
     190          984 :     let replays = replays_state(types_rule);
     191              :     antares_store::filter::TemporalFilter {
     192          984 :         range: tq
     193          984 :             .filter(|_| !replays)
     194          984 :             .map(|t| antares_store::filter::InstanceRange {
     195          422 :                 timerel: &t.timerel,
     196          422 :                 time_at: &t.time_at,
     197          422 :                 end_time_at: t.end_time_at.as_deref(),
     198          422 :                 timeproperty: &t.timeproperty,
     199          422 :             }),
     200          984 :         last_n: page_n
     201          984 :             .store_cap(default)
     202          984 :             .filter(|_| cap_keeps_shown(types_rule, &page_n, timeprop, range_start(tq))),
     203          984 :         oldest_first: !page_n.desc,
     204          984 :         timeproperty: timeprop,
     205          984 :         ..Default::default()
     206              :     }
     207          984 : }
     208              : 
     209              : /// The Entity's Temporal Evolution: the local one, and under 5.7.3.4 the
     210              : /// matching retrieveTemporal registrations' merged into it (4.5.5;
     211              : /// auxiliary instances only fill timestamps absent elsewhere).
     212          984 : async fn load_evolution(
     213          984 :     req: &RetrieveReq<'_>,
     214          984 :     tf: &antares_store::filter::TemporalFilter<'_>,
     215          984 :     timeprop: &str,
     216          984 :     warnings: &mut Vec<String>,
     217          984 : ) -> ApiResult<Value> {
     218          984 :     let (st, tenant, id) = (req.st, req.tenant, req.id);
     219          984 :     let local = match st.temporal.get_temporal(tenant, id, tf).await? {
     220          918 :         Some(doc) => Some(doc),
     221           62 :         None => kept_no_history(st, tenant, id).await?,
     222              :     };
     223          980 :     let looped = crate::federation::via_loop(
     224          980 :         req.headers,
     225          980 :         &crate::federation::alias_for(&st.host_alias, tenant),
     226              :     );
     227          980 :     if !crate::federation::active(req.params) || looped {
     228            0 :         return Ok(local.ok_or_else(|| {
     229            0 :             NgsiError::ResourceNotFound(format!("temporal entity {id} not found"))
     230            0 :         })?);
     231          980 :     }
     232          980 :     let mut proxies = Vec::new();
     233          980 :     let fed = crate::federation::fed_retrieve_temporal(
     234          980 :         st,
     235          980 :         tenant,
     236          980 :         req.headers,
     237          980 :         req.ctx,
     238          980 :         id,
     239          980 :         req.params,
     240          980 :         req.map,
     241          980 :         warnings,
     242          980 :         &mut proxies,
     243          980 :     )
     244          980 :     .await?;
     245          980 :     let mut local = local;
     246          980 :     if let Some(b) = local.as_mut() {
     247          920 :         crate::federation::drop_proxied_local(b, &proxies, req.ctx);
     248          920 :     }
     249          980 :     Ok(merge_retrieved(local, &fed, id, timeprop)?)
     250          984 : }
     251              : 
     252              : /// The local Evolution, or the first exclusive remote one when there is
     253              : /// none (any remote one when all are auxiliary), with every other remote
     254              : /// copy merged in, the exclusive ones first.
     255          980 : fn merge_retrieved(
     256          980 :     local: Option<Value>,
     257          980 :     fed: &[(bool, Value)],
     258          980 :     id: &str,
     259          980 :     timeprop: &str,
     260          980 : ) -> Result<Value, NgsiError> {
     261          980 :     let (mut base, skip) = match local {
     262          920 :         Some(b) => (b, None),
     263              :         None => {
     264           60 :             let idx = fed.iter().position(|(aux, _)| !aux).or(if fed.is_empty() {
     265           58 :                 None
     266              :             } else {
     267            2 :                 Some(0)
     268              :             });
     269           60 :             match idx.and_then(|i| fed.get(i).map(|(_, d)| (d.clone(), i))) {
     270            2 :                 Some((d, i)) => (d, Some(i)),
     271              :                 None => {
     272           58 :                     return Err(NgsiError::ResourceNotFound(format!(
     273           58 :                         "temporal entity {id} not found"
     274           58 :                     )))
     275              :                 }
     276              :             }
     277              :         }
     278              :     };
     279         1844 :     for aux_pass in [false, true] {
     280         1844 :         for (i, (aux, d)) in fed.iter().enumerate() {
     281            8 :             if Some(i) == skip {
     282            4 :                 continue;
     283            4 :             }
     284            4 :             if *aux == aux_pass {
     285            2 :                 merge_temporal_docs(&mut base, d, *aux, timeprop);
     286            2 :             }
     287              :         }
     288              :     }
     289          922 :     Ok(base)
     290          980 : }
     291              : 
     292              : /// 5.7.3: attrs matching nothing ⇒ 404. A `pick` may name core members
     293              : /// instead — 5.7.3.3 admits "id", "type", "scope" or an Attribute name
     294              : /// — and then it selects no Attribute at all. 5.7.3.5 still reduces the
     295              : /// Entity to the members it names, so that empty selection is the
     296              : /// answer rather than a Temporal Evolution holding none of it.
     297          916 : fn check_selection(
     298          916 :     doc: &Value,
     299          916 :     trepr: &TRepr,
     300          916 :     attrs_filter: Option<&Vec<String>>,
     301          916 :     id: &str,
     302          916 : ) -> Result<(), NgsiError> {
     303          916 :     let core_only_pick =
     304          916 :         trepr.attrs.is_none() && trepr.pick.is_some() && attrs_filter.is_some_and(Vec::is_empty);
     305          150 :     match attrs_filter {
     306          150 :         Some(want) if !core_only_pick && !want.iter().any(|a| doc.get(a).is_some()) => {
     307           10 :             Err(NgsiError::ResourceNotFound(format!(
     308           10 :                 "temporal entity {id} has none of the requested attributes"
     309           10 :             )))
     310              :         }
     311          906 :         _ => Ok(()),
     312              :     }
     313          916 : }
     314              : 
     315              : /// The Evolution's body, 200 or 206 with its Content-Range, and the
     316              : /// 6.4.7.3 links to the next page.
     317          890 : fn respond_retrieved(
     318          890 :     req: &RetrieveReq<'_>,
     319          890 :     accept: Accept,
     320          890 :     w: &Windowed,
     321          890 :     trepr: &TRepr,
     322          890 :     tq: Option<&TemporalQ>,
     323          890 :     payload: Value,
     324          890 : ) -> Response {
     325          890 :     let (st, page_n) = (req.st, trepr.page);
     326              :     // aggregated responses are complete by construction — never 206 (6.3.10)
     327          890 :     let cr = if trepr.aggregated {
     328           42 :         None
     329              :     } else {
     330          848 :         content_range(
     331          848 :             w.truncated,
     332          848 :             w.ts_min.as_deref(),
     333          848 :             w.ts_max.as_deref(),
     334          848 :             tq,
     335          848 :             page_n,
     336              :         )
     337              :     };
     338          890 :     let status = if cr.is_some() {
     339          120 :         StatusCode::PARTIAL_CONTENT
     340              :     } else {
     341          770 :         StatusCode::OK
     342              :     };
     343          890 :     let mut resp = respond(status, payload, req.ctx, accept, req.tenant);
     344          890 :     attach_paging(
     345          890 :         &mut resp,
     346          890 :         None,
     347          890 :         &interval_links(
     348          890 :             req.params,
     349          890 :             &format!(
     350          890 :                 "/ngsi-ld/v1/temporal/entities/{}",
     351          890 :                 crate::federation::path_segment(req.id)
     352          890 :             ),
     353          890 :             accept,
     354          890 :             page_n,
     355          890 :             w.more,
     356          890 :             page_n.step(
     357          890 :                 trepr.aggregated,
     358          890 :                 st.temporal_default_instances,
     359          890 :                 st.temporal_instance_limit,
     360          890 :             ),
     361          890 :             w.cut_served.zip(cut_boundary(w, page_n.desc)),
     362          890 :         ),
     363              :     );
     364          890 :     if let Some(cr) = cr {
     365          120 :         if let Ok(v) = cr.parse() {
     366          120 :             resp.headers_mut().insert("Content-Range", v);
     367          120 :         }
     368          770 :     }
     369          890 :     resp
     370          890 : }
     371              : 
     372              : /// 5.7.3.4 / 4.5.5: merge one remote Temporal Evolution into `base` by
     373              : /// appending instances per Attribute; auxiliary data contributes an
     374              : /// instance only when no instance with the same timeproperty value was
     375              : /// received from elsewhere.
     376           24 : pub(super) fn merge_temporal_docs(base: &mut Value, add: &Value, aux: bool, timeprop: &str) {
     377           24 :     let (Some(target), Some(source)) = (base.as_object_mut(), add.as_object()) else {
     378            0 :         return;
     379              :     };
     380           72 :     for (k, v) in source {
     381           72 :         if [
     382           72 :             "id",
     383           72 :             "type",
     384           72 :             "scope",
     385           72 :             "createdAt",
     386           72 :             "modifiedAt",
     387           72 :             "deletedAt",
     388           72 :             "expiresAt",
     389           72 :         ]
     390           72 :         .contains(&k.as_str())
     391              :         {
     392           48 :             target.entry(k.clone()).or_insert_with(|| v.clone());
     393           48 :             continue;
     394           24 :         }
     395           24 :         let incoming: Vec<Value> = match v {
     396           24 :             Value::Array(x) => x.clone(),
     397            0 :             other => vec![other.clone()],
     398              :         };
     399           24 :         match target.get_mut(k).and_then(Value::as_array_mut) {
     400            4 :             None => {
     401            4 :                 target.insert(k.clone(), Value::Array(incoming));
     402            4 :             }
     403           20 :             Some(cur) => {
     404           32 :                 for ni in incoming {
     405           32 :                     if aux {
     406           12 :                         let ts = ni.get(timeprop).and_then(Value::as_str).map(dt_key);
     407           12 :                         if ts.is_some()
     408           12 :                             && cur.iter().any(|ci| {
     409           12 :                                 ci.get(timeprop).and_then(Value::as_str).map(dt_key) == ts
     410           12 :                             })
     411              :                         {
     412            8 :                             continue;
     413            4 :                         }
     414           20 :                     }
     415              :                     // 4.5.5.3: an instance with the same datasetId (or both
     416              :                     // default) AND the same timeproperty value is a
     417              :                     // CONFLICTING instance of one slot — resolve to one, the
     418              :                     // most recent modifiedAt winning.
     419              :                     // Two instances only share a slot when they carry the SAME
     420              :                     // timeproperty value. Without the is_some() guard a pair of
     421              :                     // instances that both lack it would compare None == None,
     422              :                     // letting a remote instance replace unrelated local history.
     423              :                     // 4.6.3 leaves the seconds fraction optional, so two
     424              :                     // Context Sources may spell one instant differently: the
     425              :                     // slot and its winner are decided on the canonical key,
     426              :                     // or the same instance comes back twice.
     427           24 :                     let ni_ts = ni.get(timeprop).and_then(Value::as_str).map(dt_key);
     428           24 :                     let slot = ni_ts.and_then(|ts| {
     429           28 :                         cur.iter_mut().find(|ci| {
     430           28 :                             ci.get(timeprop).and_then(Value::as_str).map(dt_key) == Some(ts.clone())
     431           16 :                                 && ci.get("datasetId") == ni.get("datasetId")
     432           28 :                         })
     433           24 :                     });
     434           24 :                     if let Some(existing) = slot {
     435           24 :                         let stamp = |i: &Value| {
     436           24 :                             dt_key(i.get("modifiedAt").and_then(Value::as_str).unwrap_or(""))
     437           24 :                         };
     438           12 :                         let newer = stamp(&ni) > stamp(existing);
     439           12 :                         if newer {
     440           12 :                             *existing = ni;
     441           12 :                         }
     442           12 :                         continue;
     443           12 :                     }
     444           12 :                     cur.push(ni);
     445              :                 }
     446              :             }
     447              :         }
     448              :     }
     449           24 : }
     450              : 
     451              : /// The Entity `id` whose retention keeps no history (a type no
     452              : /// rule opts in, or `off`), as a Temporal Evolution of no Attribute: it
     453              : /// exists and recorded nothing, which is no 404. The member checks after
     454              : /// the read hold for it as for any other.
     455           62 : pub(super) async fn kept_no_history(
     456           62 :     st: &AppState,
     457           62 :     tenant: &TenantId,
     458           62 :     id: &str,
     459           62 : ) -> Result<Option<Value>, ApiError> {
     460           62 :     let Some(doc) = st
     461           62 :         .store
     462           62 :         .get(tenant, antares_store::Kind::Entity, id)
     463           62 :         .await?
     464              :     else {
     465           46 :         return Ok(None);
     466              :     };
     467           16 :     if crate::history::records_history(st, tenant, &doc) {
     468           14 :         return Ok(None);
     469            2 :     }
     470            2 :     let mut shell = serde_json::Map::new();
     471            6 :     for k in ["id", "type", "scope"] {
     472            6 :         if let Some(v) = doc.get(k) {
     473            4 :             shell.insert(k.to_owned(), v.clone());
     474            4 :         }
     475              :     }
     476            2 :     Ok(Some(Value::Object(shell)))
     477           62 : }
        

Generated by: LCOV version 2.0-1