use core::fmt;
use core::str::FromStr;
use uuid::Uuid;
use crate::error::IdError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct SiteId(Uuid);
impl SiteId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
#[must_use]
pub const fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
#[must_use]
pub const fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for SiteId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for SiteId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl FromStr for SiteId {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Uuid::parse_str(s).map(Self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct PlanId(Uuid);
impl PlanId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
#[must_use]
pub const fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for PlanId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for PlanId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
macro_rules! slug_id {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct $name(String);
impl $name {
pub fn new(name: impl AsRef<str>) -> Result<Self, IdError> {
let name = name.as_ref().trim().to_ascii_lowercase();
if name.is_empty() {
return Err(IdError::Empty);
}
if name.chars().count() > 64 {
return Err(IdError::TooLong(name.chars().count()));
}
if let Some(bad) = name
.chars()
.find(|c| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')))
{
return Err(IdError::BadCharacter(bad));
}
Ok(Self(name))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for $name {
type Err = IdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl TryFrom<String> for $name {
type Error = IdError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<$name> for String {
fn from(value: $name) -> Self {
value.0
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
};
}
slug_id!(
AssetId
);
slug_id!(
CircuitId
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn asset_ids_are_case_folded_and_validated() {
assert_eq!(
AssetId::new("Wallbox-Garage").unwrap().as_str(),
"wallbox-garage"
);
assert_eq!(AssetId::new(" battery ").unwrap().as_str(), "battery");
assert_eq!(AssetId::new(""), Err(IdError::Empty));
assert_eq!(AssetId::new("wall box"), Err(IdError::BadCharacter(' ')));
assert_eq!(AssetId::new("a".repeat(65)), Err(IdError::TooLong(65)));
}
#[test]
fn site_ids_are_time_ordered() {
let a = SiteId::new();
let b = SiteId::new();
assert!(a < b, "v7 UUIDs sort by creation time");
}
}