Skip to main content

kernel/event/
who.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Who a ledger line is attributed to.
7
8use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12use crate::address::Address;
13use crate::error::{AxCode, AxError};
14
15/// The city's own desk, as every ledger line it writes spells it.
16const CITY: &str = "city";
17
18/// A human being, as a ledger line spells them. One word for all of
19/// them: the ledger says an instruction came from outside the machine,
20/// and which person it was is the session's business rather than the
21/// history's.
22const PERSON: &str = "person";
23
24/// The three parties a ledger line can be attributed to.
25///
26/// Every line of every history carries one of these, and until this
27/// type existed each writer spelled its own: six call sites wrote the
28/// literal `"city"` and a resident wrote its address, so a seventh
29/// spelling was one keystroke away and nothing would have refused it.
30///
31/// **The wire form is what the histories already hold.** The city is
32/// `city`, a person is `person`, and a resident is its address, exactly
33/// as before; no ledger on disk has to be re-spelled for this type to
34/// read it.
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub enum Who {
37    /// The city's desk: genesis, dispatch, truncation, relaying.
38    City,
39    /// A person, through a client.
40    Person,
41    /// A resident of a building, at the address it is known by.
42    Resident(Address),
43}
44
45impl Who {
46    /// A resident's attribution.
47    ///
48    /// # Errors
49    /// `E_INVALID_ARGS` for an address spelled `city` or `person`:
50    /// those two words are the other two parties on this wire, and a
51    /// resident answering to one of them would make a line's author
52    /// unreadable.
53    pub fn resident(addr: Address) -> Result<Who, AxError> {
54        if addr.as_str() == CITY || addr.as_str() == PERSON {
55            return Err(AxError::failure(
56                AxCode::InvalidArgs,
57                "attribute a ledger line",
58                addr.as_str(),
59            )
60            .with_recovery(
61                "give this resident an address other than `city` or `person`: both \
62                 words name another party the ledger attributes lines to",
63            ));
64        }
65        Ok(Who::Resident(addr))
66    }
67
68    /// The word a ledger line carries.
69    #[must_use]
70    pub fn as_str(&self) -> &str {
71        match self {
72            Who::City => CITY,
73            Who::Person => PERSON,
74            Who::Resident(addr) => addr.as_str(),
75        }
76    }
77
78    /// Reads back what [`Who::as_str`] wrote.
79    ///
80    /// # Errors
81    /// `E_INVALID_ARGS` for a word that is neither reserved nor a
82    /// canonical address.
83    pub fn parse(raw: &str) -> Result<Who, AxError> {
84        match raw {
85            CITY => Ok(Who::City),
86            PERSON => Ok(Who::Person),
87            other => Ok(Who::Resident(Address::parse(other)?)),
88        }
89    }
90}
91
92impl fmt::Display for Who {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        f.write_str(self.as_str())
95    }
96}
97
98impl Serialize for Who {
99    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
100        serializer.serialize_str(self.as_str())
101    }
102}
103
104impl<'de> Deserialize<'de> for Who {
105    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
106        let raw = String::deserialize(deserializer)?;
107        Who::parse(&raw).map_err(serde::de::Error::custom)
108    }
109}
110
111#[cfg(test)]
112#[allow(
113    clippy::unwrap_used,
114    clippy::expect_used,
115    clippy::panic,
116    clippy::indexing_slicing,
117    reason = "test code"
118)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn the_three_parties_write_the_words_the_histories_already_hold() {
124        let resident = Who::resident(Address::parse("lobby/scribe.md").unwrap()).unwrap();
125        assert_eq!(Who::City.to_string(), "city");
126        assert_eq!(Who::Person.to_string(), "person");
127        assert_eq!(resident.to_string(), "lobby/scribe.md");
128        for party in [Who::City, Who::Person, resident] {
129            assert_eq!(Who::parse(party.as_str()).unwrap(), party);
130            assert_eq!(
131                serde_json::to_string(&party).unwrap(),
132                format!("\"{party}\"")
133            );
134        }
135    }
136
137    #[test]
138    fn a_resident_may_not_answer_to_either_reserved_word() {
139        for reserved in ["city", "person"] {
140            let err = Who::resident(Address::parse(reserved).unwrap()).unwrap_err();
141            assert_eq!(err.code(), &AxCode::InvalidArgs);
142        }
143    }
144
145    #[test]
146    fn a_word_that_is_not_an_address_is_refused_rather_than_guessed() {
147        let err = Who::parse("../elsewhere").unwrap_err();
148        assert_eq!(err.code(), &AxCode::InvalidArgs);
149    }
150}