use serde::{Deserialize, Serialize};
pub mod feature {
pub const KV_CAS: u64 = 1 << 0;
pub const READ_YOUR_WRITES: u64 = 1 << 1;
pub const STRONG_CONSISTENCY: u64 = 1 << 2;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OpVersions {
pub query: u32,
pub control: u32,
pub kv: u32,
pub fork: u32,
#[serde(default, skip_serializing_if = "is_zero")]
pub agent: u32,
#[serde(default, skip_serializing_if = "is_zero")]
pub graph: u32,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub features: u64,
}
fn is_zero(value: &u32) -> bool {
*value == 0
}
fn is_zero_u64(value: &u64) -> bool {
*value == 0
}
impl OpVersions {
pub fn new(query: u32, control: u32, kv: u32, fork: u32) -> Self {
Self {
query,
control,
kv,
fork,
agent: 0,
graph: 0,
features: 0,
}
}
#[must_use]
pub fn with_agent(mut self, agent: u32) -> Self {
self.agent = agent;
self
}
#[must_use]
pub fn with_graph(mut self, graph: u32) -> Self {
self.graph = graph;
self
}
#[must_use]
pub fn with_features(mut self, features: u64) -> Self {
self.features = features;
self
}
pub const fn has_feature(&self, bit: u64) -> bool {
self.features & bit == bit
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HelloReply {
pub versions: OpVersions,
}
impl HelloReply {
pub fn new(versions: OpVersions) -> Self {
Self { versions }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BackendDescriptor {
pub id: String,
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<String>,
}
impl BackendDescriptor {
pub fn new(id: impl Into<String>, kind: impl Into<String>) -> Self {
Self {
id: id.into(),
kind: kind.into(),
label: None,
version: None,
capabilities: Vec::new(),
}
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
#[must_use]
pub fn with_capabilities<I, S>(mut self, capabilities: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.capabilities = capabilities.into_iter().map(Into::into).collect();
self
}
pub fn has_capability(&self, tag: &str) -> bool {
self.capabilities.iter().any(|c| c == tag)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BackendAnnounce {
pub versions: OpVersions,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub backends: Vec<BackendDescriptor>,
}
impl BackendAnnounce {
pub fn new(versions: OpVersions) -> Self {
Self {
versions,
backends: Vec::new(),
}
}
#[must_use]
pub fn with_backends(mut self, backends: Vec<BackendDescriptor>) -> Self {
self.backends = backends;
self
}
}
#[cfg(all(test, feature = "cbor"))]
mod tests {
use super::*;
use crate::codes::{CONTROL_OP_VERSION, FORK_OP_VERSION, KV_OP_VERSION, QUERY_OP_VERSION};
use crate::framing::{decode_named, encode_named};
#[test]
fn given_a_hello_reply_when_round_tripped_then_should_preserve_versions() {
let reply = HelloReply::new(OpVersions::new(
QUERY_OP_VERSION,
CONTROL_OP_VERSION,
KV_OP_VERSION,
FORK_OP_VERSION,
));
let bytes = encode_named(&reply).expect("hello reply serializes");
let back: HelloReply = decode_named(&bytes).expect("hello reply deserializes");
assert_eq!(back, reply);
}
#[test]
fn given_a_backend_announce_when_round_tripped_then_should_preserve_features() {
let announce = BackendAnnounce::new(
OpVersions::new(
QUERY_OP_VERSION,
CONTROL_OP_VERSION,
KV_OP_VERSION,
FORK_OP_VERSION,
)
.with_features(feature::KV_CAS | feature::READ_YOUR_WRITES),
);
let bytes = encode_named(&announce).expect("serializes");
let back: BackendAnnounce = decode_named(&bytes).expect("deserializes");
assert_eq!(back, announce);
assert!(back.versions.has_feature(feature::KV_CAS));
}
#[test]
fn given_an_empty_hello_body_when_decoded_then_should_yield_no_versions() {
assert!(decode_named::<HelloReply>(&[]).is_err());
}
#[test]
fn given_advertised_backends_when_round_tripped_then_should_preserve_them_and_skip_empty() {
let announce = BackendAnnounce::new(OpVersions::new(
QUERY_OP_VERSION,
CONTROL_OP_VERSION,
KV_OP_VERSION,
FORK_OP_VERSION,
))
.with_backends(vec![
BackendDescriptor::new("embedded", "embedded"),
BackendDescriptor::new("warehouse", "columnar")
.with_label("Analytics warehouse")
.with_version("2.1.0")
.with_capabilities(["ingest", "query", "percentile"]),
]);
let bytes = encode_named(&announce).expect("encodes");
let back: BackendAnnounce = decode_named(&bytes).expect("decodes");
assert_eq!(back, announce);
assert_eq!(back.backends.len(), 2);
assert_eq!(back.backends[1].id, "warehouse");
assert_eq!(back.backends[1].kind, "columnar");
assert_eq!(
back.backends[1].label.as_deref(),
Some("Analytics warehouse")
);
assert_eq!(back.backends[1].version.as_deref(), Some("2.1.0"));
assert!(back.backends[1].has_capability("query"));
assert!(!back.backends[1].has_capability("vector_search"));
assert_eq!(back.backends[0].label, None);
assert_eq!(back.backends[0].version, None);
assert!(back.backends[0].capabilities.is_empty());
let minimal_json = serde_json::to_string(&back.backends[0]).expect("json");
assert!(
!minimal_json.contains("label")
&& !minimal_json.contains("version")
&& !minimal_json.contains("capabilities"),
"absent advisory fields omitted: {minimal_json}"
);
let plain = BackendAnnounce::new(OpVersions::new(1, 1, 1, 1));
let json = serde_json::to_string(&plain).expect("json");
assert!(!json.contains("backends"), "empty backends omitted: {json}");
}
#[test]
fn given_advertised_features_when_round_tripped_then_should_preserve_bits_and_skip_zero() {
let versions = OpVersions::new(
QUERY_OP_VERSION,
CONTROL_OP_VERSION,
KV_OP_VERSION,
FORK_OP_VERSION,
)
.with_features(feature::KV_CAS | feature::READ_YOUR_WRITES);
assert!(versions.has_feature(feature::KV_CAS));
assert!(versions.has_feature(feature::READ_YOUR_WRITES));
assert!(!versions.has_feature(feature::STRONG_CONSISTENCY));
assert!(versions.has_feature(feature::KV_CAS | feature::READ_YOUR_WRITES));
assert!(!versions.has_feature(feature::KV_CAS | feature::STRONG_CONSISTENCY));
let reply = HelloReply::new(versions);
let bytes = encode_named(&reply).expect("encodes");
let back: HelloReply = decode_named(&bytes).expect("decodes");
assert_eq!(back, reply);
assert!(back.versions.has_feature(feature::READ_YOUR_WRITES));
let plain = HelloReply::new(OpVersions::new(1, 1, 1, 1));
let json = serde_json::to_string(&plain).expect("json");
assert!(!json.contains("features"), "zero features omitted: {json}");
}
}