Line data Source code
1 : // SPDX-License-Identifier: EUPL-1.2
2 : //! Registration matching over a registration document: the `CsrSpec`
3 : //! of a request or subscription, the 5.9 information/registration
4 : //! match (4.3.6.1), the csf and scope filters, the temporal interval
5 : //! and expiry of a registration, and the 5.11.2 subscription match.
6 : //! Pure functions over documents; no route and no store.
7 :
8 : use antares_jsonld::Context;
9 : use antares_store::Kind;
10 : use serde_json::{Map, Value};
11 : use std::collections::HashMap;
12 :
13 : /// 5.9.2.4: a registration whose expiresAt has been reached counts as
14 : /// deleted — lazily filtered on every read/match path (dt_key so fraction
15 : /// spellings cannot misorder, 4.11).
16 10432124 : pub fn reg_expired(doc: &Value) -> bool {
17 10432124 : doc.get("expiresAt")
18 10432124 : .and_then(Value::as_str)
19 10432124 : .is_some_and(|e| antares_model::dt_key(e) < antares_model::dt_key(&crate::state::now_iso()))
20 10432124 : }
21 :
22 : /// Output shaping: compact IRIs.
23 651 : pub fn present_registration(doc: &Value, ctx: &Context, sys_attrs: bool) -> Value {
24 651 : let Some(obj) = doc.as_object() else {
25 0 : return doc.clone();
26 : };
27 651 : let mut out = Map::new();
28 4228 : for (k, v) in obj {
29 4228 : match k.as_str() {
30 : // 5.2.9 has no member under this prefix: the creating subject
31 : // (policy::SUBJECT_MEMBER) stays in the broker, in a response
32 : // and in a 5.11 notification alike
33 4228 : k if k.starts_with("__") => continue,
34 4198 : "createdAt" | "modifiedAt" if !sys_attrs => continue,
35 3076 : "information" => {
36 651 : let infos: Vec<Value> = v
37 651 : .as_array()
38 651 : .cloned()
39 651 : .unwrap_or_default()
40 651 : .iter()
41 711 : .map(|info| {
42 711 : let mut ni = info.as_object().cloned().unwrap_or_default();
43 711 : if let Some(es) = ni.get("entities").and_then(Value::as_array) {
44 707 : let nes: Vec<Value> = es
45 707 : .iter()
46 767 : .map(|e| {
47 767 : let mut ne = e.as_object().cloned().unwrap_or_default();
48 767 : match ne.get("type") {
49 759 : Some(Value::String(t)) => {
50 759 : let c = ctx.compact_iri(t);
51 759 : ne.insert("type".into(), Value::String(c));
52 759 : }
53 8 : Some(Value::Array(ts)) => {
54 8 : let cs: Vec<Value> = ts
55 8 : .iter()
56 8 : .filter_map(Value::as_str)
57 16 : .map(|t| Value::String(ctx.compact_iri(t)))
58 8 : .collect();
59 8 : ne.insert("type".into(), Value::Array(cs));
60 : }
61 0 : _ => {}
62 : }
63 767 : Value::Object(ne)
64 767 : })
65 707 : .collect();
66 707 : ni.insert("entities".into(), Value::Array(nes));
67 4 : }
68 2133 : for names_key in antares_model::REGISTRATION_ATTRIBUTE_MEMBERS {
69 2133 : if let Some(names) = ni.get(names_key).and_then(Value::as_array) {
70 480 : let nn: Vec<Value> = names
71 480 : .iter()
72 480 : .filter_map(Value::as_str)
73 716 : .map(|n| Value::String(ctx.compact_iri(n)))
74 480 : .collect();
75 480 : ni.insert(names_key.into(), Value::Array(nn));
76 1653 : }
77 : }
78 711 : Value::Object(ni)
79 711 : })
80 651 : .collect();
81 651 : out.insert("information".into(), Value::Array(infos));
82 : }
83 2425 : _ => {
84 2425 : out.insert(k.clone(), v.clone());
85 2425 : }
86 : }
87 : }
88 651 : Value::Object(out)
89 651 : }
90 :
91 : /// 5.10.2.4 temporal matching against observationInterval/managementInterval.
92 32 : pub(crate) fn temporal_interval_matches(doc: &Value, tq: &crate::temporalq::TemporalQ) -> bool {
93 32 : let key = if tq.timeproperty == "observedAt" {
94 16 : "observationInterval"
95 : } else {
96 16 : "managementInterval"
97 : };
98 32 : let Some(iv) = doc.get(key).and_then(Value::as_object) else {
99 0 : return false; // relevant interval not present ⇒ no match
100 : };
101 : // 4.11 comparison on the canonical key — equal instants in different
102 : // 4.6.3 fraction spellings must hit the bounds exactly.
103 32 : let dt = antares_model::dt_key;
104 32 : let start = dt(iv.get("startAt").and_then(Value::as_str).unwrap_or(""));
105 32 : let end = iv.get("endAt").and_then(Value::as_str).map(dt); // open-ended when absent
106 32 : match tq.timerel.as_str() {
107 : // interval contains times before/after timeAt (037_09, 047_10/11)
108 32 : "before" => start < dt(&tq.time_at),
109 16 : "after" => end.is_none_or(|e| e > dt(&tq.time_at)),
110 0 : "between" => {
111 : // overlap between [timeAt, endTimeAt] and the interval
112 0 : let qe = dt(tq.end_time_at.as_deref().unwrap_or(&tq.time_at));
113 0 : dt(&tq.time_at) <= end.unwrap_or_else(|| "9999".into()) && qe >= start
114 : }
115 0 : _ => true,
116 : }
117 32 : }
118 :
119 : /// The entity/attribute specification matched against registrations (5.12).
120 : #[derive(Default)]
121 : pub struct CsrSpec {
122 : /// Expanded type IRIs (or raw 4.17 selector expressions).
123 : pub types: Option<Vec<String>>,
124 : pub ids: Option<Vec<String>>,
125 : pub id_pattern: Option<String>,
126 : /// Expanded attribute IRIs.
127 : pub attrs: Option<Vec<String>>,
128 : /// 5.12 datasetId condition (should-level): with both the request and
129 : /// the CSourceRegistration specifying datasetId, they match only with
130 : /// "at least one value in common"; one side alone always matches.
131 : pub dataset_ids: Option<Vec<String>>,
132 : /// 4.9 Context Source Filter: with a csf present, only registrations
133 : /// whose Context Source Properties match it are considered (query,
134 : /// temporal query, purge, entityMaps — 5.7.2.4/5.7.4.4/5.6.21.4).
135 : pub csf: Option<antares_ql::QNode>,
136 : /// 5.2.9 location ("Location for which the Context Source may be able
137 : /// to provide information") + 4.3.6.1: a geo query is only distributed
138 : /// to registrations whose location geometry matches it.
139 : pub geo: Option<antares_ql::geo::GeoQuery>,
140 : /// 5.2.9 observationInterval/managementInterval: "matched against the
141 : /// observationInterval for overlap" — a temporal read is only
142 : /// distributed to registrations whose declared interval overlaps the
143 : /// temporal query; a registration declaring NO interval stays
144 : /// unconstrained (both members are optional).
145 : pub temporal: Option<crate::temporalq::TemporalQ>,
146 : }
147 :
148 : /// 5.12: does an EntityInfo element match the entity specification?
149 34974 : fn entity_info_matches(spec: &CsrSpec, ei: &Value, ctx: &Context) -> bool {
150 34974 : if let Some(types) = &spec.types {
151 33942 : let its = ei_types(ei);
152 : // EntityInfo without a type restricts only by id/idPattern. Each spec
153 : // entry is a 4.17 Entity Type Selection over the WHOLE declared type
154 : // list (a conjunction needs every named type present); a plain
155 : // expanded IRI is the one-term case of the same evaluation.
156 33942 : if !its.is_empty()
157 33958 : && !types.iter().any(|t| {
158 33958 : its.contains(&t.as_str()) || antares_ql::type_selection_matches(t, &its, ctx)
159 33958 : })
160 : {
161 304 : return false;
162 33638 : }
163 1032 : }
164 34670 : let ei_id = ei.get("id").and_then(Value::as_str);
165 34670 : let ei_pat = ei.get("idPattern").and_then(Value::as_str);
166 34670 : if ei_id.is_none() && ei_pat.is_none() {
167 1750 : return true;
168 32920 : }
169 32920 : if let Some(ids) = &spec.ids {
170 32838 : if let Some(rid) = ei_id {
171 1804 : if ids.iter().any(|i| i == rid) {
172 1782 : return true;
173 18 : }
174 31038 : }
175 31056 : if let Some(p) = ei_pat {
176 31038 : if let Ok(re) = antares_ql::regex::compile(p) {
177 31048 : if ids.iter().any(|i| re.find(i).is_some()) {
178 298 : return true;
179 30740 : }
180 0 : }
181 18 : }
182 82 : }
183 30840 : if let Some(qp) = &spec.id_pattern {
184 24 : if let Some(rid) = ei_id {
185 12 : if antares_ql::regex::compile(qp).is_ok_and(|re| re.find(rid).is_some()) {
186 6 : return true;
187 6 : }
188 12 : }
189 18 : if ei_pat.is_some() {
190 12 : return true; // both patterns present ⇒ assumed compatible (5.12)
191 6 : }
192 30816 : }
193 : // no id restriction given by the query side ⇒ EntityInfo id restrictions
194 : // don't exclude it when the type matched
195 30822 : spec.ids.is_none() && spec.id_pattern.is_none()
196 34974 : }
197 :
198 3936 : fn attrs_match_info(attrs: &Option<Vec<String>>, info: &Value) -> bool {
199 3936 : let Some(attrs) = attrs else { return true };
200 1658 : if attrs.is_empty() {
201 0 : return true;
202 1658 : }
203 : // TS 104 175 9.4.2: attributeNames, else the deprecated pair
204 1658 : match antares_model::registration_attribute_names(info) {
205 478 : None => true,
206 1254 : Some(names) => names.iter().any(|n| attrs.iter().any(|w| w == n)),
207 : }
208 3936 : }
209 :
210 : /// 5.12: the RegistrationInfo elements of `doc.information` that match `spec`.
211 34902 : pub fn matching_infos<'a>(spec: &CsrSpec, doc: &'a Value, ctx: &Context) -> Vec<&'a Value> {
212 34902 : let Some(infos) = doc.get("information").and_then(Value::as_array) else {
213 0 : return Vec::new();
214 : };
215 34902 : infos
216 34902 : .iter()
217 34956 : .filter(|info| {
218 34956 : let entity_ok = match info.get("entities").and_then(Value::as_array) {
219 22 : None => true,
220 34974 : Some(es) => es.iter().any(|ei| entity_info_matches(spec, ei, ctx)),
221 : };
222 34956 : entity_ok && attrs_match_info(&spec.attrs, info)
223 34956 : })
224 34902 : .collect()
225 34902 : }
226 :
227 : /// 5.10.2.4: the context source filter (csf, 4.9) evaluates over the
228 : /// registration document's own Context Source Properties — its members are
229 : /// wrapped as Property instances so the shared 4.9 evaluator applies.
230 110 : pub(crate) fn csf_matches(csf: &antares_ql::QNode, reg: &Value, ctx: &Context) -> bool {
231 110 : let Some(obj) = reg.as_object() else {
232 0 : return false;
233 : };
234 110 : let mut pseudo = Map::new();
235 110 : pseudo.insert("id".into(), obj.get("id").cloned().unwrap_or(Value::Null));
236 110 : pseudo.insert(
237 110 : "type".into(),
238 110 : serde_json::json!(["ContextSourceRegistration"]),
239 : );
240 910 : for (k, v) in obj {
241 : // the `__` members are the broker's (policy::SUBJECT_MEMBER), not
242 : // Context Source Properties a client may filter on
243 910 : if ["id", "type", "information", "createdAt", "modifiedAt"].contains(&k.as_str())
244 448 : || k.starts_with("__")
245 : {
246 538 : continue;
247 372 : }
248 : // a Context Source Property stored in attribute form (5.2.9) is an
249 : // instance already — only bare scalars need the Property wrap
250 372 : let inst = match v {
251 56 : Value::Array(_) => v.clone(),
252 50 : Value::Object(o) if o.contains_key("value") || o.contains_key("object") => {
253 42 : serde_json::json!([v])
254 : }
255 274 : _ => serde_json::json!([{"type": "Property", "value": v}]),
256 : };
257 372 : pseudo.insert(ctx.expand_key(k), inst);
258 : }
259 110 : antares_ql::eval::eval_q(csf, &Value::Object(pseudo), ctx, &|_| None)
260 110 : }
261 :
262 2017 : pub fn csr_matches(spec: &CsrSpec, doc: &Value, ctx: &Context) -> bool {
263 2017 : !matching_infos(spec, doc, ctx).is_empty()
264 2017 : }
265 :
266 : /// Full 5.11.2.4 match of a registration against a csource subscription:
267 : /// 5.12 entity/attr matching + temporal interval rules + geoQ vs the
268 : /// registration's own `location`.
269 : ///
270 : /// Not the same rule as `antares_matcher::selector_match`, which it shares a
271 : /// signature with: that one asks whether an ENTITY satisfies a
272 : /// subscription's `entities` selector, this one asks whether a REGISTRATION
273 : /// does — and a registration carries its selector inside `information`, has
274 : /// an observation/management interval a latest-information subscription must
275 : /// not match, and answers `geoQ` from its own `location` rather than from a
276 : /// GeoProperty of the data. The part that IS the same rule is the selector
277 : /// walk, and it is not written twice: `spec_for_subscription` turns the
278 : /// subscription into a `CsrSpec` and `csr_matches` walks it.
279 478 : pub fn csr_matches_subscription(sub: &Value, reg: &Value, ctx: &Context) -> bool {
280 : // An expired registration is no longer a Context Source: it must not be
281 : // reported as newlyMatching, nor receive a forwarded subscription copy.
282 478 : if reg_expired(reg) {
283 1 : return false;
284 477 : }
285 477 : let spec = spec_for_subscription(sub);
286 477 : if !csr_matches(&spec, reg, ctx) {
287 80 : return false;
288 397 : }
289 397 : let has_interval =
290 397 : reg.get("observationInterval").is_some() || reg.get("managementInterval").is_some();
291 397 : match sub.get("temporalQ").and_then(Value::as_object) {
292 : None => {
293 381 : if has_interval {
294 0 : return false; // latest-information sources only (5.11.2.4)
295 381 : }
296 : }
297 16 : Some(tq) => {
298 16 : let mut params: HashMap<String, String> = HashMap::new();
299 64 : for k in ["timerel", "timeAt", "endTimeAt", "timeproperty"] {
300 64 : if let Some(s) = tq.get(k).and_then(Value::as_str) {
301 48 : params.insert(k.into(), s.into());
302 48 : }
303 : }
304 16 : if let Ok(Some(t)) = crate::temporalq::TemporalQ::from_params(¶ms, false) {
305 16 : if t.timerel != "any" && !temporal_interval_matches(reg, &t) {
306 0 : return false;
307 16 : }
308 0 : }
309 : }
310 : }
311 397 : if let Some(g) = sub.get("geoQ").and_then(Value::as_object) {
312 38 : if let Ok(Some(gq)) =
313 38 : antares_ql::geo::GeoQuery::from_params(&antares_matcher::geo_params(g))
314 : {
315 : // Table 5.2.6.5.2-1: matched against the registration GeoJSON
316 : // member `geoproperty` names (location, observationSpace,
317 : // operationSpace), stored by its compacted name
318 38 : let member = if gq.geoproperty.is_empty() {
319 14 : "location".to_owned()
320 : } else {
321 24 : ctx.compact_iri(&ctx.expand_key(&gq.geoproperty))
322 : };
323 38 : match reg.get(&member) {
324 32 : Some(geom) => {
325 32 : if !gq.matches_geometry(geom) {
326 8 : return false;
327 24 : }
328 : }
329 6 : None => return false,
330 : }
331 0 : }
332 359 : }
333 : // 5.11.2.4: csf vs the registration's Context Source Properties, scopeQ
334 : // vs its scope property
335 383 : if let Some(csf) = sub.get("csf").and_then(Value::as_str) {
336 28 : match antares_ql::parse_q(csf) {
337 28 : Ok(ast) if csf_matches(&ast, reg, ctx) => {}
338 12 : _ => return false,
339 : }
340 355 : }
341 371 : if let Some(sq) = sub.get("scopeQ").and_then(Value::as_str) {
342 8 : if !antares_ql::scope::scope_matches(sq, reg) {
343 4 : return false;
344 4 : }
345 363 : }
346 367 : true
347 478 : }
348 :
349 : /// Build the 5.12 spec for a csource subscription (5.11.2.4): entities
350 : /// selectors + watchedAttributes ∪ notification.attributes.
351 1813 : pub fn spec_for_subscription(sub: &Value) -> CsrSpec {
352 1813 : let mut spec = CsrSpec::default();
353 1813 : if let Some(es) = sub.get("entities").and_then(Value::as_array) {
354 1813 : let mut types = Vec::new();
355 1813 : let mut ids = Vec::new();
356 1813 : let mut patterns = Vec::new();
357 : // a selector naming no id restricts no id: then neither list does
358 1813 : let mut unrestricted = false;
359 1837 : for e in es {
360 1837 : if let Some(t) = e.get("type").and_then(Value::as_str) {
361 1837 : types.push(t.to_owned());
362 1837 : }
363 1837 : let before = ids.len();
364 1837 : match e.get("id") {
365 80 : Some(Value::String(i)) => ids.push(i.clone()),
366 4 : Some(Value::Array(a)) => {
367 4 : ids.extend(a.iter().filter_map(Value::as_str).map(str::to_owned));
368 4 : }
369 1753 : _ => {}
370 : }
371 1837 : let pattern = e.get("idPattern").and_then(Value::as_str);
372 1837 : if let Some(p) = pattern {
373 16 : patterns.push(format!("(?:{p})"));
374 1821 : }
375 1837 : unrestricted |= ids.len() == before && pattern.is_none();
376 : }
377 1813 : if !types.is_empty() {
378 1813 : spec.types = Some(types);
379 1813 : }
380 : // every selector's ids and idPatterns, as one id list
381 : // and one alternation
382 1813 : if !unrestricted {
383 88 : spec.ids = Some(ids).filter(|i| !i.is_empty());
384 88 : spec.id_pattern = Some(patterns.join("|")).filter(|p| !p.is_empty());
385 1725 : }
386 0 : }
387 1813 : let mut attrs: Vec<String> = sub
388 1813 : .get("watchedAttributes")
389 1813 : .and_then(Value::as_array)
390 1813 : .map(|a| {
391 152 : a.iter()
392 152 : .filter_map(Value::as_str)
393 152 : .map(str::to_owned)
394 152 : .collect()
395 152 : })
396 1813 : .unwrap_or_default();
397 1813 : if let Some(na) = sub
398 1813 : .get("notification")
399 1813 : .and_then(|n| n.get("attributes"))
400 1813 : .and_then(Value::as_array)
401 48 : {
402 48 : attrs.extend(na.iter().filter_map(Value::as_str).map(str::to_owned));
403 1765 : }
404 1813 : if !attrs.is_empty() {
405 200 : spec.attrs = Some(attrs);
406 1613 : }
407 1813 : spec
408 1813 : }
409 :
410 : /// The kind one Registration Subscription id is stored under.
411 1613 : pub(crate) fn csr_kind(id: &str) -> Kind {
412 1613 : if id.starts_with(INTERNAL_CSR_PREFIX) {
413 1044 : Kind::DistSub
414 : } else {
415 569 : Kind::CSourceSubscription
416 : }
417 1613 : }
418 :
419 : /// 5.8.1.4: "The mapping of the received subscriptionId with the own
420 : /// Subscription identifier is stored" (inbound), "a mapping of the id of
421 : /// the Context Source Registration to the received subscriptionId is
422 : /// stored" (remotes), and "the mapping of the id of the Subscription to the
423 : /// … Context Source Registration Subscription shall be stored" (csr_sub).
424 : /// All three live in the store (Kind::DistSub) so persistent modes keep the
425 : /// consumer half across restarts: one doc per (tenant, own Subscription id)
426 : /// = {"csr_sub": id, "remotes": {reg_id: [endpoint, remote sub id]}}, plus
427 : /// inbound index docs under the internal "distsub-index" tenant
428 : /// (id = remote subscriptionId, doc = {"tenant", "own"}).
429 : /// 5.8.1.4: the Registration Subscription the distributed half owns is
430 : /// broker plumbing, not a resource a Context Source Subscriber created —
431 : /// 5.11.5.4 lists the subscriptions clients made through 5.11.2, and this
432 : /// one carries the internal `urn:antares:distsub:` endpoint naming the
433 : /// tenant and the owning Subscription. It is stored under `Kind::DistSub`,
434 : /// so the 5.11 endpoints cannot read, patch or delete it, and its id
435 : /// namespace is what tells the two apart on the notification path.
436 : pub(crate) const INTERNAL_CSR_PREFIX: &str = "urn:ngsi-ld:CSourceSubscription:distsub:";
437 :
438 : /// 5.2.8: EntityInfo type is a String or String[] — yield every named type.
439 51290 : pub(crate) fn ei_types(ei: &Value) -> Vec<&str> {
440 51290 : match ei.get("type") {
441 51084 : Some(Value::String(s)) => vec![s.as_str()],
442 20 : Some(Value::Array(a)) => a.iter().filter_map(Value::as_str).collect(),
443 186 : _ => Vec::new(),
444 : }
445 51290 : }
446 :
447 : #[cfg(test)]
448 : mod tests {
449 : use super::*;
450 : use antares_jsonld::Loader;
451 : use serde_json::json;
452 :
453 : /// a CSR subscription's id arrays and every selector's
454 : /// idPattern restrict which registrations it watches; a selector with
455 : /// neither restricts none.
456 : #[test]
457 4 : fn a_subscription_spec_reads_every_selectors_ids_and_patterns() {
458 4 : let spec = spec_for_subscription(&json!({"entities": [
459 4 : {"type": "T", "id": ["urn:a", "urn:b"]},
460 4 : {"type": "T", "idPattern": "^urn:c"},
461 4 : {"type": "T", "idPattern": "^urn:d"}
462 4 : ]}));
463 4 : assert_eq!(spec.ids, Some(vec!["urn:a".to_owned(), "urn:b".to_owned()]));
464 4 : assert_eq!(spec.id_pattern.as_deref(), Some("(?:^urn:c)|(?:^urn:d)"));
465 4 : let open = spec_for_subscription(&json!({"entities": [
466 4 : {"type": "T", "id": "urn:a"}, {"type": "U"}
467 4 : ]}));
468 4 : assert_eq!((open.ids, open.id_pattern), (None, None));
469 4 : }
470 :
471 : /// TS 104 175 Table 5.2.6.5.2-1: a CSR subscription's geoQ "is matched
472 : /// against the specified Context Source Registration GeoJSON attributes,
473 : /// e.g. location, observationSpace or operationSpace": `geoproperty`
474 : /// names which one, `location` when it names none (12.4.2).
475 : /// A csf reads the Registration's Context Source Properties, never the
476 : /// broker's `__` members: a client whose `@context` maps a term onto
477 : /// the member cannot learn the subject another Registration was created
478 : /// by, whatever form a broker member is stored in. The 4.9 grammar refuses `__subject` and a full IRI, so
479 : /// a term is the only way to name it.
480 : #[tokio::test]
481 4 : async fn csf_never_reads_the_recorded_subject() {
482 4 : let core = Loader::new().core();
483 4 : let (iri, name) = (
484 4 : core.expand_key("__subject"),
485 4 : core.expand_key("registrationName"),
486 4 : );
487 4 : let other = core.expand_key("__other");
488 4 : let ctx = Loader::new()
489 4 : .resolve(&json!({"s": iri, "o": other, "n": name}))
490 4 : .await
491 4 : .unwrap_or_else(|e| panic!("{e}"));
492 4 : let reg = json!({
493 4 : "id": "urn:ngsi-ld:ContextSourceRegistration:s",
494 4 : "type": "ContextSourceRegistration",
495 4 : "information": [{"entities": [{"type": "Car"}]}],
496 4 : "endpoint": "http://a.example.com",
497 4 : "registrationName": "r",
498 4 : "__subject": [["X-Subject", "alice"]],
499 : // any broker member, whatever its form: one in Property form
500 : // would match if the prefix were read
501 4 : "__other": {"value": "alice"}
502 : });
503 20 : for q in ["s", "s[1]==\"alice\"", "s==\"alice\"", "o", "o==\"alice\""] {
504 20 : let csf = antares_ql::parse_q(q).unwrap_or_else(|e| panic!("{q}: {e}"));
505 20 : assert!(!csf_matches(&csf, ®, &ctx), "{q}");
506 : }
507 4 : let named = antares_ql::parse_q("n").unwrap_or_else(|e| panic!("{e}"));
508 4 : assert!(csf_matches(&named, ®, &ctx), "a property still matches");
509 4 : }
510 :
511 : #[test]
512 4 : fn clause_12_4_2_geoq_geoproperty_matches_observation_space() {
513 4 : let ctx = Loader::new().core();
514 8 : let point = |x: f64| json!({"type": "Point", "coordinates": [x, 0.5]});
515 4 : let reg = json!({
516 4 : "id": "urn:ngsi-ld:ContextSourceRegistration:geo",
517 4 : "type": "ContextSourceRegistration",
518 4 : "information": [{"entities": [{"type": "Building"}]}],
519 4 : "endpoint": "http://a.example.com",
520 4 : "location": point(5.0),
521 4 : "observationSpace": point(0.5)
522 : });
523 20 : let sub = |geoproperty: Option<&str>| {
524 20 : let mut geo = json!({"georel": "within", "geometry": "Polygon",
525 20 : "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]]});
526 20 : if let Some(p) = geoproperty {
527 16 : geo["geoproperty"] = json!(p);
528 16 : }
529 20 : json!({"entities": [{"type": "Building"}], "geoQ": geo})
530 20 : };
531 4 : assert!(csr_matches_subscription(
532 4 : &sub(Some("observationSpace")),
533 4 : ®,
534 4 : &ctx
535 : ));
536 4 : assert!(
537 4 : csr_matches_subscription(
538 4 : &sub(Some("https://uri.etsi.org/ngsi-ld/observationSpace")),
539 4 : ®,
540 4 : &ctx
541 : ),
542 : "the expanded name names the same member"
543 : );
544 4 : assert!(
545 4 : !csr_matches_subscription(&sub(None), ®, &ctx),
546 : "location is outside"
547 : );
548 4 : assert!(!csr_matches_subscription(
549 4 : &sub(Some("location")),
550 4 : ®,
551 4 : &ctx
552 4 : ));
553 4 : assert!(
554 4 : !csr_matches_subscription(&sub(Some("operationSpace")), ®, &ctx),
555 : "a member the registration lacks matches nothing"
556 : );
557 4 : }
558 :
559 : /// 5.11.2.4: a csource subscription's csf matches the registration's
560 : /// own Context Source Properties, and its scopeQ matches the
561 : /// registration scope.
562 : #[test]
563 4 : fn csource_subscription_csf_and_scope_matching() {
564 4 : let ctx = Loader::new().core();
565 4 : let reg_a = json!({
566 4 : "id": "urn:ngsi-ld:ContextSourceRegistration:sub-a",
567 4 : "type": "ContextSourceRegistration",
568 4 : "information": [{"entities": [{"type": "Building"}]}],
569 4 : "endpoint": "http://a.example.com",
570 4 : "scope": "/Madrid/Centro"
571 : });
572 4 : let reg_b = json!({
573 4 : "id": "urn:ngsi-ld:ContextSourceRegistration:sub-b",
574 4 : "type": "ContextSourceRegistration",
575 4 : "information": [{"entities": [{"type": "Building"}]}],
576 4 : "endpoint": "http://b.example.com",
577 4 : "scope": "/Berlin"
578 : });
579 4 : let sub_csf = json!({
580 4 : "entities": [{"type": "Building"}],
581 4 : "csf": "endpoint==\"http://a.example.com\""
582 : });
583 4 : assert!(csr_matches_subscription(&sub_csf, ®_a, &ctx));
584 4 : assert!(
585 4 : !csr_matches_subscription(&sub_csf, ®_b, &ctx),
586 : "csf must exclude the other endpoint"
587 : );
588 4 : let sub_scope = json!({
589 4 : "entities": [{"type": "Building"}],
590 4 : "scopeQ": "/Madrid/#"
591 : });
592 4 : assert!(csr_matches_subscription(&sub_scope, ®_a, &ctx));
593 4 : assert!(
594 4 : !csr_matches_subscription(&sub_scope, ®_b, &ctx),
595 : "scopeQ must exclude /Berlin"
596 : );
597 4 : }
598 : }
|