LCOV - code coverage report
Current view: top level - antares-api/src/notify - payload.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 98.5 % 272 268
Test Date: 2026-11-02 13:21:27 Functions: 60.4 % 144 87

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! Notification body construction (5.3.1, 5.8.6): the per-subscription shape, tombstones of deleted attributes and the data members.
       3              : 
       4              : use super::*;
       5              : 
       6              : /// Per-type NGSI-LD-null member and its showChanges previous-member (5.8.6).
       7          476 : pub(super) fn null_members(atype: &str) -> (&'static str, Value, &'static str) {
       8          476 :     let null = Value::String("urn:ngsi-ld:null".into());
       9          476 :     match atype {
      10          476 :         "Relationship" => ("object", null, "previousObject"),
      11          392 :         "LanguageProperty" => (
      12          100 :             "languageMap",
      13          100 :             json!({"@none": "urn:ngsi-ld:null"}),
      14          100 :             "previousLanguageMap",
      15          100 :         ),
      16          292 :         "JsonProperty" => ("json", null, "previousJson"),
      17          272 :         "VocabProperty" => ("vocab", null, "previousVocab"),
      18          252 :         _ => ("value", null, "previousValue"),
      19              :     }
      20          476 : }
      21              : 
      22          132 : pub(super) fn current_member(atype: &str) -> &'static str {
      23          132 :     null_members(atype).0
      24          132 : }
      25              : 
      26              : /// The deletion tombstone for one former attribute instance (5.8.6 payload
      27              : /// forms: typed null + optional datasetId / sysAttrs stamps / previous value).
      28          212 : pub(super) fn tombstone(before_inst: &Value, sys: bool, show: bool, now: &str) -> Value {
      29          212 :     let atype = before_inst
      30          212 :         .get("type")
      31          212 :         .and_then(Value::as_str)
      32          212 :         .unwrap_or("Property");
      33          212 :     let (member, null_val, prev_member) = null_members(atype);
      34          212 :     let mut m = Map::new();
      35          212 :     m.insert("type".into(), Value::String(atype.to_owned()));
      36          212 :     m.insert(member.into(), null_val);
      37          212 :     if let Some(ds) = before_inst.get("datasetId") {
      38           32 :         m.insert("datasetId".into(), ds.clone());
      39          180 :     }
      40          212 :     if sys {
      41           40 :         for k in ["createdAt", "modifiedAt"] {
      42           40 :             if let Some(v) = before_inst.get(k) {
      43           20 :                 m.insert(k.into(), v.clone());
      44           20 :             }
      45              :         }
      46           20 :         m.insert("deletedAt".into(), Value::String(now.to_owned()));
      47          192 :     }
      48          212 :     if show {
      49           60 :         if let Some(prev) = before_inst.get(member) {
      50           60 :             m.insert(prev_member.into(), prev.clone());
      51           60 :         }
      52          152 :     }
      53          212 :     Value::Object(m)
      54          212 : }
      55              : 
      56       323624 : pub(super) fn instance_ds(inst: &Value) -> Option<&str> {
      57       323624 :     inst.get("datasetId").and_then(Value::as_str)
      58       323624 : }
      59              : 
      60              : /// Instances of `before[attr]` that no longer exist in `after[attr]`
      61              : /// (matched by datasetId — instance-level deletions count, 046_22_06).
      62          164 : pub(super) fn deleted_instances<'a>(
      63          164 :     before: &'a Value,
      64          164 :     after: Option<&Value>,
      65          164 :     attr: &str,
      66          164 : ) -> Vec<&'a Value> {
      67          164 :     let b: Vec<&Value> = before
      68          164 :         .get(attr)
      69          164 :         .and_then(Value::as_array)
      70          164 :         .map(|a| a.iter().collect())
      71          164 :         .unwrap_or_default();
      72          164 :     let a: Vec<&Value> = after
      73          164 :         .and_then(|d| d.get(attr))
      74          164 :         .and_then(Value::as_array)
      75          164 :         .map(|x| x.iter().collect())
      76          164 :         .unwrap_or_default();
      77          164 :     b.into_iter()
      78          208 :         .filter(|bi| !a.iter().any(|ai| instance_ds(ai) == instance_ds(bi)))
      79          164 :         .collect()
      80          164 : }
      81              : 
      82              : pub(crate) struct NotifShape {
      83              :     pub(crate) repr: crate::repr::Repr,
      84              :     pub(crate) show_changes: bool,
      85              :     pub(crate) join: Option<(String, usize)>,
      86              : }
      87              : 
      88         5020 : pub(crate) fn notif_shape(sub: &Value, ctx: &Context) -> NotifShape {
      89         5020 :     let n = sub.get("notification").and_then(Value::as_object);
      90         5020 :     let format = n
      91         5020 :         .and_then(|n| n.get("format"))
      92         5020 :         .and_then(Value::as_str)
      93         5020 :         .unwrap_or("normalized");
      94         5020 :     let mut repr = crate::repr::Repr {
      95         5020 :         sys_attrs: n
      96         5020 :             .and_then(|n| n.get("sysAttrs"))
      97         5020 :             .and_then(Value::as_bool)
      98         5020 :             .unwrap_or(false),
      99         5020 :         key_values: matches!(format, "keyValues" | "simplified"),
     100         5020 :         concise: format == "concise",
     101              :         // Table 5.2.12-1: "Language filter to be applied to the query (clause 4.15)"
     102         5020 :         lang: sub.get("lang").and_then(Value::as_str).map(str::to_owned),
     103         5020 :         ..Default::default()
     104              :     };
     105        15060 :     let names = |key: &str| -> Option<Vec<String>> {
     106        15060 :         n.and_then(|n| n.get(key))
     107        15060 :             .and_then(Value::as_array)
     108        15060 :             .map(|a| {
     109           52 :                 a.iter()
     110           52 :                     .filter_map(Value::as_str)
     111           52 :                     .map(str::to_owned)
     112           52 :                     .collect()
     113           52 :             })
     114        15060 :     };
     115         5020 :     if let Some(attrs) = names("attributes") {
     116           20 :         repr.attrs = Some(attrs.iter().map(|a| ctx.expand_key(a)).collect());
     117         5000 :     }
     118              :     // 4.21 NGSI-LD Attribute Projection Language: pick/omit values are
     119              :     // projection language strings, which Table 5.2.14.1-1 requires for the
     120              :     // notification members too ("a valid attribute projection language string
     121              :     // as per clause 4.21"). Each term may carry a LinkedEntityTerm
     122              :     // (`ProjectionTerm = AttrName *1(LinkedEntityTerm)`), which is what
     123              :     // constrains an Attribute inside a Linked Entity retrieved by join. Building
     124              :     // the nodes by hand here degraded `refDevice{type}` to a literal Attribute
     125              :     // name matching nothing, so the term was dropped instead of applied.
     126              :     // A term that fails to parse is dropped rather than kept flat: for pick that
     127              :     // withholds the Attribute, which is the safe direction.
     128         5020 :     let nodes = |list: Vec<String>| -> Vec<crate::repr::ProjNode> {
     129           32 :         list.iter()
     130           82 :             .filter_map(|term| crate::repr::parse_projection(term, ctx).ok())
     131           32 :             .flatten()
     132           32 :             .collect()
     133           32 :     };
     134         5020 :     if let Some(pick) = names("pick") {
     135           26 :         repr.pick = Some(nodes(pick));
     136         4994 :     }
     137         5020 :     if let Some(omit) = names("omit") {
     138            6 :         repr.omit = Some(nodes(omit));
     139         5014 :     }
     140         5020 :     if let Some(ds) = sub.get("datasetId").and_then(Value::as_array) {
     141            4 :         repr.dataset_id = Some(
     142            4 :             ds.iter()
     143            4 :                 .filter_map(Value::as_str)
     144            4 :                 .map(str::to_owned)
     145            4 :                 .collect(),
     146            4 :         );
     147         5016 :     }
     148         5020 :     let join = n
     149         5020 :         .and_then(|n| n.get("join"))
     150         5020 :         .and_then(Value::as_str)
     151         5020 :         .filter(|j| *j == "inline" || *j == "flat")
     152         5020 :         .map(|j| {
     153              :             // the stored member is bounded at creation, but a Subscription
     154              :             // written before that bound existed is still on disk, so the
     155              :             // traversal takes the ceiling from here too
     156           56 :             let level = (n
     157           56 :                 .and_then(|n| n.get("joinLevel"))
     158           56 :                 .and_then(Value::as_u64)
     159           56 :                 .map_or(1, |n| usize::try_from(n).unwrap_or(usize::MAX)))
     160           56 :             .min(crate::bounds::MAX_JOIN_LEVEL);
     161           56 :             (j.to_owned(), level)
     162           56 :         });
     163              :     NotifShape {
     164         5020 :         repr,
     165         5020 :         show_changes: n
     166         5020 :             .and_then(|n| n.get("showChanges"))
     167         5020 :             .and_then(Value::as_bool)
     168         5020 :             .unwrap_or(false),
     169         5020 :         join,
     170              :     }
     171         5020 : }
     172              : 
     173              : /// Everything [`build_data`] reads of one Subscription for one change, as a
     174              : /// memo key: the members [`notif_shape`] reads, the deleted Attributes its
     175              : /// triggers name and the `@context` it compacts with. Subscriptions with
     176              : /// equal keys get equal payloads, so 1 000 subscriptions of one view render
     177              : /// one. The `@context` counts by identity: the drain's memo hands
     178              : /// every Subscription naming one URL the same `Arc`, and a key holds an
     179              : /// address only while its `Arc` lives in a matched group, so no other
     180              : /// `@context` can take that address in the meantime.
     181         9058 : pub(super) fn payload_key(
     182         9058 :     sub: &Value,
     183         9058 :     ctx: &Arc<Context>,
     184         9058 :     deleted: &[String],
     185         9058 :     entity_deleted: bool,
     186         9058 : ) -> String {
     187         9058 :     let n = sub.get("notification");
     188        72464 :     let member = |k: &str| n.and_then(|n| n.get(k));
     189         9058 :     json!([
     190         9058 :         Arc::as_ptr(ctx).addr(),
     191         9058 :         member("format"),
     192         9058 :         member("sysAttrs"),
     193         9058 :         member("attributes"),
     194         9058 :         member("pick"),
     195         9058 :         member("omit"),
     196         9058 :         member("showChanges"),
     197         9058 :         member("join"),
     198         9058 :         member("joinLevel"),
     199         9058 :         sub.get("lang"),
     200         9058 :         sub.get("datasetId"),
     201              :         deleted,
     202              :         entity_deleted,
     203              :     ])
     204         9058 :     .to_string()
     205         9058 : }
     206              : 
     207              : /// Build the notification `data` array for one change and one subscription.
     208              : #[allow(clippy::too_many_arguments)] // one param per 5.8.6 payload input
     209         4956 : pub(super) async fn build_data(
     210         4956 :     st: &AppState,
     211         4956 :     tenant: &TenantId,
     212         4956 :     sub: &Value,
     213         4956 :     ctx: &Context,
     214         4956 :     before: Option<&Value>,
     215         4956 :     after: Option<&Value>,
     216         4956 :     relevant_deleted_attrs: &[String],
     217         4956 :     entity_deleted_fired: bool,
     218         4956 :     now: &str,
     219         4956 : ) -> Vec<Value> {
     220         4956 :     let shape = notif_shape(sub, ctx);
     221         4956 :     let sys = shape.repr.sys_attrs;
     222         4956 :     let show = shape.show_changes;
     223         4956 :     let internal = match after {
     224         4924 :         Some(a) => {
     225         4924 :             let mut doc = a.clone();
     226         4924 :             if show {
     227              :                 // previous* on changed instances (046_31..33). An entity off
     228              :                 // the change feed is an object; one that is not carries
     229              :                 // nothing to decorate and travels on unchanged.
     230           68 :                 if let (Some(b), Some(obj)) = (before, doc.as_object_mut()) {
     231          376 :                     for (k, v) in obj.iter_mut() {
     232          376 :                         if crate::repr::ENTITY_META.contains(&k.as_str()) {
     233          240 :                             continue;
     234          136 :                         }
     235          136 :                         let Some(arr) = v.as_array_mut() else {
     236            0 :                             continue;
     237              :                         };
     238          136 :                         for inst in arr {
     239          136 :                             let Some(bi) = b.get(k).and_then(Value::as_array).and_then(|ba| {
     240          132 :                                 ba.iter().find(|x| instance_ds(x) == instance_ds(inst))
     241          132 :                             }) else {
     242            4 :                                 continue;
     243              :                             };
     244          132 :                             let atype = inst
     245          132 :                                 .get("type")
     246          132 :                                 .and_then(Value::as_str)
     247          132 :                                 .unwrap_or("Property");
     248          132 :                             let member = current_member(atype);
     249          132 :                             let (_, _, prev_member) = null_members(atype);
     250          132 :                             if bi.get(member) != inst.get(member) {
     251           34 :                                 if let (Some(pv), Some(io)) =
     252           34 :                                     (bi.get(member).cloned(), inst.as_object_mut())
     253           34 :                                 {
     254           34 :                                     io.insert(prev_member.into(), pv);
     255           34 :                                 }
     256           98 :                             }
     257              :                         }
     258              :                     }
     259            4 :                 }
     260         4856 :             }
     261              :             // deletion tombstones appended beside surviving instances
     262         4924 :             if let Some(b) = before {
     263          328 :                 for attr in relevant_deleted_attrs {
     264          164 :                     let gone = deleted_instances(b, Some(&doc), attr);
     265          164 :                     if gone.is_empty() {
     266            0 :                         continue;
     267          164 :                     }
     268              :                     // one tombstone per deleted instance, each with its
     269              :                     // datasetId, deleteAll included (046_22_08)
     270          164 :                     let tss: Vec<Value> = gone
     271          164 :                         .iter()
     272          180 :                         .map(|di| tombstone(di, sys, show, now))
     273          164 :                         .collect();
     274          164 :                     let Some(target) = doc.as_object_mut() else {
     275            0 :                         continue;
     276              :                     };
     277          164 :                     if let Some(arr) = target
     278          164 :                         .entry(attr.clone())
     279          164 :                         .or_insert_with(|| Value::Array(vec![]))
     280          164 :                         .as_array_mut()
     281          164 :                     {
     282          164 :                         arr.extend(tss);
     283          164 :                     }
     284              :                 }
     285         4596 :             }
     286         4924 :             doc
     287              :         }
     288              :         None => {
     289              :             // entity deleted: tombstone entity (046_21) + per-trigger attrs.
     290              :             // The caller returns early unless one of before/after is there,
     291              :             // so this arm has a before; without one there is nothing to
     292              :             // describe and nothing to notify about.
     293           32 :             let Some(b) = before else {
     294            0 :                 return Vec::new();
     295              :             };
     296           32 :             let mut m = Map::new();
     297           64 :             for k in ["id", "type"] {
     298           64 :                 if let Some(v) = b.get(k) {
     299           64 :                     m.insert(k.into(), v.clone());
     300           64 :                 }
     301              :             }
     302           32 :             if sys {
     303           16 :                 for k in ["createdAt", "modifiedAt"] {
     304           16 :                     if let Some(v) = b.get(k) {
     305           16 :                         m.insert(k.into(), v.clone());
     306           16 :                     }
     307              :                 }
     308           24 :             }
     309           32 :             m.insert("deletedAt".into(), Value::String(now.to_owned()));
     310              :             // 5.8.6: an Entity deletion notifies "only the deletedAt system
     311              :             // property" unless showChanges, then every attribute, tombstoned
     312              :             // (046_37); attributeDeleted alone names the attributes it fired for
     313           32 :             let attrs: Vec<String> = match (entity_deleted_fired, show) {
     314           12 :                 (true, true) => attr_keys(b),
     315           12 :                 (true, false) => Vec::new(),
     316            8 :                 (false, _) => relevant_deleted_attrs.to_vec(),
     317              :             };
     318           32 :             for attr in attrs {
     319           32 :                 let insts: Vec<Value> = b
     320           32 :                     .get(&attr)
     321           32 :                     .and_then(Value::as_array)
     322           32 :                     .map(|a| a.iter().map(|i| tombstone(i, sys, show, now)).collect())
     323           32 :                     .unwrap_or_default();
     324           32 :                 if !insts.is_empty() {
     325           32 :                     m.insert(attr, Value::Array(insts));
     326           32 :                 }
     327              :             }
     328           32 :             Value::Object(m)
     329              :         }
     330              :     };
     331         4956 :     let shaped = crate::repr::apply(&internal, &shape.repr);
     332         4956 :     let mut main = crate::repr::compact_for(&shape.repr, &shaped, ctx);
     333         4956 :     let mut data = Vec::new();
     334           24 :     match &shape.join {
     335           56 :         Some((mode, level)) if mode == "inline" => {
     336           32 :             crate::repr::inline_join(st, tenant, ctx, &shape.repr, &mut main, &internal, *level)
     337           32 :                 .await;
     338           32 :             data.push(main);
     339              :         }
     340           24 :         Some((mode, level)) if mode == "flat" => {
     341           24 :             let main_id = internal.get("id").and_then(Value::as_str).unwrap_or("");
     342           24 :             let mut linked = std::collections::BTreeMap::new();
     343           24 :             crate::repr::collect_flat(st, tenant, ctx, &shape.repr, &internal, *level, &mut linked)
     344           24 :                 .await;
     345           24 :             data.push(main);
     346           24 :             for (id, (ldoc, lrepr)) in linked {
     347           24 :                 if id != main_id {
     348           24 :                     data.push(crate::repr::compact_for(
     349           24 :                         &lrepr,
     350           24 :                         &crate::repr::apply(&ldoc, &lrepr),
     351           24 :                         ctx,
     352           24 :                     ));
     353           24 :                 }
     354              :             }
     355              :         }
     356         4900 :         _ => data.push(main),
     357              :     }
     358         4956 :     data
     359         4956 : }
        

Generated by: LCOV version 2.0-1