1use std::collections::BTreeMap;
25
26use kernel::{AxCode, AxError, B3Hash, EventRecord, Seq, TimeMs};
27
28use crate::frames::Query;
29
30#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct CityLabel(String);
34
35impl CityLabel {
36 pub fn parse(raw: &str) -> Result<Self, AxError> {
42 if raw.is_empty() || raw.chars().any(char::is_control) {
43 return Err(AxError::failure(
44 AxCode::ConfigInvalid,
45 "label an upstream city",
46 "the label is empty or holds control characters",
47 )
48 .with_recovery("give the city a short single-line name"));
49 }
50 Ok(Self(raw.to_owned()))
51 }
52
53 #[must_use]
54 pub fn as_str(&self) -> &str {
55 &self.0
56 }
57}
58
59#[derive(Debug, Clone)]
62pub struct Upstream {
63 pub label: CityLabel,
64 pub address: String,
67 pub token_digest: Option<B3Hash>,
69}
70
71#[derive(Debug, Clone, PartialEq)]
75pub struct Sighting {
76 pub city: CityLabel,
77 pub event: EventRecord,
78}
79
80#[derive(Debug, Default)]
86pub struct Aggregate {
87 upstreams: BTreeMap<CityLabel, Upstream>,
88}
89
90impl Aggregate {
91 #[must_use]
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 pub fn attach(&mut self, upstream: Upstream) {
100 self.upstreams.insert(upstream.label.clone(), upstream);
101 }
102
103 pub fn detach(&mut self, label: &CityLabel) -> bool {
104 self.upstreams.remove(label).is_some()
105 }
106
107 pub fn cities(&self) -> impl Iterator<Item = &Upstream> {
111 self.upstreams.values()
112 }
113
114 pub fn ask(&self, label: &CityLabel, query: Query) -> Result<Forwarded, AxError> {
123 let upstream = self.upstreams.get(label).ok_or_else(|| {
124 AxError::failure(
125 AxCode::ConfigInvalid,
126 "forward a query to another city",
127 format!("no city is attached under the label `{}`", label.as_str()),
128 )
129 .with_recovery("attach the city with its address and pairing code first")
130 })?;
131 Ok(Forwarded {
132 address: upstream.address.clone(),
133 query,
134 })
135 }
136
137 #[must_use]
145 pub fn merge(streams: Vec<(CityLabel, Vec<EventRecord>)>) -> Vec<Sighting> {
146 let mut merged: Vec<Sighting> = streams
147 .into_iter()
148 .flat_map(|(city, events)| {
149 events.into_iter().map(move |event| Sighting {
150 city: city.clone(),
151 event,
152 })
153 })
154 .collect();
155 merged.sort_by_key(sort_key);
156 merged
157 }
158}
159
160fn sort_key(sighting: &Sighting) -> (TimeMs, CityLabel, Seq) {
161 (
162 sighting.event.t(),
163 sighting.city.clone(),
164 sighting.event.seq(),
165 )
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct Forwarded {
172 pub address: String,
173 pub query: Query,
174}
175
176#[cfg(test)]
177#[allow(
178 clippy::unwrap_used,
179 clippy::expect_used,
180 clippy::panic,
181 clippy::indexing_slicing,
182 clippy::arithmetic_side_effects,
183 reason = "test code"
184)]
185mod tests {
186 use super::*;
187
188 fn city(name: &str) -> CityLabel {
189 CityLabel::parse(name).unwrap()
190 }
191
192 fn upstream(name: &str) -> Upstream {
193 Upstream {
194 label: city(name),
195 address: format!("{name}.local:8787"),
196 token_digest: None,
197 }
198 }
199
200 #[test]
201 fn attaching_the_same_label_twice_replaces_rather_than_duplicates() {
202 let mut aggregate = Aggregate::new();
203 aggregate.attach(upstream("studio"));
204 let mut corrected = upstream("studio");
205 corrected.address = "192.168.1.9:8787".to_owned();
206 aggregate.attach(corrected);
207 assert_eq!(aggregate.cities().count(), 1);
208 let only = aggregate.cities().next().unwrap();
209 assert_eq!(only.address, "192.168.1.9:8787");
210 }
211
212 #[test]
213 fn asking_an_unattached_city_is_refused_not_guessed() {
214 let aggregate = Aggregate::new();
215 let err = aggregate
216 .ask(&city("nowhere"), Query::CityView)
217 .expect_err("an unknown label has no address to guess");
218 assert_eq!(*err.code(), AxCode::ConfigInvalid);
219 assert!(!err.recovery().is_empty());
220 }
221
222 fn event(at: u64, seq: u64) -> EventRecord {
223 EventRecord::from_draft(
224 kernel::EventDraft {
225 run: kernel::RunId::from_bytes([4u8; 16]),
226 t: TimeMs::new(at),
227 who: "test".to_owned(),
228 addr: None,
229 kind: kernel::EventKind::RunStarted,
230 data: kernel::Payload::empty(),
231 ig: false,
232 },
233 Seq::new(seq),
234 B3Hash::digest(b"prev"),
235 )
236 }
237
238 #[test]
239 fn merging_is_the_same_sequence_however_the_streams_arrive() {
240 let left = (city("attic"), vec![event(10, 1), event(30, 2)]);
245 let right = (city("studio"), vec![event(10, 7), event(20, 8)]);
246
247 let one = Aggregate::merge(vec![left.clone(), right.clone()]);
248 let other = Aggregate::merge(vec![right, left]);
249 assert_eq!(one, other, "merge order cannot depend on arrival order");
250
251 let trail: Vec<(&str, u64)> = one
252 .iter()
253 .map(|s| (s.city.as_str(), s.event.t().value()))
254 .collect();
255 assert_eq!(
256 trail,
257 [("attic", 10), ("studio", 10), ("studio", 20), ("attic", 30)]
258 );
259 }
260
261 #[test]
262 fn cities_iterate_in_label_order_regardless_of_attachment_order() {
263 let mut aggregate = Aggregate::new();
264 for name in ["studio", "attic", "shed"] {
265 aggregate.attach(upstream(name));
266 }
267 let order: Vec<&str> = aggregate.cities().map(|u| u.label.as_str()).collect();
268 assert_eq!(order, ["attic", "shed", "studio"]);
269 }
270}