use std::fmt;
use snafu::ensure;
use crate::Result;
use crate::error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Party {
pub id: String,
pub transport_endpoint: Option<String>,
}
impl Party {
pub fn new(id: impl Into<String>, transport_endpoint: Option<String>) -> Self {
Party {
id: id.into(),
transport_endpoint,
}
}
pub fn inproc(id: impl Into<String>) -> Self {
Party {
id: id.into(),
transport_endpoint: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartyList {
parties: Vec<Party>,
}
impl PartyList {
pub fn new() -> Self {
PartyList {
parties: Vec::new(),
}
}
pub fn from_parties(parties: Vec<Party>) -> Self {
PartyList { parties }
}
pub fn parties(&self) -> &[Party] {
&self.parties
}
pub fn len(&self) -> usize {
self.parties.len()
}
pub fn is_empty(&self) -> bool {
self.parties.is_empty()
}
pub fn push(&mut self, party: Party) {
self.parties.push(party);
}
pub fn get(&self, idx: usize) -> Result<&Party> {
self.parties.get(idx).ok_or_else(|| {
error::PartyIndexOutOfRangeSnafu {
idx,
count: self.parties.len(),
}
.build()
})
}
pub fn find(&self, id: &str) -> Option<&Party> {
self.parties.iter().find(|p| p.id == id)
}
pub fn validate(&self, threshold: u32) -> Result<()> {
ensure!(!self.parties.is_empty(), error::EmptyPartyListSnafu {});
ensure!(threshold >= 1, error::ThresholdTooSmallSnafu { threshold });
ensure!(
threshold as usize <= self.parties.len(),
error::ThresholdTooLargeSnafu {
threshold,
party_count: self.parties.len(),
}
);
for (i, p) in self.parties.iter().enumerate() {
for q in &self.parties[i + 1..] {
ensure!(p.id != q.id, error::DuplicatePartyIdSnafu { id: &p.id });
}
}
Ok(())
}
}
impl Default for PartyList {
fn default() -> Self {
PartyList::new()
}
}
impl fmt::Display for Party {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.transport_endpoint {
Some(ep) => write!(f, "{}@{}", self.id, ep),
None => write!(f, "{}", self.id),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn party_inproc_has_no_endpoint() {
let p = Party::inproc("node-1");
assert_eq!(p.id, "node-1");
assert!(p.transport_endpoint.is_none());
assert_eq!(format!("{p}"), "node-1");
}
#[test]
fn party_display_with_endpoint() {
let p = Party::new("node-1", Some("quic://h:443".to_string()));
assert_eq!(format!("{p}"), "node-1@quic://h:443");
}
#[test]
fn party_list_get_out_of_range_errors() {
let list = PartyList::from_parties(vec![Party::inproc("a")]);
let err = list.get(5).unwrap_err();
assert!(
matches!(
err,
error::Error::PartyIndexOutOfRange {
idx: 5,
count: 1,
..
}
),
"expected PartyIndexOutOfRange, got {err:?}"
);
}
#[test]
fn party_list_validate_rejects_empty() {
let list = PartyList::new();
assert!(list.validate(1).is_err());
}
#[test]
fn party_list_validate_rejects_zero_threshold() {
let list = PartyList::from_parties(vec![Party::inproc("a"), Party::inproc("b")]);
let err = list.validate(0).unwrap_err();
assert!(matches!(
err,
error::Error::ThresholdTooSmall { threshold: 0, .. }
));
}
#[test]
fn party_list_validate_rejects_threshold_above_party_count() {
let list = PartyList::from_parties(vec![Party::inproc("a"), Party::inproc("b")]);
let err = list.validate(3).unwrap_err();
assert!(matches!(
err,
error::Error::ThresholdTooLarge {
threshold: 3,
party_count: 2,
..
}
));
}
#[test]
fn party_list_validate_rejects_duplicate_ids() {
let list = PartyList::from_parties(vec![Party::inproc("a"), Party::inproc("a")]);
let err = list.validate(1).unwrap_err();
assert!(matches!(err, error::Error::DuplicatePartyId { .. }));
}
#[test]
fn party_list_validate_accepts_valid_roster() {
let list = PartyList::from_parties(vec![
Party::inproc("a"),
Party::inproc("b"),
Party::inproc("c"),
]);
list.validate(2).expect("valid roster should pass");
}
#[test]
fn party_list_find_by_id() {
let list = PartyList::from_parties(vec![Party::inproc("a"), Party::inproc("b")]);
assert_eq!(list.find("b").map(|p| p.id.as_str()), Some("b"));
assert!(list.find("z").is_none());
}
}