pub use kcode_k1_person_types::PersonId;
use kcode_k1_persons_projection::PersonAction;
pub use kcode_k1_txn_ordering::TxId;
const INVALID_PAYLOAD: &str = "invalid persons wire payload";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PersonsWire {
bytes: Vec<u8>,
}
impl PersonsWire {
pub fn create(name: String) -> Result<Self, String> {
PersonAction::create(name.clone())?;
let mut bytes = Vec::with_capacity(2 + name.len());
bytes.extend_from_slice(&[1, 1]);
bytes.extend_from_slice(name.as_bytes());
Ok(Self { bytes })
}
pub fn update(person: PersonId, name: String) -> Result<Self, String> {
PersonAction::update(person, name.clone())?;
let mut bytes = Vec::with_capacity(14 + name.len());
bytes.extend_from_slice(&[1, 2]);
bytes.extend_from_slice(person.as_tx_id().as_bytes());
bytes.extend_from_slice(name.as_bytes());
Ok(Self { bytes })
}
pub fn resolve(canonical: PersonId, alias: PersonId) -> Self {
let mut bytes = Vec::with_capacity(26);
bytes.extend_from_slice(&[1, 3]);
bytes.extend_from_slice(canonical.as_tx_id().as_bytes());
bytes.extend_from_slice(alias.as_tx_id().as_bytes());
Self { bytes }
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
}
fn invalid_payload() -> String {
INVALID_PAYLOAD.to_owned()
}
fn decode_person_id(bytes: &[u8]) -> Result<PersonId, String> {
let bytes = <[u8; 12]>::try_from(bytes).map_err(|_| invalid_payload())?;
Ok(PersonId::from_tx_id(TxId::from_bytes(bytes)))
}
fn decode_name(bytes: &[u8]) -> Result<String, String> {
std::str::from_utf8(bytes)
.map(str::to_owned)
.map_err(|_| invalid_payload())
}
pub fn decode(payload: &[u8]) -> Result<PersonAction, String> {
if payload.first() != Some(&1) {
return Err(invalid_payload());
}
let kind = payload.get(1).ok_or_else(invalid_payload)?;
match kind {
1 if (3..=130).contains(&payload.len()) => {
let name = decode_name(&payload[2..])?;
PersonAction::create(name).map_err(|_| invalid_payload())
}
2 if (15..=142).contains(&payload.len()) => {
let person = decode_person_id(&payload[2..14])?;
let name = decode_name(&payload[14..])?;
PersonAction::update(person, name).map_err(|_| invalid_payload())
}
3 if payload.len() == 26 => {
let canonical = decode_person_id(&payload[2..14])?;
let alias = decode_person_id(&payload[14..26])?;
Ok(PersonAction::resolve(canonical, alias))
}
_ => Err(invalid_payload()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fmt::Debug;
fn person(bytes: [u8; 12]) -> PersonId {
PersonId::from_tx_id(TxId::from_bytes(bytes))
}
fn assert_invalid(payload: &[u8]) {
assert_eq!(decode(payload), Err(INVALID_PAYLOAD.to_owned()));
}
#[test]
fn exact_bytes_and_roundtrips() {
let create = PersonsWire::create("Ada".to_owned()).unwrap();
assert_eq!(create.as_bytes(), b"\x01\x01Ada");
assert_eq!(
decode(create.as_bytes()).unwrap(),
PersonAction::create("Ada".to_owned()).unwrap()
);
let update_person = person([7; 12]);
let update = PersonsWire::update(update_person, "Grace".to_owned()).unwrap();
let mut update_bytes = vec![1, 2];
update_bytes.extend_from_slice(&[7; 12]);
update_bytes.extend_from_slice(b"Grace");
assert_eq!(update.as_bytes(), update_bytes);
assert_eq!(
decode(update.as_bytes()).unwrap(),
PersonAction::update(update_person, "Grace".to_owned()).unwrap()
);
let canonical = person([3; 12]);
let alias = person([9; 12]);
let resolve = PersonsWire::resolve(canonical, alias);
let mut resolve_bytes = vec![1, 3];
resolve_bytes.extend_from_slice(&[3; 12]);
resolve_bytes.extend_from_slice(&[9; 12]);
assert_eq!(resolve.as_bytes(), resolve_bytes);
assert_eq!(
decode(resolve.as_bytes()).unwrap(),
PersonAction::resolve(canonical, alias)
);
}
#[test]
fn unicode_and_name_boundaries_roundtrip() {
let unicode = "Zoë🙂".to_owned();
let wire = PersonsWire::create(unicode.clone()).unwrap();
let mut expected = vec![1, 1];
expected.extend_from_slice(unicode.as_bytes());
assert_eq!(wire.as_bytes(), expected);
assert_eq!(
decode(wire.as_bytes()).unwrap(),
PersonAction::create(unicode).unwrap()
);
for size in [1, 128] {
let name = "x".repeat(size);
let create = PersonsWire::create(name.clone()).unwrap();
assert_eq!(create.as_bytes().len(), size + 2);
assert_eq!(
decode(create.as_bytes()).unwrap(),
PersonAction::create(name.clone()).unwrap()
);
let update = PersonsWire::update(person([5; 12]), name.clone()).unwrap();
assert_eq!(update.as_bytes().len(), size + 14);
assert_eq!(
decode(update.as_bytes()).unwrap(),
PersonAction::update(person([5; 12]), name).unwrap()
);
}
}
#[test]
fn constructor_name_errors_are_preserved() {
let cases = [
("", "person name must be 1 through 128 UTF-8 bytes"),
("a\n", "person name must not contain control characters"),
(" ", "person name must contain a non-whitespace character"),
];
for (name, error) in cases {
assert_eq!(PersonsWire::create(name.to_owned()).unwrap_err(), error);
assert_eq!(
PersonsWire::update(person([1; 12]), name.to_owned()).unwrap_err(),
error
);
}
}
#[test]
fn malformed_payloads_collapse_to_one_error() {
assert_invalid(&[]);
assert_invalid(&[1]);
assert_invalid(&[2, 1, b'a']);
assert_invalid(&[1, 4, b'a']);
assert_invalid(&[1, 1]);
assert_invalid(&[1, 2]);
assert_invalid(&[1, 1, 0xff]);
let mut invalid_update_utf8 = vec![1, 2];
invalid_update_utf8.extend_from_slice(&[2; 12]);
invalid_update_utf8.push(0xff);
assert_invalid(&invalid_update_utf8);
for name in [vec![b' '], vec![b'\n'], vec![b'a'; 129]] {
let mut create = vec![1, 1];
create.extend_from_slice(&name);
assert_invalid(&create);
let mut update = vec![1, 2];
update.extend_from_slice(&[2; 12]);
update.extend_from_slice(&name);
assert_invalid(&update);
}
}
#[test]
fn resolve_rejects_short_and_trailing_payloads() {
let wire = PersonsWire::resolve(person([4; 12]), person([6; 12]));
assert_invalid(&wire.as_bytes()[..25]);
let mut trailing = wire.as_bytes().to_vec();
trailing.push(0);
assert_invalid(&trailing);
}
#[test]
fn wire_traits_include_thread_safety() {
fn assert_traits<T: Clone + Debug + Eq + PartialEq + Send + Sync>() {}
assert_traits::<PersonsWire>();
}
}