Skip to main content

use_local/
lib.rs

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/// Error returned by local presence constructors.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum LocalValueError {
19    /// The supplied value was empty after trimming whitespace.
20    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/// A business location label or address string.
34#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub struct BusinessLocation(String);
36
37impl BusinessLocation {
38    /// Creates a business location label.
39    ///
40    /// # Errors
41    ///
42    /// Returns [`LocalValueError::Empty`] when the label is empty.
43    pub fn new(value: impl AsRef<str>) -> Result<Self, LocalValueError> {
44        non_empty(value, "business location").map(Self)
45    }
46
47    /// Returns the location label.
48    #[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/// Human-readable opening-hours label.
75#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
76pub struct OpeningHoursLabel(String);
77
78impl OpeningHoursLabel {
79    /// Creates an opening-hours label.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`LocalValueError::Empty`] when the label is empty.
84    pub fn new(value: impl AsRef<str>) -> Result<Self, LocalValueError> {
85        non_empty(value, "opening hours label").map(Self)
86    }
87
88    /// Returns the label text.
89    #[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/// Local category label for external surfaces.
102#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
103pub struct LocalCategory(String);
104
105impl LocalCategory {
106    /// Creates a local category label.
107    ///
108    /// # Errors
109    ///
110    /// Returns [`LocalValueError::Empty`] when the category is empty.
111    pub fn new(value: impl AsRef<str>) -> Result<Self, LocalValueError> {
112        non_empty(value, "local category").map(Self)
113    }
114
115    /// Returns the category label.
116    #[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/// Local business presence shape.
143#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
144pub enum BusinessPresenceKind {
145    /// Customers can visit a storefront.
146    Storefront,
147    /// The business serves customers in an area without public storefront visits.
148    ServiceArea,
149    /// The business has both storefront and service-area behavior.
150    Hybrid,
151}
152
153impl BusinessPresenceKind {
154    /// Returns `true` when the kind includes service-area behavior.
155    #[must_use]
156    pub const fn is_service_area(self) -> bool {
157        matches!(self, Self::ServiceArea | Self::Hybrid)
158    }
159}
160
161/// Local visibility hint for external profiles.
162#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
163pub enum LocalVisibilityHint {
164    /// Customers should book before arriving.
165    AppointmentOnly,
166    /// Walk-ins are accepted.
167    WalkInsAccepted,
168    /// Delivery is available.
169    Delivers,
170    /// Service happens at a customer location.
171    OnSiteService,
172    /// Service is available online.
173    OnlineService,
174    /// Local pickup is available.
175    LocalPickup,
176}
177
178/// A local service-area business record.
179#[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    /// Creates a local business descriptor.
192    ///
193    /// # Errors
194    ///
195    /// Returns [`LocalValueError::Empty`] when the business name is empty.
196    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    /// Sets a location label.
209    #[must_use]
210    pub fn with_location(mut self, location: BusinessLocation) -> Self {
211        self.location = Some(location);
212        self
213    }
214
215    /// Sets an opening-hours label.
216    #[must_use]
217    pub fn with_opening_hours(mut self, label: OpeningHoursLabel) -> Self {
218        self.opening_hours = Some(label);
219        self
220    }
221
222    /// Sets a service-area label.
223    ///
224    /// # Errors
225    ///
226    /// Returns [`LocalValueError::Empty`] when the label is empty.
227    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    /// Adds a local category.
236    #[must_use]
237    pub fn with_category(mut self, category: LocalCategory) -> Self {
238        self.categories.push(category);
239        self
240    }
241
242    /// Adds a visibility hint.
243    #[must_use]
244    pub fn with_visibility_hint(mut self, hint: LocalVisibilityHint) -> Self {
245        self.visibility_hints.push(hint);
246        self
247    }
248
249    /// Returns the business name.
250    #[must_use]
251    pub fn name(&self) -> &str {
252        &self.name
253    }
254
255    /// Returns the presence kind.
256    #[must_use]
257    pub const fn kind(&self) -> BusinessPresenceKind {
258        self.kind
259    }
260
261    /// Returns `true` when the business includes service-area behavior.
262    #[must_use]
263    pub const fn is_service_area_business(&self) -> bool {
264        self.kind.is_service_area()
265    }
266
267    /// Returns the optional service-area label.
268    #[must_use]
269    pub fn service_area_label(&self) -> Option<&str> {
270        self.service_area_label.as_deref()
271    }
272
273    /// Returns local categories.
274    #[must_use]
275    pub fn categories(&self) -> &[LocalCategory] {
276        &self.categories
277    }
278
279    /// Returns visibility hints.
280    #[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}