kcode_k1_persons_wire/
lib.rs1pub use kcode_k1_person_types::PersonId;
2use kcode_k1_persons_projection::PersonAction;
3pub use kcode_k1_txn_ordering::TxId;
4
5const INVALID_PAYLOAD: &str = "invalid persons wire payload";
6
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub struct PersonsWire {
9 bytes: Vec<u8>,
10}
11
12impl PersonsWire {
13 pub fn create(name: String) -> Result<Self, String> {
14 PersonAction::create(name.clone())?;
15 let mut bytes = Vec::with_capacity(2 + name.len());
16 bytes.extend_from_slice(&[1, 1]);
17 bytes.extend_from_slice(name.as_bytes());
18 Ok(Self { bytes })
19 }
20
21 pub fn update(person: PersonId, name: String) -> Result<Self, String> {
22 PersonAction::update(person, name.clone())?;
23 let mut bytes = Vec::with_capacity(14 + name.len());
24 bytes.extend_from_slice(&[1, 2]);
25 bytes.extend_from_slice(person.as_tx_id().as_bytes());
26 bytes.extend_from_slice(name.as_bytes());
27 Ok(Self { bytes })
28 }
29
30 pub fn resolve(canonical: PersonId, alias: PersonId) -> Self {
31 let mut bytes = Vec::with_capacity(26);
32 bytes.extend_from_slice(&[1, 3]);
33 bytes.extend_from_slice(canonical.as_tx_id().as_bytes());
34 bytes.extend_from_slice(alias.as_tx_id().as_bytes());
35 Self { bytes }
36 }
37
38 pub fn as_bytes(&self) -> &[u8] {
39 &self.bytes
40 }
41}
42
43fn invalid_payload() -> String {
44 INVALID_PAYLOAD.to_owned()
45}
46
47fn decode_person_id(bytes: &[u8]) -> Result<PersonId, String> {
48 let bytes = <[u8; 12]>::try_from(bytes).map_err(|_| invalid_payload())?;
49 Ok(PersonId::from_tx_id(TxId::from_bytes(bytes)))
50}
51
52fn decode_name(bytes: &[u8]) -> Result<String, String> {
53 std::str::from_utf8(bytes)
54 .map(str::to_owned)
55 .map_err(|_| invalid_payload())
56}
57
58pub fn decode(payload: &[u8]) -> Result<PersonAction, String> {
59 if payload.first() != Some(&1) {
60 return Err(invalid_payload());
61 }
62 let kind = payload.get(1).ok_or_else(invalid_payload)?;
63 match kind {
64 1 if (3..=130).contains(&payload.len()) => {
65 let name = decode_name(&payload[2..])?;
66 PersonAction::create(name).map_err(|_| invalid_payload())
67 }
68 2 if (15..=142).contains(&payload.len()) => {
69 let person = decode_person_id(&payload[2..14])?;
70 let name = decode_name(&payload[14..])?;
71 PersonAction::update(person, name).map_err(|_| invalid_payload())
72 }
73 3 if payload.len() == 26 => {
74 let canonical = decode_person_id(&payload[2..14])?;
75 let alias = decode_person_id(&payload[14..26])?;
76 Ok(PersonAction::resolve(canonical, alias))
77 }
78 _ => Err(invalid_payload()),
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use std::fmt::Debug;
86
87 fn person(bytes: [u8; 12]) -> PersonId {
88 PersonId::from_tx_id(TxId::from_bytes(bytes))
89 }
90
91 fn assert_invalid(payload: &[u8]) {
92 assert_eq!(decode(payload), Err(INVALID_PAYLOAD.to_owned()));
93 }
94
95 #[test]
96 fn exact_bytes_and_roundtrips() {
97 let create = PersonsWire::create("Ada".to_owned()).unwrap();
98 assert_eq!(create.as_bytes(), b"\x01\x01Ada");
99 assert_eq!(
100 decode(create.as_bytes()).unwrap(),
101 PersonAction::create("Ada".to_owned()).unwrap()
102 );
103
104 let update_person = person([7; 12]);
105 let update = PersonsWire::update(update_person, "Grace".to_owned()).unwrap();
106 let mut update_bytes = vec![1, 2];
107 update_bytes.extend_from_slice(&[7; 12]);
108 update_bytes.extend_from_slice(b"Grace");
109 assert_eq!(update.as_bytes(), update_bytes);
110 assert_eq!(
111 decode(update.as_bytes()).unwrap(),
112 PersonAction::update(update_person, "Grace".to_owned()).unwrap()
113 );
114
115 let canonical = person([3; 12]);
116 let alias = person([9; 12]);
117 let resolve = PersonsWire::resolve(canonical, alias);
118 let mut resolve_bytes = vec![1, 3];
119 resolve_bytes.extend_from_slice(&[3; 12]);
120 resolve_bytes.extend_from_slice(&[9; 12]);
121 assert_eq!(resolve.as_bytes(), resolve_bytes);
122 assert_eq!(
123 decode(resolve.as_bytes()).unwrap(),
124 PersonAction::resolve(canonical, alias)
125 );
126 }
127
128 #[test]
129 fn unicode_and_name_boundaries_roundtrip() {
130 let unicode = "Zoë🙂".to_owned();
131 let wire = PersonsWire::create(unicode.clone()).unwrap();
132 let mut expected = vec![1, 1];
133 expected.extend_from_slice(unicode.as_bytes());
134 assert_eq!(wire.as_bytes(), expected);
135 assert_eq!(
136 decode(wire.as_bytes()).unwrap(),
137 PersonAction::create(unicode).unwrap()
138 );
139
140 for size in [1, 128] {
141 let name = "x".repeat(size);
142 let create = PersonsWire::create(name.clone()).unwrap();
143 assert_eq!(create.as_bytes().len(), size + 2);
144 assert_eq!(
145 decode(create.as_bytes()).unwrap(),
146 PersonAction::create(name.clone()).unwrap()
147 );
148 let update = PersonsWire::update(person([5; 12]), name.clone()).unwrap();
149 assert_eq!(update.as_bytes().len(), size + 14);
150 assert_eq!(
151 decode(update.as_bytes()).unwrap(),
152 PersonAction::update(person([5; 12]), name).unwrap()
153 );
154 }
155 }
156
157 #[test]
158 fn constructor_name_errors_are_preserved() {
159 let cases = [
160 ("", "person name must be 1 through 128 UTF-8 bytes"),
161 ("a\n", "person name must not contain control characters"),
162 (" ", "person name must contain a non-whitespace character"),
163 ];
164 for (name, error) in cases {
165 assert_eq!(PersonsWire::create(name.to_owned()).unwrap_err(), error);
166 assert_eq!(
167 PersonsWire::update(person([1; 12]), name.to_owned()).unwrap_err(),
168 error
169 );
170 }
171 }
172
173 #[test]
174 fn malformed_payloads_collapse_to_one_error() {
175 assert_invalid(&[]);
176 assert_invalid(&[1]);
177 assert_invalid(&[2, 1, b'a']);
178 assert_invalid(&[1, 4, b'a']);
179 assert_invalid(&[1, 1]);
180 assert_invalid(&[1, 2]);
181 assert_invalid(&[1, 1, 0xff]);
182
183 let mut invalid_update_utf8 = vec![1, 2];
184 invalid_update_utf8.extend_from_slice(&[2; 12]);
185 invalid_update_utf8.push(0xff);
186 assert_invalid(&invalid_update_utf8);
187
188 for name in [vec![b' '], vec![b'\n'], vec![b'a'; 129]] {
189 let mut create = vec![1, 1];
190 create.extend_from_slice(&name);
191 assert_invalid(&create);
192 let mut update = vec![1, 2];
193 update.extend_from_slice(&[2; 12]);
194 update.extend_from_slice(&name);
195 assert_invalid(&update);
196 }
197 }
198
199 #[test]
200 fn resolve_rejects_short_and_trailing_payloads() {
201 let wire = PersonsWire::resolve(person([4; 12]), person([6; 12]));
202 assert_invalid(&wire.as_bytes()[..25]);
203 let mut trailing = wire.as_bytes().to_vec();
204 trailing.push(0);
205 assert_invalid(&trailing);
206 }
207
208 #[test]
209 fn wire_traits_include_thread_safety() {
210 fn assert_traits<T: Clone + Debug + Eq + PartialEq + Send + Sync>() {}
211 assert_traits::<PersonsWire>();
212 }
213}