Skip to main content

kcode_k1_persons_wire/
lib.rs

1pub 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}