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_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,
features: 0,
}
}
#[must_use]
pub fn with_agent(mut self, agent: u32) -> Self {
self.agent = agent;
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, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BackendAnnounce {
pub versions: OpVersions,
}
impl BackendAnnounce {
pub fn new(versions: OpVersions) -> Self {
Self { versions }
}
}
#[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_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}");
}
}