use std::cmp::Ordering;
use std::fmt;
use crate::{AdministrationSequence, CommitSequence};
pub const DUAL_FRONTIER_BYTES: usize = 18;
const COMPONENT_ABSENT: u8 = 0x00;
const COMPONENT_PRESENT: u8 = 0x01;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct DualFrontier {
application: Option<CommitSequence>,
administration: Option<AdministrationSequence>,
}
impl DualFrontier {
pub const INITIAL: Self = Self {
application: None,
administration: None,
};
#[must_use]
pub const fn new(
application: Option<CommitSequence>,
administration: Option<AdministrationSequence>,
) -> Self {
Self {
application,
administration,
}
}
#[must_use]
pub const fn application(self) -> Option<CommitSequence> {
self.application
}
#[must_use]
pub const fn administration(self) -> Option<AdministrationSequence> {
self.administration
}
#[must_use]
pub const fn is_initial(self) -> bool {
self.application.is_none() && self.administration.is_none()
}
#[must_use]
pub fn advances_from(self, predecessor: Self) -> bool {
self.partial_cmp(&predecessor) == Some(Ordering::Greater)
}
#[must_use]
pub const fn to_canonical_bytes(self) -> [u8; DUAL_FRONTIER_BYTES] {
let mut encoded = [0_u8; DUAL_FRONTIER_BYTES];
if let Some(application) = self.application {
encoded[0] = COMPONENT_PRESENT;
let value = application.to_be_bytes();
let mut index = 0;
while index < 8 {
encoded[1 + index] = value[index];
index += 1;
}
} else {
encoded[0] = COMPONENT_ABSENT;
}
if let Some(administration) = self.administration {
encoded[9] = COMPONENT_PRESENT;
let value = administration.to_be_bytes();
let mut index = 0;
while index < 8 {
encoded[10 + index] = value[index];
index += 1;
}
} else {
encoded[9] = COMPONENT_ABSENT;
}
encoded
}
pub fn from_canonical_bytes(
bytes: [u8; DUAL_FRONTIER_BYTES],
) -> Result<Self, DualFrontierError> {
let application = decode_component(bytes[0], &bytes[1..9])?
.map(|value| CommitSequence::new(value).ok_or(DualFrontierError::ZeroSequence))
.transpose()?;
let administration = decode_component(bytes[9], &bytes[10..18])?
.map(|value| AdministrationSequence::new(value).ok_or(DualFrontierError::ZeroSequence))
.transpose()?;
Ok(Self::new(application, administration))
}
}
fn decode_component(presence: u8, value: &[u8]) -> Result<Option<u64>, DualFrontierError> {
let raw = u64::from_be_bytes(
value
.try_into()
.map_err(|_| DualFrontierError::InvalidShape)?,
);
match presence {
COMPONENT_ABSENT if raw == 0 => Ok(None),
COMPONENT_ABSENT => Err(DualFrontierError::NonCanonicalAbsence),
COMPONENT_PRESENT => Ok(Some(raw)),
_ => Err(DualFrontierError::InvalidShape),
}
}
impl PartialOrd for DualFrontier {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let application = component_cmp(
self.application.map(CommitSequence::get),
other.application.map(CommitSequence::get),
);
let administration = component_cmp(
self.administration.map(AdministrationSequence::get),
other.administration.map(AdministrationSequence::get),
);
match (application, administration) {
(Ordering::Equal, value) | (value, Ordering::Equal) => Some(value),
(Ordering::Less, Ordering::Less) => Some(Ordering::Less),
(Ordering::Greater, Ordering::Greater) => Some(Ordering::Greater),
_ => None,
}
}
}
fn component_cmp(left: Option<u64>, right: Option<u64>) -> Ordering {
left.unwrap_or(0).cmp(&right.unwrap_or(0))
}
impl fmt::Display for DualFrontier {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.application, self.administration) {
(Some(application), Some(administration)) => {
write!(formatter, "(app {application}, admin {administration})")
}
(Some(application), None) => write!(formatter, "(app {application}, admin none)"),
(None, Some(administration)) => write!(formatter, "(app none, admin {administration})"),
(None, None) => formatter.write_str("(app none, admin none)"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DualFrontierError {
InvalidShape,
NonCanonicalAbsence,
ZeroSequence,
}
impl fmt::Display for DualFrontierError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::InvalidShape => "dual frontier bytes have an invalid shape",
Self::NonCanonicalAbsence => {
"an absent dual frontier component is not canonically zero"
}
Self::ZeroSequence => "a present dual frontier component must be nonzero",
})
}
}
impl std::error::Error for DualFrontierError {}
#[cfg(test)]
mod tests {
use super::*;
fn application(value: u64) -> Option<CommitSequence> {
CommitSequence::new(value)
}
fn administration(value: u64) -> Option<AdministrationSequence> {
AdministrationSequence::new(value)
}
fn pair(app: u64, admin: u64) -> DualFrontier {
DualFrontier::new(application(app), administration(admin))
}
#[test]
fn initial_is_both_components_absent() {
assert!(DualFrontier::INITIAL.is_initial());
assert_eq!(DualFrontier::INITIAL.application(), None);
assert_eq!(DualFrontier::INITIAL.administration(), None);
assert!(!pair(1, 0).is_initial());
}
#[test]
fn canonical_bytes_round_trip_over_every_presence_combination() {
for frontier in [
DualFrontier::INITIAL,
pair(1, 0),
pair(0, 1),
pair(7, 9),
pair(u64::MAX, u64::MAX),
] {
let encoded = frontier.to_canonical_bytes();
assert_eq!(encoded.len(), DUAL_FRONTIER_BYTES);
assert_eq!(
DualFrontier::from_canonical_bytes(encoded).expect("canonical round trip"),
frontier
);
}
}
#[test]
fn absence_is_encoded_explicitly_and_never_confused_with_zero() {
let encoded = DualFrontier::INITIAL.to_canonical_bytes();
assert_eq!(encoded, [0_u8; DUAL_FRONTIER_BYTES]);
let present_zero = {
let mut bytes = encoded;
bytes[0] = COMPONENT_PRESENT;
bytes
};
assert_eq!(
DualFrontier::from_canonical_bytes(present_zero),
Err(DualFrontierError::ZeroSequence)
);
let absent_nonzero = {
let mut bytes = encoded;
bytes[8] = 1;
bytes
};
assert_eq!(
DualFrontier::from_canonical_bytes(absent_nonzero),
Err(DualFrontierError::NonCanonicalAbsence)
);
let unknown_tag = {
let mut bytes = encoded;
bytes[9] = 0x02;
bytes
};
assert_eq!(
DualFrontier::from_canonical_bytes(unknown_tag),
Err(DualFrontierError::InvalidShape)
);
}
#[test]
fn a_command_frame_advances_only_the_application_component() {
let predecessor = pair(3, 4);
let covered = pair(5, 4);
assert!(covered.advances_from(predecessor));
assert!(!predecessor.advances_from(covered));
assert_eq!(covered.partial_cmp(&predecessor), Some(Ordering::Greater));
}
#[test]
fn a_service_audit_frame_advances_only_the_administration_component() {
let predecessor = pair(5, 4);
let covered = pair(5, 6);
assert!(covered.advances_from(predecessor));
assert!(!predecessor.advances_from(covered));
}
#[test]
fn an_equal_pair_never_advances() {
assert!(!pair(5, 4).advances_from(pair(5, 4)));
assert!(!DualFrontier::INITIAL.advances_from(DualFrontier::INITIAL));
assert_eq!(pair(5, 4).partial_cmp(&pair(5, 4)), Some(Ordering::Equal));
}
#[test]
fn the_first_frame_advances_from_the_initial_frontier() {
assert!(pair(1, 0).advances_from(DualFrontier::INITIAL));
assert!(pair(0, 1).advances_from(DualFrontier::INITIAL));
assert!(pair(1, 1).advances_from(DualFrontier::INITIAL));
}
#[test]
fn an_incomparable_pair_is_never_an_advance_in_either_direction() {
let left = pair(9, 2);
let right = pair(3, 7);
assert_eq!(left.partial_cmp(&right), None);
assert_eq!(right.partial_cmp(&left), None);
assert!(!left.advances_from(right));
assert!(!right.advances_from(left));
}
#[test]
fn a_regressed_component_is_never_an_advance() {
assert!(!pair(4, 4).advances_from(pair(5, 4)));
assert!(!pair(5, 3).advances_from(pair(5, 4)));
assert!(!DualFrontier::INITIAL.advances_from(pair(1, 0)));
}
#[test]
fn display_names_both_components_without_leaking_anything_else() {
assert_eq!(pair(5, 4).to_string(), "(app 5, admin 4)");
assert_eq!(pair(5, 0).to_string(), "(app 5, admin none)");
assert_eq!(pair(0, 4).to_string(), "(app none, admin 4)");
assert_eq!(DualFrontier::INITIAL.to_string(), "(app none, admin none)");
}
}