1use crate::topology::WireTopology;
2use serde::{Deserialize, Serialize};
3
4pub mod feature {
11 pub const KV_CAS: u64 = 1 << 0;
13 pub const READ_YOUR_WRITES: u64 = 1 << 1;
15 pub const STRONG_CONSISTENCY: u64 = 1 << 2;
17 pub const KV_CAS_FENCED: u64 = 1 << 3;
19 pub const AGENT_WORKFLOW: u64 = 1 << 4;
21 pub const KEYWORD_SEARCH: u64 = 1 << 5;
23 pub const WATCH: u64 = 1 << 6;
26 pub const AUTHZ: u64 = 1 << 7;
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[non_exhaustive]
35pub struct OpVersions {
36 pub query: u32,
37 pub control: u32,
38 pub kv: u32,
39 pub fork: u32,
40 #[serde(default, skip_serializing_if = "is_zero")]
44 pub agent: u32,
45 #[serde(default, skip_serializing_if = "is_zero")]
50 pub graph: u32,
51 #[serde(default, skip_serializing_if = "is_zero_u64")]
57 pub features: u64,
58}
59
60fn is_zero(value: &u32) -> bool {
61 *value == 0
62}
63
64fn is_zero_u64(value: &u64) -> bool {
65 *value == 0
66}
67
68impl OpVersions {
69 pub fn new(query: u32, control: u32, kv: u32, fork: u32) -> Self {
72 Self {
73 query,
74 control,
75 kv,
76 fork,
77 agent: 0,
78 graph: 0,
79 features: 0,
80 }
81 }
82
83 #[must_use]
85 pub fn with_agent(mut self, agent: u32) -> Self {
86 self.agent = agent;
87 self
88 }
89
90 #[must_use]
92 pub fn with_graph(mut self, graph: u32) -> Self {
93 self.graph = graph;
94 self
95 }
96
97 #[must_use]
100 pub fn with_features(mut self, features: u64) -> Self {
101 self.features = features;
102 self
103 }
104
105 pub const fn has_feature(&self, bit: u64) -> bool {
107 self.features & bit == bit
108 }
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
117#[non_exhaustive]
118pub struct HelloReply {
119 pub versions: OpVersions,
120}
121
122impl HelloReply {
123 pub fn new(versions: OpVersions) -> Self {
125 Self { versions }
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
135#[non_exhaustive]
136pub struct BackendDescriptor {
137 pub id: String,
138 pub kind: String,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub label: Option<String>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub version: Option<String>,
149 #[serde(default, skip_serializing_if = "Vec::is_empty")]
158 pub capabilities: Vec<String>,
159}
160
161impl BackendDescriptor {
162 pub fn new(id: impl Into<String>, kind: impl Into<String>) -> Self {
164 Self {
165 id: id.into(),
166 kind: kind.into(),
167 label: None,
168 version: None,
169 capabilities: Vec::new(),
170 }
171 }
172
173 #[must_use]
175 pub fn with_label(mut self, label: impl Into<String>) -> Self {
176 self.label = Some(label.into());
177 self
178 }
179
180 #[must_use]
182 pub fn with_version(mut self, version: impl Into<String>) -> Self {
183 self.version = Some(version.into());
184 self
185 }
186
187 #[must_use]
190 pub fn with_capabilities<I, S>(mut self, capabilities: I) -> Self
191 where
192 I: IntoIterator<Item = S>,
193 S: Into<String>,
194 {
195 self.capabilities = capabilities.into_iter().map(Into::into).collect();
196 self
197 }
198
199 pub fn has_capability(&self, tag: &str) -> bool {
201 self.capabilities.iter().any(|c| c == tag)
202 }
203}
204
205const fn backend_ready_by_default() -> bool {
215 true
216}
217
218const fn backend_is_ready(ready: &bool) -> bool {
219 *ready
220}
221
222#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
223#[non_exhaustive]
224pub struct BackendAnnounce {
225 pub versions: OpVersions,
226 #[serde(
227 default = "backend_ready_by_default",
228 skip_serializing_if = "backend_is_ready"
229 )]
230 pub ready: bool,
231 #[serde(default, skip_serializing_if = "Vec::is_empty")]
236 pub backends: Vec<BackendDescriptor>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub topology: Option<WireTopology>,
243}
244
245impl BackendAnnounce {
246 pub fn new(versions: OpVersions) -> Self {
248 Self {
249 versions,
250 ready: true,
251 backends: Vec::new(),
252 topology: None,
253 }
254 }
255
256 #[must_use]
257 pub const fn unavailable(mut self) -> Self {
258 self.ready = false;
259 self
260 }
261
262 #[must_use]
264 pub fn with_backends(mut self, backends: Vec<BackendDescriptor>) -> Self {
265 self.backends = backends;
266 self
267 }
268
269 #[must_use]
271 pub fn with_topology(mut self, topology: WireTopology) -> Self {
272 self.topology = Some(topology);
273 self
274 }
275}
276
277#[cfg(all(test, feature = "cbor"))]
278mod tests {
279 use super::*;
280 use crate::codes::{CONTROL_OP_VERSION, FORK_OP_VERSION, KV_OP_VERSION, QUERY_OP_VERSION};
281 use crate::framing::{decode_named, encode_named};
282
283 #[test]
284 fn given_a_hello_reply_when_round_tripped_then_should_preserve_versions() {
285 let reply = HelloReply::new(OpVersions::new(
288 QUERY_OP_VERSION,
289 CONTROL_OP_VERSION,
290 KV_OP_VERSION,
291 FORK_OP_VERSION,
292 ));
293 let bytes = encode_named(&reply).expect("hello reply serializes");
294 let back: HelloReply = decode_named(&bytes).expect("hello reply deserializes");
295 assert_eq!(back, reply);
296 }
297
298 #[test]
299 fn given_a_backend_announce_when_round_tripped_then_should_preserve_features() {
300 let announce = BackendAnnounce::new(
301 OpVersions::new(
302 QUERY_OP_VERSION,
303 CONTROL_OP_VERSION,
304 KV_OP_VERSION,
305 FORK_OP_VERSION,
306 )
307 .with_features(feature::KV_CAS | feature::READ_YOUR_WRITES),
308 );
309 let bytes = encode_named(&announce).expect("serializes");
310 let back: BackendAnnounce = decode_named(&bytes).expect("deserializes");
311 assert_eq!(back, announce);
312 assert!(back.versions.has_feature(feature::KV_CAS));
313 }
314
315 #[test]
316 fn given_an_unavailable_backend_announce_when_round_tripped_then_should_stay_unavailable() {
317 let announce = BackendAnnounce::new(OpVersions::new(
318 QUERY_OP_VERSION,
319 CONTROL_OP_VERSION,
320 KV_OP_VERSION,
321 FORK_OP_VERSION,
322 ))
323 .unavailable();
324 let bytes = encode_named(&announce).expect("serializes");
325 let back: BackendAnnounce = decode_named(&bytes).expect("deserializes");
326 assert!(!back.ready);
327 }
328
329 #[test]
330 fn given_an_empty_hello_body_when_decoded_then_should_yield_no_versions() {
331 assert!(decode_named::<HelloReply>(&[]).is_err());
334 }
335
336 #[test]
337 fn given_advertised_backends_when_round_tripped_then_should_preserve_them_and_skip_empty() {
338 let announce = BackendAnnounce::new(OpVersions::new(
339 QUERY_OP_VERSION,
340 CONTROL_OP_VERSION,
341 KV_OP_VERSION,
342 FORK_OP_VERSION,
343 ))
344 .with_backends(vec![
345 BackendDescriptor::new("embedded", "embedded"),
346 BackendDescriptor::new("warehouse", "columnar")
347 .with_label("Analytics warehouse")
348 .with_version("2.1.0")
349 .with_capabilities(["ingest", "query", "percentile"]),
350 ]);
351 let bytes = encode_named(&announce).expect("encodes");
352 let back: BackendAnnounce = decode_named(&bytes).expect("decodes");
353 assert_eq!(back, announce);
354 assert_eq!(back.backends.len(), 2);
355 assert_eq!(back.backends[1].id, "warehouse");
356 assert_eq!(back.backends[1].kind, "columnar");
357 assert_eq!(
358 back.backends[1].label.as_deref(),
359 Some("Analytics warehouse")
360 );
361 assert_eq!(back.backends[1].version.as_deref(), Some("2.1.0"));
362 assert!(back.backends[1].has_capability("query"));
363 assert!(!back.backends[1].has_capability("vector_search"));
364 assert_eq!(back.backends[0].label, None);
366 assert_eq!(back.backends[0].version, None);
367 assert!(back.backends[0].capabilities.is_empty());
368 let minimal_json = serde_json::to_string(&back.backends[0]).expect("json");
369 assert!(
370 !minimal_json.contains("label")
371 && !minimal_json.contains("version")
372 && !minimal_json.contains("capabilities"),
373 "absent advisory fields omitted: {minimal_json}"
374 );
375
376 let plain = BackendAnnounce::new(OpVersions::new(1, 1, 1, 1));
379 let json = serde_json::to_string(&plain).expect("json");
380 assert!(!json.contains("backends"), "empty backends omitted: {json}");
381 }
382
383 #[test]
384 fn given_announce_without_topology_when_decoded_then_should_default_none() {
385 let announce = BackendAnnounce::new(OpVersions::new(
386 QUERY_OP_VERSION,
387 CONTROL_OP_VERSION,
388 KV_OP_VERSION,
389 FORK_OP_VERSION,
390 ));
391 let bytes = encode_named(&announce).expect("encodes");
392 let back: BackendAnnounce = decode_named(&bytes).expect("decodes");
393 assert_eq!(back.topology, None);
394 let json = serde_json::to_string(&announce).expect("json");
395 assert!(
396 !json.contains("topology"),
397 "absent topology omitted: {json}"
398 );
399 }
400
401 #[test]
402 fn given_announce_with_topology_when_round_tripped_then_should_preserve_it() {
403 let custom = WireTopology {
404 ops_stream: "custom-ops".to_owned(),
405 ..WireTopology::default()
406 };
407 let announce = BackendAnnounce::new(OpVersions::new(
408 QUERY_OP_VERSION,
409 CONTROL_OP_VERSION,
410 KV_OP_VERSION,
411 FORK_OP_VERSION,
412 ))
413 .with_topology(custom.clone());
414 let bytes = encode_named(&announce).expect("encodes");
415 let back: BackendAnnounce = decode_named(&bytes).expect("decodes");
416 assert_eq!(back.topology, Some(custom));
417 }
418
419 #[test]
420 fn given_advertised_features_when_round_tripped_then_should_preserve_bits_and_skip_zero() {
421 let versions = OpVersions::new(
422 QUERY_OP_VERSION,
423 CONTROL_OP_VERSION,
424 KV_OP_VERSION,
425 FORK_OP_VERSION,
426 )
427 .with_features(feature::KV_CAS | feature::READ_YOUR_WRITES);
428 assert!(versions.has_feature(feature::KV_CAS));
429 assert!(versions.has_feature(feature::READ_YOUR_WRITES));
430 assert!(!versions.has_feature(feature::STRONG_CONSISTENCY));
431 assert!(versions.has_feature(feature::KV_CAS | feature::READ_YOUR_WRITES));
433 assert!(!versions.has_feature(feature::KV_CAS | feature::STRONG_CONSISTENCY));
434 let reply = HelloReply::new(versions);
435 let bytes = encode_named(&reply).expect("encodes");
436 let back: HelloReply = decode_named(&bytes).expect("decodes");
437 assert_eq!(back, reply);
438 assert!(back.versions.has_feature(feature::READ_YOUR_WRITES));
439 let plain = HelloReply::new(OpVersions::new(1, 1, 1, 1));
442 let json = serde_json::to_string(&plain).expect("json");
443 assert!(!json.contains("features"), "zero features omitted: {json}");
444 }
445}