use crate::{Context, JsonType, SchemaContext, Stage, TypeKind, ValidationError, Value};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::{fmt, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DomainError(&'static str);
impl fmt::Display for DomainError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
impl std::error::Error for DomainError {}
macro_rules! string_domain {
($name:ident, $rule:literal, $pattern:literal) => {
impl crate::AsyncCheck for $name {}
impl $name {
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.as_str()) }
}
impl Serialize for $name {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
String::deserialize(deserializer)?.parse().map_err(serde::de::Error::custom)
}
}
impl JsonType for $name {
fn decode_input(input: crate::Input<'_>, ctx: &mut Context, coerce: bool) -> Option<Self> {
Self::decode(crate::direct::scalar(input, ctx, "domain string")?, ctx, coerce)
}
fn decode(value: Value, ctx: &mut Context, _: bool) -> Option<Self> {
if let Value::String(text) = value {
match text.parse() {
Ok(value) => return Some(value),
Err(error) => ctx.push(ValidationError::new($rule, format!("{error}")).with_stage(Stage::Parse)),
}
} else {
ctx.push(ValidationError::new($rule, "expected a domain string").with_stage(Stage::Parse));
}
None
}
fn encode(&self, _: &mut Context) -> Value { Value::String(self.0.clone()) }
fn schema(_: &mut SchemaContext, _: bool) -> Value {
serde_json::json!({"type":"string", "pattern":$pattern, "x-rydantic-local-rules":[$rule]})
}
fn kind() -> TypeKind { TypeKind::String }
}
};
}
#[cfg(feature = "country")]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CountryAlpha2(String);
#[cfg(feature = "country")]
impl FromStr for CountryAlpha2 {
type Err = DomainError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() == 2
&& s.bytes().all(|b| b.is_ascii_uppercase())
&& rust_iso3166::from_alpha2(s).is_some()
{
Ok(Self(s.into()))
} else {
Err(DomainError(
"expected an uppercase ISO 3166-1 alpha-2 catalog code",
))
}
}
}
#[cfg(feature = "country")]
string_domain!(CountryAlpha2, "country_alpha2_catalog", "^[A-Z]{2}$");
#[cfg(feature = "country")]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CountryAlpha3(String);
#[cfg(feature = "country")]
impl FromStr for CountryAlpha3 {
type Err = DomainError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() == 3
&& s.bytes().all(|b| b.is_ascii_uppercase())
&& rust_iso3166::from_alpha3(s).is_some()
{
Ok(Self(s.into()))
} else {
Err(DomainError(
"expected an uppercase ISO 3166-1 alpha-3 catalog code",
))
}
}
}
#[cfg(feature = "country")]
string_domain!(CountryAlpha3, "country_alpha3_catalog", "^[A-Z]{3}$");
#[cfg(feature = "currency")]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CurrencyCode(String);
#[cfg(feature = "currency")]
impl FromStr for CurrencyCode {
type Err = DomainError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() == 3
&& s.bytes().all(|b| b.is_ascii_uppercase())
&& iso_currency::Currency::from_code(s).is_some()
{
Ok(Self(s.into()))
} else {
Err(DomainError(
"expected an uppercase code in the currency catalog",
))
}
}
}
#[cfg(feature = "currency")]
string_domain!(CurrencyCode, "currency_catalog", "^[A-Z]{3}$");
#[cfg(feature = "phone")]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PhoneNumber(String);
#[cfg(feature = "phone")]
impl PhoneNumber {
pub fn parse_with_region(text: &str, region: &str) -> Result<Self, DomainError> {
if region.len() != 2 || !region.bytes().all(|b| b.is_ascii_uppercase()) {
return Err(DomainError("expected an uppercase phone region code"));
}
if text.len() > 128
|| !text
.bytes()
.all(|b| b.is_ascii_digit() || b"+()- .".contains(&b))
{
return Err(DomainError(
"only digits and phone punctuation are accepted; extensions are unsupported",
));
}
let region = region
.parse()
.map_err(|_| DomainError("unknown phone region code"))?;
Self::parsed(text, Some(region))
}
fn parsed(text: &str, region: Option<phonenumber::country::Id>) -> Result<Self, DomainError> {
let number =
phonenumber::parse(region, text).map_err(|_| DomainError("invalid phone number"))?;
if number.extension().is_some() {
return Err(DomainError("phone extensions are unsupported"));
}
if !phonenumber::is_valid(&number) {
return Err(DomainError(
"phone number fails numbering-plan metadata validation",
));
}
let canonical = number.format().mode(phonenumber::Mode::E164).to_string();
if !Self::is_e164_shape(&canonical) {
return Err(DomainError("phone number exceeds canonical E.164 limits"));
}
Ok(Self(canonical))
}
fn is_e164_shape(text: &str) -> bool {
let bytes = text.as_bytes();
(3..=16).contains(&bytes.len())
&& bytes[0] == b'+'
&& (b'1'..=b'9').contains(&bytes[1])
&& bytes[2..].iter().all(u8::is_ascii_digit)
}
}
#[cfg(feature = "phone")]
impl FromStr for PhoneNumber {
type Err = DomainError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !Self::is_e164_shape(s) {
return Err(DomainError(
"expected canonical E.164 (+ and at most 15 digits), without extension",
));
}
let parsed = Self::parsed(s, None)?;
if parsed.as_str() != s {
return Err(DomainError("expected canonical E.164"));
}
Ok(parsed)
}
}
#[cfg(feature = "phone")]
string_domain!(PhoneNumber, "phone_e164_metadata", "^\\+[1-9][0-9]{1,14}$");