1use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12use crate::address::Address;
13use crate::error::{AxCode, AxError};
14use crate::event::identity::RunId;
15
16const CITY: &str = "city";
18
19const PERSON: &str = "person";
24
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub enum Who {
38 City,
40 Person,
42 Resident(Address),
44}
45
46impl Who {
47 pub fn resident(addr: Address) -> Result<Who, AxError> {
55 if addr.as_str() == CITY || addr.as_str() == PERSON {
56 return Err(AxError::failure(
57 AxCode::InvalidArgs,
58 "attribute a ledger line",
59 addr.as_str(),
60 )
61 .with_recovery(
62 "give this resident an address other than `city` or `person`: both \
63 words name another party the ledger attributes lines to",
64 ));
65 }
66 Ok(Who::Resident(addr))
67 }
68
69 #[must_use]
71 pub fn as_str(&self) -> &str {
72 match self {
73 Who::City => CITY,
74 Who::Person => PERSON,
75 Who::Resident(addr) => addr.as_str(),
76 }
77 }
78
79 #[must_use]
89 pub fn handed_down_by(&self, predecessor: Option<RunId>, parent: Option<RunId>) -> String {
90 match self {
91 Who::City => "the city".to_owned(),
92 Who::Person => "the User".to_owned(),
93 Who::Resident(addr) => match predecessor.or(parent) {
94 Some(run) => format!("@{addr}, run {run}", addr = addr.as_str()),
95 None => format!("@{addr}", addr = addr.as_str()),
96 },
97 }
98 }
99
100 pub fn parse(raw: &str) -> Result<Who, AxError> {
106 match raw {
107 CITY => Ok(Who::City),
108 PERSON => Ok(Who::Person),
109 other => Ok(Who::Resident(Address::parse(other)?)),
110 }
111 }
112}
113
114#[must_use]
121pub fn escape_markup(text: &str) -> String {
122 text.chars()
123 .fold(String::with_capacity(text.len()), |mut out, c| {
124 match c {
125 '<' => out.push_str("<"),
126 '>' => out.push_str(">"),
127 '&' => out.push_str("&"),
128 other => out.push(other),
129 }
130 out
131 })
132}
133
134impl fmt::Display for Who {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.write_str(self.as_str())
137 }
138}
139
140impl Serialize for Who {
141 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
142 serializer.serialize_str(self.as_str())
143 }
144}
145
146impl<'de> Deserialize<'de> for Who {
147 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
148 let raw = String::deserialize(deserializer)?;
149 Who::parse(&raw).map_err(serde::de::Error::custom)
150 }
151}
152
153#[cfg(test)]
154#[allow(
155 clippy::unwrap_used,
156 clippy::expect_used,
157 clippy::panic,
158 clippy::indexing_slicing,
159 reason = "test code"
160)]
161mod tests {
162 use super::*;
163 use proptest::prelude::*;
164
165 proptest! {
166 #[test]
170 fn escaped_text_holds_no_bracket_and_reads_back(text in "[a-z<>&; ]{0,24}") {
171 let escaped = escape_markup(&text);
172 prop_assert!(!escaped.contains(['<', '>']), "{}", escaped);
173 let read = escaped
174 .replace("<", "<")
175 .replace(">", ">")
176 .replace("&", "&");
177 prop_assert_eq!(read, text);
178 }
179 }
180
181 #[test]
182 fn the_three_parties_write_the_words_the_histories_already_hold() {
183 let resident = Who::resident(Address::parse("lobby/scribe.md").unwrap()).unwrap();
184 assert_eq!(Who::City.to_string(), "city");
185 assert_eq!(Who::Person.to_string(), "person");
186 assert_eq!(resident.to_string(), "lobby/scribe.md");
187 for party in [Who::City, Who::Person, resident] {
188 assert_eq!(Who::parse(party.as_str()).unwrap(), party);
189 assert_eq!(
190 serde_json::to_string(&party).unwrap(),
191 format!("\"{party}\"")
192 );
193 }
194 }
195
196 #[test]
197 fn work_handed_down_names_the_run_that_handed_it_down() {
198 let lead = Who::resident(Address::parse("lab/lead").unwrap()).unwrap();
199 let parent = RunId::parse("0198f6a2-7c4a-7bbb-9d1e-000000000001").unwrap();
200 let before = RunId::parse("0198f6a2-7c4a-7bbb-9d1e-000000000002").unwrap();
201 assert_eq!(
202 [
203 Who::Person.handed_down_by(None, None),
204 Who::City.handed_down_by(None, Some(parent)),
205 lead.handed_down_by(None, Some(parent)),
206 lead.handed_down_by(Some(before), Some(parent)),
207 lead.handed_down_by(None, None),
208 ],
209 [
210 "the User".to_owned(),
211 "the city".to_owned(),
212 format!("@lab/lead, run {parent}"),
213 format!("@lab/lead, run {before}"),
214 "@lab/lead".to_owned(),
215 ]
216 );
217 }
218
219 #[test]
220 fn a_resident_may_not_answer_to_either_reserved_word() {
221 for reserved in ["city", "person"] {
222 let err = Who::resident(Address::parse(reserved).unwrap()).unwrap_err();
223 assert_eq!(err.code(), &AxCode::InvalidArgs);
224 }
225 }
226
227 #[test]
228 fn a_word_that_is_not_an_address_is_refused_rather_than_guessed() {
229 let err = Who::parse("../elsewhere").unwrap_err();
230 assert_eq!(err.code(), &AxCode::InvalidArgs);
231 }
232}