#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
fn non_empty(value: impl AsRef<str>, field: &'static str) -> Result<String, LocalValueError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(LocalValueError::Empty { field })
} else {
Ok(trimmed.to_string())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LocalValueError {
Empty { field: &'static str },
}
impl fmt::Display for LocalValueError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty { field } => write!(formatter, "{field} cannot be empty"),
}
}
}
impl Error for LocalValueError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct BusinessLocation(String);
impl BusinessLocation {
pub fn new(value: impl AsRef<str>) -> Result<Self, LocalValueError> {
non_empty(value, "business location").map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for BusinessLocation {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for BusinessLocation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for BusinessLocation {
type Err = LocalValueError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct OpeningHoursLabel(String);
impl OpeningHoursLabel {
pub fn new(value: impl AsRef<str>) -> Result<Self, LocalValueError> {
non_empty(value, "opening hours label").map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for OpeningHoursLabel {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LocalCategory(String);
impl LocalCategory {
pub fn new(value: impl AsRef<str>) -> Result<Self, LocalValueError> {
non_empty(value, "local category").map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for LocalCategory {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for LocalCategory {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for LocalCategory {
type Err = LocalValueError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum BusinessPresenceKind {
Storefront,
ServiceArea,
Hybrid,
}
impl BusinessPresenceKind {
#[must_use]
pub const fn is_service_area(self) -> bool {
matches!(self, Self::ServiceArea | Self::Hybrid)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum LocalVisibilityHint {
AppointmentOnly,
WalkInsAccepted,
Delivers,
OnSiteService,
OnlineService,
LocalPickup,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServiceAreaBusiness {
name: String,
kind: BusinessPresenceKind,
location: Option<BusinessLocation>,
opening_hours: Option<OpeningHoursLabel>,
service_area_label: Option<String>,
categories: Vec<LocalCategory>,
visibility_hints: Vec<LocalVisibilityHint>,
}
impl ServiceAreaBusiness {
pub fn new(name: impl AsRef<str>, kind: BusinessPresenceKind) -> Result<Self, LocalValueError> {
Ok(Self {
name: non_empty(name, "business name")?,
kind,
location: None,
opening_hours: None,
service_area_label: None,
categories: Vec::new(),
visibility_hints: Vec::new(),
})
}
#[must_use]
pub fn with_location(mut self, location: BusinessLocation) -> Self {
self.location = Some(location);
self
}
#[must_use]
pub fn with_opening_hours(mut self, label: OpeningHoursLabel) -> Self {
self.opening_hours = Some(label);
self
}
pub fn with_service_area_label(
mut self,
label: impl AsRef<str>,
) -> Result<Self, LocalValueError> {
self.service_area_label = Some(non_empty(label, "service area label")?);
Ok(self)
}
#[must_use]
pub fn with_category(mut self, category: LocalCategory) -> Self {
self.categories.push(category);
self
}
#[must_use]
pub fn with_visibility_hint(mut self, hint: LocalVisibilityHint) -> Self {
self.visibility_hints.push(hint);
self
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn kind(&self) -> BusinessPresenceKind {
self.kind
}
#[must_use]
pub const fn is_service_area_business(&self) -> bool {
self.kind.is_service_area()
}
#[must_use]
pub fn service_area_label(&self) -> Option<&str> {
self.service_area_label.as_deref()
}
#[must_use]
pub fn categories(&self) -> &[LocalCategory] {
&self.categories
}
#[must_use]
pub fn visibility_hints(&self) -> &[LocalVisibilityHint] {
&self.visibility_hints
}
}
#[cfg(test)]
mod tests {
use super::{
BusinessLocation, BusinessPresenceKind, LocalCategory, LocalValueError,
LocalVisibilityHint, OpeningHoursLabel, ServiceAreaBusiness,
};
#[test]
fn validates_local_labels() {
assert_eq!(
BusinessLocation::new(" "),
Err(LocalValueError::Empty {
field: "business location"
})
);
assert_eq!(
OpeningHoursLabel::new("Mon-Fri").unwrap().as_str(),
"Mon-Fri"
);
}
#[test]
fn composes_service_area_business() {
let business = ServiceAreaBusiness::new("Example Plumbing", BusinessPresenceKind::Hybrid)
.unwrap()
.with_category(LocalCategory::new("Plumber").unwrap())
.with_visibility_hint(LocalVisibilityHint::OnSiteService)
.with_service_area_label("Metro area")
.unwrap();
assert!(business.is_service_area_business());
assert_eq!(business.service_area_label(), Some("Metro area"));
assert_eq!(business.categories().len(), 1);
}
}