use std::borrow::Borrow;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{0} must not be empty")]
pub struct EmptyId(pub &'static str);
macro_rules! define_id {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent))]
#[serde(try_from = "String", into = "String")]
pub struct $name(String);
impl $name {
pub fn parse(value: impl Into<String>) -> Result<Self, EmptyId> {
let value = value.into();
if value.trim().is_empty() {
return Err(EmptyId(stringify!($name)));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({:?})", stringify!($name), 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 = EmptyId;
fn from_str(value: &str) -> Result<Self, EmptyId> {
Self::parse(value)
}
}
impl TryFrom<String> for $name {
type Error = EmptyId;
fn try_from(value: String) -> Result<Self, EmptyId> {
Self::parse(value)
}
}
impl TryFrom<&str> for $name {
type Error = EmptyId;
fn try_from(value: &str) -> Result<Self, EmptyId> {
Self::parse(value)
}
}
impl TryFrom<&String> for $name {
type Error = EmptyId;
fn try_from(value: &String) -> Result<Self, EmptyId> {
Self::parse(value.as_str())
}
}
impl From<&$name> for $name {
fn from(value: &$name) -> Self {
value.clone()
}
}
impl From<$name> for String {
fn from(value: $name) -> Self {
value.0
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for $name {
fn borrow(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for $name {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl PartialEq<str> for $name {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for $name {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<String> for $name {
fn eq(&self, other: &String) -> bool {
&self.0 == other
}
}
impl PartialEq<$name> for String {
fn eq(&self, other: &$name) -> bool {
self == &other.0
}
}
impl PartialEq<$name> for &str {
fn eq(&self, other: &$name) -> bool {
*self == other.0
}
}
};
}
define_id!(
TenantId
);
define_id!(
SubjectId
);
define_id!(
RequestId
);
define_id!(
SessionId
);
define_id!(
RunId
);
define_id!(
InputId
);
define_id!(
ToolCallId
);
define_id!(
InteractionId
);
define_id!(
ProfileRevisionId
);
define_id!(
InstanceId
);
define_id!(
ActionIntentId
);
define_id!(
NodeId
);
define_id!(
SpaceId
);
define_id!(
RevisionId
);
define_id!(
AssetId
);
define_id!(
ReleaseId
);
define_id!(
InviteId
);
define_id!(
CommandId
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ids_are_distinct_types_with_string_behaviour() {
let session = SessionId::try_from("s1").unwrap();
assert_eq!(session, "s1");
assert_eq!(session.to_string(), "s1");
assert_eq!(serde_json::to_string(&session).unwrap(), "\"s1\"");
let parsed: SessionId = serde_json::from_str("\"s2\"").unwrap();
assert_eq!(parsed.as_str(), "s2");
assert_eq!(format!("{session:?}"), "SessionId(\"s1\")");
assert_eq!(SessionId::parse(" "), Err(EmptyId("SessionId")));
assert!("".parse::<RunId>().is_err());
let mut set = std::collections::BTreeSet::new();
set.insert(RunId::try_from(String::from("r")).unwrap());
assert!(set.contains("r"));
}
#[test]
fn every_constructor_rejects_empty_and_whitespace() {
for blank in ["", " ", "\t\n"] {
assert_eq!(TenantId::try_from(blank), Err(EmptyId("TenantId")));
assert_eq!(
TenantId::try_from(blank.to_owned()),
Err(EmptyId("TenantId"))
);
assert_eq!(
TenantId::try_from(&blank.to_owned()),
Err(EmptyId("TenantId"))
);
assert_eq!(blank.parse::<TenantId>(), Err(EmptyId("TenantId")));
let json = serde_json::to_string(blank).unwrap();
let rejected = serde_json::from_str::<TenantId>(&json).unwrap_err();
assert!(
rejected.to_string().contains("TenantId must not be empty"),
"{rejected}"
);
#[derive(serde::Deserialize)]
struct Envelope {
#[allow(dead_code)]
tenant_id: TenantId,
}
assert!(
serde_json::from_str::<Envelope>(&format!("{{\"tenant_id\":{json}}}")).is_err()
);
}
}
}