opsis_core/
subscription.rs1use serde::{Deserialize, Serialize};
2use ulid::Ulid;
3
4use crate::event::OpsisEvent;
5use crate::spatial::Bbox;
6use crate::state::StateDomain;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct ClientId(pub String);
11
12impl Default for ClientId {
13 fn default() -> Self {
14 Self(Ulid::new().to_string())
15 }
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Subscription {
24 pub domains: Vec<StateDomain>,
26 pub bbox: Option<Bbox>,
28 pub severity_threshold: f32,
30 pub keywords: Vec<String>,
33}
34
35impl Subscription {
36 pub fn all() -> Self {
38 Self {
39 domains: Vec::new(),
40 bbox: None,
41 severity_threshold: 0.0,
42 keywords: Vec::new(),
43 }
44 }
45
46 pub fn matches(&self, event: &OpsisEvent) -> bool {
48 if !self.domains.is_empty() {
50 match &event.domain {
51 Some(domain) if self.domains.contains(domain) => {}
52 _ => return false,
53 }
54 }
55
56 let severity = event.severity.unwrap_or(0.0);
58 if severity < self.severity_threshold {
59 return false;
60 }
61
62 if let Some(ref bbox) = self.bbox {
64 match event.location {
65 Some(ref loc) => {
66 if !bbox.contains(loc) {
67 return false;
68 }
69 }
70 None => return false,
71 }
72 }
73
74 if !self.keywords.is_empty() {
76 let has_keyword = self.keywords.iter().any(|kw| {
77 let kw_lower = kw.to_lowercase();
78 event
79 .tags
80 .iter()
81 .any(|t| t.to_lowercase().contains(&kw_lower))
82 });
83 if !has_keyword {
84 return false;
85 }
86 }
87
88 true
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::clock::WorldTick;
96 use crate::event::{EventId, EventSource, OpsisEventKind};
97 use crate::feed::{FeedSource, SchemaKey};
98 use crate::spatial::GeoPoint;
99 use chrono::Utc;
100
101 fn sample_event() -> OpsisEvent {
102 OpsisEvent {
103 id: EventId::default(),
104 tick: WorldTick(1),
105 timestamp: Utc::now(),
106 source: EventSource::Feed(FeedSource::new("test")),
107 kind: OpsisEventKind::WorldObservation {
108 summary: "Stock market surge".into(),
109 },
110 location: Some(GeoPoint::new(5.0, 5.0)),
111 domain: Some(StateDomain::Finance),
112 severity: Some(0.7),
113 schema_key: SchemaKey::new("test.v1"),
114 tags: vec!["finance".into(), "market".into()],
115 }
116 }
117
118 #[test]
119 fn all_matches_everything() {
120 let sub = Subscription::all();
121 assert!(sub.matches(&sample_event()));
122 }
123
124 #[test]
125 fn domain_filter() {
126 let sub = Subscription {
127 domains: vec![StateDomain::Weather],
128 ..Subscription::all()
129 };
130 assert!(!sub.matches(&sample_event()));
131
132 let sub = Subscription {
133 domains: vec![StateDomain::Finance],
134 ..Subscription::all()
135 };
136 assert!(sub.matches(&sample_event()));
137 }
138
139 #[test]
140 fn domain_filter_no_domain_event() {
141 let mut evt = sample_event();
142 evt.domain = None;
143 let sub = Subscription {
144 domains: vec![StateDomain::Finance],
145 ..Subscription::all()
146 };
147 assert!(!sub.matches(&evt));
148 }
149
150 #[test]
151 fn severity_filter() {
152 let sub = Subscription {
153 severity_threshold: 0.9,
154 ..Subscription::all()
155 };
156 assert!(!sub.matches(&sample_event()));
157
158 let sub = Subscription {
159 severity_threshold: 0.5,
160 ..Subscription::all()
161 };
162 assert!(sub.matches(&sample_event()));
163 }
164
165 #[test]
166 fn severity_filter_none_severity() {
167 let mut evt = sample_event();
168 evt.severity = None;
169 let sub = Subscription {
170 severity_threshold: 0.1,
171 ..Subscription::all()
172 };
173 assert!(!sub.matches(&evt));
174 }
175
176 #[test]
177 fn bbox_filter() {
178 let bbox = Bbox::new(GeoPoint::new(0.0, 0.0), GeoPoint::new(10.0, 10.0));
179 let sub = Subscription {
180 bbox: Some(bbox),
181 ..Subscription::all()
182 };
183 assert!(sub.matches(&sample_event()));
184
185 let bbox_far = Bbox::new(GeoPoint::new(20.0, 20.0), GeoPoint::new(30.0, 30.0));
186 let sub = Subscription {
187 bbox: Some(bbox_far),
188 ..Subscription::all()
189 };
190 assert!(!sub.matches(&sample_event()));
191 }
192
193 #[test]
194 fn keyword_filter() {
195 let sub = Subscription {
196 keywords: vec!["finance".into()],
197 ..Subscription::all()
198 };
199 assert!(sub.matches(&sample_event()));
200
201 let sub = Subscription {
202 keywords: vec!["earthquake".into()],
203 ..Subscription::all()
204 };
205 assert!(!sub.matches(&sample_event()));
206
207 let sub = Subscription {
209 keywords: vec!["market".into()],
210 ..Subscription::all()
211 };
212 assert!(sub.matches(&sample_event()));
213 }
214}