1use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12use crate::address::Address;
13use crate::error::{AxCode, AxError};
14
15const CITY: &str = "city";
18
19const BUILDING: &str = "building";
21
22const WORKSHOP: &str = "workshop";
24
25const AT: char = ':';
27
28#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
40pub enum Scope {
41 City,
43 Building(Address),
45 Workshop(Address),
47}
48
49impl Scope {
50 #[must_use]
56 pub fn covers(&self, addr: &Address) -> bool {
57 match self {
58 Scope::City => true,
59 Scope::Building(scope) | Scope::Workshop(scope) => addr.is_within(scope),
60 }
61 }
62
63 pub fn parse(raw: &str) -> Result<Scope, AxError> {
70 if raw == CITY {
71 return Ok(Scope::City);
72 }
73 let unknown = || {
74 AxError::failure(AxCode::WireMismatch, "read which scope a line names", raw)
75 .with_recovery(
76 "open this city with the build that wrote its history: this one knows \
77 `city`, `building:<address>` and `workshop:<address>`",
78 )
79 };
80 let (kind, addr) = raw.split_once(AT).ok_or_else(unknown)?;
81 let addr = Address::parse(addr)?;
82 match kind {
83 BUILDING => Ok(Scope::Building(addr)),
84 WORKSHOP => Ok(Scope::Workshop(addr)),
85 _ => Err(unknown()),
86 }
87 }
88}
89
90impl fmt::Display for Scope {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 match self {
93 Scope::City => f.write_str(CITY),
94 Scope::Building(addr) => write!(f, "{BUILDING}{AT}{addr}", addr = addr.as_str()),
95 Scope::Workshop(addr) => write!(f, "{WORKSHOP}{AT}{addr}", addr = addr.as_str()),
96 }
97 }
98}
99
100impl Serialize for Scope {
101 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
102 serializer.serialize_str(&self.to_string())
103 }
104}
105
106impl<'de> Deserialize<'de> for Scope {
107 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
108 let raw = String::deserialize(deserializer)?;
109 Scope::parse(&raw).map_err(serde::de::Error::custom)
110 }
111}
112
113#[cfg(test)]
114#[allow(
115 clippy::unwrap_used,
116 clippy::expect_used,
117 clippy::panic,
118 clippy::indexing_slicing,
119 reason = "test code"
120)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn the_three_scopes_write_the_words_the_histories_already_hold() {
126 let building = Scope::Building(Address::parse("lab").unwrap());
127 let workshop = Scope::Workshop(Address::parse("lab/bench").unwrap());
128 assert_eq!(Scope::City.to_string(), "city");
129 assert_eq!(building.to_string(), "building:lab");
130 assert_eq!(workshop.to_string(), "workshop:lab/bench");
131 for scope in [Scope::City, building, workshop] {
132 assert_eq!(Scope::parse(&scope.to_string()).unwrap(), scope);
133 assert_eq!(
134 serde_json::to_string(&scope).unwrap(),
135 format!("\"{scope}\"")
136 );
137 }
138 }
139
140 #[test]
141 fn a_scope_reaches_what_is_inside_it_and_nothing_else() {
142 let inside = Address::parse("lab/bench.md").unwrap();
143 let elsewhere = Address::parse("hall/clerk.md").unwrap();
144 let lab = Scope::Building(Address::parse("lab").unwrap());
145 assert!(Scope::City.covers(&inside));
146 assert!(lab.covers(&inside));
147 assert!(!lab.covers(&elsewhere));
148 }
149
150 #[test]
151 fn a_word_naming_no_scope_is_refused_rather_than_guessed() {
152 for raw in ["lab", "borough:lab", "building:"] {
153 assert!(Scope::parse(raw).is_err(), "{raw} should not read");
154 }
155 }
156}