1#![doc(html_root_url = "https://docs.rs/krabka-voters/0.8.0")]
10
11use std::collections::{BTreeMap, BTreeSet};
12
13pub use krabka_ids::NodeId;
19use serde::{Deserialize, Serialize};
20use uuid::Uuid;
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct VoterEndpoint {
25 pub name: String,
26 pub host: String,
27 pub port: u16,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub struct KRaftVersionRange {
33 pub min: u16,
34 pub max: u16,
35}
36
37impl Default for KRaftVersionRange {
38 fn default() -> Self {
39 Self { min: 0, max: 1 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
45pub struct Voter {
46 pub id: NodeId,
47 pub directory_id: Uuid,
48 pub endpoints: Vec<VoterEndpoint>,
49 pub kraft_version: KRaftVersionRange,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
54pub struct VoterSet {
55 voters: BTreeMap<NodeId, Voter>,
56}
57
58impl VoterSet {
59 #[must_use]
60 pub fn from_voters(voters: impl IntoIterator<Item = Voter>) -> Self {
61 Self {
62 voters: voters.into_iter().map(|v| (v.id, v)).collect(),
63 }
64 }
65
66 #[must_use]
67 pub fn contains(&self, id: NodeId) -> bool {
68 self.voters.contains_key(&id)
69 }
70
71 #[must_use]
72 pub fn get(&self, id: NodeId) -> Option<&Voter> {
73 self.voters.get(&id)
74 }
75
76 #[must_use]
77 pub fn ids(&self) -> BTreeSet<NodeId> {
78 self.voters.keys().copied().collect()
79 }
80
81 #[must_use]
82 pub fn len(&self) -> usize {
83 self.voters.len()
84 }
85
86 #[must_use]
87 pub fn is_empty(&self) -> bool {
88 self.voters.is_empty()
89 }
90
91 pub fn iter(&self) -> impl Iterator<Item = &Voter> {
92 self.voters.values()
93 }
94
95 #[must_use]
97 pub fn with_voter(&self, voter: Voter) -> Self {
98 let mut next = self.clone();
99 next.voters.insert(voter.id, voter);
100 next
101 }
102
103 #[must_use]
105 pub fn without_voter(&self, id: NodeId) -> Self {
106 let mut next = self.clone();
107 next.voters.remove(&id);
108 next
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use assert2::check;
115
116 use super::*;
117
118 fn sample(id: NodeId) -> Voter {
119 Voter {
120 id,
121 directory_id: Uuid::from_u128(u128::from(id.0)),
122 endpoints: vec![VoterEndpoint {
123 name: "CONTROLLER".into(),
124 host: "127.0.0.1".into(),
125 port: 9093,
126 }],
127 kraft_version: KRaftVersionRange::default(),
128 }
129 }
130
131 #[test]
132 fn add_remove_are_immutable_copies() {
133 let base = VoterSet::from_voters([sample(NodeId(1))]);
134 let added = base.with_voter(sample(NodeId(2)));
135 let removed = added.without_voter(NodeId(1));
136 assert2::assert!(base.ids().into_iter().collect::<Vec<_>>() == vec![NodeId(1)]);
137 assert2::assert!(added.ids().into_iter().collect::<Vec<_>>() == vec![NodeId(1), NodeId(2)]);
138 assert2::assert!(removed.ids().into_iter().collect::<Vec<_>>() == vec![NodeId(2)]);
139 }
140
141 #[test]
142 fn ids_are_sorted() {
143 let set = VoterSet::from_voters([sample(NodeId(3)), sample(NodeId(1)), sample(NodeId(2))]);
144 assert2::assert!(
145 set.ids().into_iter().collect::<Vec<_>>() == vec![NodeId(1), NodeId(2), NodeId(3)]
146 );
147 }
148
149 #[test]
150 fn accessors_reflect_contents() {
151 let set = VoterSet::from_voters([sample(NodeId(1)), sample(NodeId(2))]);
152 check!(
153 (
154 set.len(),
155 set.is_empty(),
156 set.get(NodeId(1)),
157 set.get(NodeId(99)),
158 set.iter().count(),
159 set.contains(NodeId(1)),
162 set.contains(NodeId(99)),
163 ) == (2, false, Some(&sample(NodeId(1))), None, 2, true, false)
164 );
165
166 let empty = VoterSet::default();
167 check!((empty.len(), empty.is_empty(), empty.iter().count()) == (0, true, 0));
168 }
169}