1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn non_empty(value: impl AsRef<str>, field: &'static str) -> Result<String, LocalValueError> {
8 let trimmed = value.as_ref().trim();
9 if trimmed.is_empty() {
10 Err(LocalValueError::Empty { field })
11 } else {
12 Ok(trimmed.to_string())
13 }
14}
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum LocalValueError {
19 Empty { field: &'static str },
21}
22
23impl fmt::Display for LocalValueError {
24 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::Empty { field } => write!(formatter, "{field} cannot be empty"),
27 }
28 }
29}
30
31impl Error for LocalValueError {}
32
33#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub struct BusinessLocation(String);
36
37impl BusinessLocation {
38 pub fn new(value: impl AsRef<str>) -> Result<Self, LocalValueError> {
44 non_empty(value, "business location").map(Self)
45 }
46
47 #[must_use]
49 pub fn as_str(&self) -> &str {
50 &self.0
51 }
52}
53
54impl AsRef<str> for BusinessLocation {
55 fn as_ref(&self) -> &str {
56 self.as_str()
57 }
58}
59
60impl fmt::Display for BusinessLocation {
61 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 formatter.write_str(self.as_str())
63 }
64}
65
66impl FromStr for BusinessLocation {
67 type Err = LocalValueError;
68
69 fn from_str(value: &str) -> Result<Self, Self::Err> {
70 Self::new(value)
71 }
72}
73
74#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
76pub struct OpeningHoursLabel(String);
77
78impl OpeningHoursLabel {
79 pub fn new(value: impl AsRef<str>) -> Result<Self, LocalValueError> {
85 non_empty(value, "opening hours label").map(Self)
86 }
87
88 #[must_use]
90 pub fn as_str(&self) -> &str {
91 &self.0
92 }
93}
94
95impl AsRef<str> for OpeningHoursLabel {
96 fn as_ref(&self) -> &str {
97 self.as_str()
98 }
99}
100
101#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
103pub struct LocalCategory(String);
104
105impl LocalCategory {
106 pub fn new(value: impl AsRef<str>) -> Result<Self, LocalValueError> {
112 non_empty(value, "local category").map(Self)
113 }
114
115 #[must_use]
117 pub fn as_str(&self) -> &str {
118 &self.0
119 }
120}
121
122impl AsRef<str> for LocalCategory {
123 fn as_ref(&self) -> &str {
124 self.as_str()
125 }
126}
127
128impl fmt::Display for LocalCategory {
129 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130 formatter.write_str(self.as_str())
131 }
132}
133
134impl FromStr for LocalCategory {
135 type Err = LocalValueError;
136
137 fn from_str(value: &str) -> Result<Self, Self::Err> {
138 Self::new(value)
139 }
140}
141
142#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
144pub enum BusinessPresenceKind {
145 Storefront,
147 ServiceArea,
149 Hybrid,
151}
152
153impl BusinessPresenceKind {
154 #[must_use]
156 pub const fn is_service_area(self) -> bool {
157 matches!(self, Self::ServiceArea | Self::Hybrid)
158 }
159}
160
161#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
163pub enum LocalVisibilityHint {
164 AppointmentOnly,
166 WalkInsAccepted,
168 Delivers,
170 OnSiteService,
172 OnlineService,
174 LocalPickup,
176}
177
178#[derive(Clone, Debug, Eq, PartialEq)]
180pub struct ServiceAreaBusiness {
181 name: String,
182 kind: BusinessPresenceKind,
183 location: Option<BusinessLocation>,
184 opening_hours: Option<OpeningHoursLabel>,
185 service_area_label: Option<String>,
186 categories: Vec<LocalCategory>,
187 visibility_hints: Vec<LocalVisibilityHint>,
188}
189
190impl ServiceAreaBusiness {
191 pub fn new(name: impl AsRef<str>, kind: BusinessPresenceKind) -> Result<Self, LocalValueError> {
197 Ok(Self {
198 name: non_empty(name, "business name")?,
199 kind,
200 location: None,
201 opening_hours: None,
202 service_area_label: None,
203 categories: Vec::new(),
204 visibility_hints: Vec::new(),
205 })
206 }
207
208 #[must_use]
210 pub fn with_location(mut self, location: BusinessLocation) -> Self {
211 self.location = Some(location);
212 self
213 }
214
215 #[must_use]
217 pub fn with_opening_hours(mut self, label: OpeningHoursLabel) -> Self {
218 self.opening_hours = Some(label);
219 self
220 }
221
222 pub fn with_service_area_label(
228 mut self,
229 label: impl AsRef<str>,
230 ) -> Result<Self, LocalValueError> {
231 self.service_area_label = Some(non_empty(label, "service area label")?);
232 Ok(self)
233 }
234
235 #[must_use]
237 pub fn with_category(mut self, category: LocalCategory) -> Self {
238 self.categories.push(category);
239 self
240 }
241
242 #[must_use]
244 pub fn with_visibility_hint(mut self, hint: LocalVisibilityHint) -> Self {
245 self.visibility_hints.push(hint);
246 self
247 }
248
249 #[must_use]
251 pub fn name(&self) -> &str {
252 &self.name
253 }
254
255 #[must_use]
257 pub const fn kind(&self) -> BusinessPresenceKind {
258 self.kind
259 }
260
261 #[must_use]
263 pub const fn is_service_area_business(&self) -> bool {
264 self.kind.is_service_area()
265 }
266
267 #[must_use]
269 pub fn service_area_label(&self) -> Option<&str> {
270 self.service_area_label.as_deref()
271 }
272
273 #[must_use]
275 pub fn categories(&self) -> &[LocalCategory] {
276 &self.categories
277 }
278
279 #[must_use]
281 pub fn visibility_hints(&self) -> &[LocalVisibilityHint] {
282 &self.visibility_hints
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::{
289 BusinessLocation, BusinessPresenceKind, LocalCategory, LocalValueError,
290 LocalVisibilityHint, OpeningHoursLabel, ServiceAreaBusiness,
291 };
292
293 #[test]
294 fn validates_local_labels() {
295 assert_eq!(
296 BusinessLocation::new(" "),
297 Err(LocalValueError::Empty {
298 field: "business location"
299 })
300 );
301 assert_eq!(
302 OpeningHoursLabel::new("Mon-Fri").unwrap().as_str(),
303 "Mon-Fri"
304 );
305 }
306
307 #[test]
308 fn composes_service_area_business() {
309 let business = ServiceAreaBusiness::new("Example Plumbing", BusinessPresenceKind::Hybrid)
310 .unwrap()
311 .with_category(LocalCategory::new("Plumber").unwrap())
312 .with_visibility_hint(LocalVisibilityHint::OnSiteService)
313 .with_service_area_label("Metro area")
314 .unwrap();
315
316 assert!(business.is_service_area_business());
317 assert_eq!(business.service_area_label(), Some("Metro area"));
318 assert_eq!(business.categories().len(), 1);
319 }
320}