use crate::error::{DomainError, DomainErrorKind};
use stillwater::refined::{Predicate, Refined};
use uuid::Uuid as UuidImpl;
#[derive(Debug, Clone, Copy, Default)]
pub struct ValidUuid;
impl Predicate<String> for ValidUuid {
type Error = DomainError;
fn check(value: &String) -> Result<(), Self::Error> {
UuidImpl::parse_str(value)
.map(|_| ())
.map_err(|_| DomainError {
format_name: "UUID",
value: value.clone(),
reason: DomainErrorKind::InvalidFormat {
expected: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
},
example: "550e8400-e29b-41d4-a716-446655440000",
})
}
fn description() -> &'static str {
"UUID"
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct UuidVersion<const V: usize>;
impl<const V: usize> Predicate<String> for UuidVersion<V> {
type Error = DomainError;
fn check(value: &String) -> Result<(), Self::Error> {
let parsed = UuidImpl::parse_str(value).map_err(|_| DomainError {
format_name: "UUID",
value: value.clone(),
reason: DomainErrorKind::InvalidFormat {
expected: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
},
example: "550e8400-e29b-41d4-a716-446655440000",
})?;
let actual_version = parsed.get_version_num();
if actual_version == V {
Ok(())
} else {
Err(DomainError {
format_name: uuid_version_name::<V>(),
value: value.clone(),
reason: DomainErrorKind::InvalidComponent {
component: "version",
reason: format!("expected v{}, got v{}", V, actual_version),
},
example: uuid_version_example::<V>(),
})
}
}
fn description() -> &'static str {
"UUID with specific version"
}
}
const fn uuid_version_name<const V: usize>() -> &'static str {
match V {
4 => "UUID v4",
7 => "UUID v7",
_ => unreachable!(),
}
}
const fn uuid_version_example<const V: usize>() -> &'static str {
match V {
4 => "550e8400-e29b-41d4-a716-446655440000",
7 => "018f6b8e-e4a0-7000-8000-000000000000",
_ => unreachable!(),
}
}
pub type Uuid = Refined<String, ValidUuid>;
pub type UuidV4 = Refined<String, UuidVersion<4>>;
pub type UuidV7 = Refined<String, UuidVersion<7>>;
pub trait ToUuid {
fn to_uuid(&self) -> UuidImpl;
}
impl<P: Predicate<String>> ToUuid for Refined<String, P> {
fn to_uuid(&self) -> UuidImpl {
UuidImpl::parse_str(self.get()).expect("already validated")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_uuid_v4() {
assert!(Uuid::new("550e8400-e29b-41d4-a716-446655440000".to_string()).is_ok());
}
#[test]
fn valid_uuid_v7() {
assert!(Uuid::new("018f6b8e-e4a0-7000-8000-000000000000".to_string()).is_ok());
}
#[test]
fn valid_uuid_lowercase() {
assert!(Uuid::new("550e8400-e29b-41d4-a716-446655440000".to_string()).is_ok());
}
#[test]
fn valid_uuid_uppercase() {
assert!(Uuid::new("550E8400-E29B-41D4-A716-446655440000".to_string()).is_ok());
}
#[test]
fn invalid_uuid_format() {
assert!(Uuid::new("not-a-uuid".to_string()).is_err());
}
#[test]
fn invalid_uuid_too_short() {
assert!(Uuid::new("550e8400-e29b-41d4".to_string()).is_err());
}
#[test]
fn invalid_uuid_wrong_chars() {
assert!(Uuid::new("550e8400-e29b-41d4-a716-44665544gggg".to_string()).is_err());
}
#[test]
fn uuid_v4_accepts_v4() {
let v4 = "550e8400-e29b-41d4-a716-446655440000";
assert!(UuidV4::new(v4.to_string()).is_ok());
}
#[test]
fn uuid_v4_rejects_v7() {
let v7 = "018f6b8e-e4a0-7000-8000-000000000000";
let result = UuidV4::new(v7.to_string());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(
err.reason,
DomainErrorKind::InvalidComponent { .. }
));
}
#[test]
fn uuid_v7_accepts_v7() {
let v7 = "018f6b8e-e4a0-7000-8000-000000000000";
assert!(UuidV7::new(v7.to_string()).is_ok());
}
#[test]
fn uuid_v7_rejects_v4() {
let v4 = "550e8400-e29b-41d4-a716-446655440000";
let result = UuidV7::new(v4.to_string());
assert!(result.is_err());
}
#[test]
fn to_uuid_returns_correct_type() {
let validated = Uuid::new("550e8400-e29b-41d4-a716-446655440000".to_string()).unwrap();
let uuid_impl = validated.to_uuid();
assert_eq!(uuid_impl.get_version_num(), 4);
}
#[test]
fn uuid_v4_to_uuid_is_version_4() {
let validated = UuidV4::new("550e8400-e29b-41d4-a716-446655440000".to_string()).unwrap();
let uuid_impl = validated.to_uuid();
assert_eq!(uuid_impl.get_version_num(), 4);
}
#[test]
fn uuid_v7_to_uuid_is_version_7() {
let validated = UuidV7::new("018f6b8e-e4a0-7000-8000-000000000000".to_string()).unwrap();
let uuid_impl = validated.to_uuid();
assert_eq!(uuid_impl.get_version_num(), 7);
}
#[test]
fn error_includes_format_name() {
let result = Uuid::new("invalid".to_string());
let err = result.unwrap_err();
assert_eq!(err.format_name, "UUID");
}
#[test]
fn error_includes_example() {
let result = Uuid::new("invalid".to_string());
let err = result.unwrap_err();
assert_eq!(err.example, "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn version_error_is_invalid_component() {
let result = UuidV4::new("018f6b8e-e4a0-7000-8000-000000000000".to_string());
let err = result.unwrap_err();
match err.reason {
DomainErrorKind::InvalidComponent { component, reason } => {
assert_eq!(component, "version");
assert!(reason.contains("v4"));
assert!(reason.contains("v7"));
}
_ => panic!("Expected InvalidComponent error"),
}
}
#[test]
fn valid_uuid_description() {
assert_eq!(ValidUuid::description(), "UUID");
}
#[test]
fn uuid_version_description() {
assert_eq!(
UuidVersion::<4>::description(),
"UUID with specific version"
);
}
#[test]
fn uuid_v4_rejects_malformed() {
let result = UuidV4::new("not-a-uuid".to_string());
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.format_name, "UUID");
assert!(matches!(err.reason, DomainErrorKind::InvalidFormat { .. }));
}
#[test]
fn uuid_v7_rejects_malformed() {
let result = UuidV7::new("invalid".to_string());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err.reason, DomainErrorKind::InvalidFormat { .. }));
}
}