use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
use std::{error::Error, fmt};
pub const V1_SCHEMA_VERSION: u32 = 1;
pub const MAX_IDENTIFIER_LENGTH: usize = 256;
pub(crate) fn validate_bounded_opaque_identifier(
value: &str,
type_name: &str,
) -> Result<(), ValidationError> {
if value.trim().is_empty() {
return Err(ValidationError::new(format!(
"{type_name} must not be empty"
)));
}
if value.len() > MAX_IDENTIFIER_LENGTH {
return Err(ValidationError::new(format!(
"{type_name} exceeds the {MAX_IDENTIFIER_LENGTH} byte limit"
)));
}
if value.chars().any(char::is_control) {
return Err(ValidationError::new(format!(
"{type_name} must not contain control characters"
)));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError(pub(crate) String);
impl ValidationError {
pub(crate) fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl Error for ValidationError {}
macro_rules! bounded_identifier {
($name:ident) => {
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
let value = value.into();
$crate::common::validate_bounded_opaque_identifier(&value, stringify!($name))?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl std::str::FromStr for $name {
type Err = ValidationError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<String> for $name {
type Error = ValidationError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<&str> for $name {
type Error = ValidationError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<$name> for String {
fn from(value: $name) -> Self {
value.0
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(DeError::custom)
}
}
};
}
bounded_identifier!(TaskId);
bounded_identifier!(TraceId);
bounded_identifier!(ProjectId);
bounded_identifier!(SessionId);
bounded_identifier!(AgentId);
bounded_identifier!(TenantId);
bounded_identifier!(PlanId);
bounded_identifier!(ReceiptId);
bounded_identifier!(OutcomeId);
bounded_identifier!(CapabilityId);
bounded_identifier!(DecisionId);
pub fn deserialize_schema_version<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: Deserializer<'de>,
{
let version = u32::deserialize(deserializer)?;
if version == V1_SCHEMA_VERSION {
Ok(version)
} else {
Err(DeError::custom(format!(
"unsupported schema_version {version}; expected {V1_SCHEMA_VERSION}"
)))
}
}
pub fn deserialize_milliunit<'de, D>(deserializer: D) -> Result<u16, D::Error>
where
D: Deserializer<'de>,
{
let value = u16::deserialize(deserializer)?;
if value <= 1000 {
Ok(value)
} else {
Err(DeError::custom("milliunit must be between 0 and 1000"))
}
}
pub fn deserialize_optional_milliunit<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<u16>::deserialize(deserializer)?;
value.map_or(Ok(None), |value| {
if value <= 1000 {
Ok(Some(value))
} else {
Err(DeError::custom("milliunit must be between 0 and 1000"))
}
})
}
pub fn validate_schema_version(version: u32) -> Result<(), ValidationError> {
if version == V1_SCHEMA_VERSION {
Ok(())
} else {
Err(ValidationError::new(format!(
"unsupported schema_version {version}; expected {V1_SCHEMA_VERSION}"
)))
}
}
pub fn validate_milliunit(value: u16, field: &str) -> Result<(), ValidationError> {
if value <= 1000 {
Ok(())
} else {
Err(ValidationError::new(format!(
"{field} must be between 0 and 1000"
)))
}
}