use serde::{Deserialize, Serialize};
use std::borrow::Borrow;
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
use uuid::Uuid;
pub const MAX_ID_LEN: usize = 512;
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum IdentityError {
#[error("{kind} must not be empty")]
Empty {
kind: &'static str,
},
#[error("{kind} exceeds maximum length of {max} bytes")]
TooLong {
kind: &'static str,
max: usize,
},
#[error("{kind} must not contain null bytes")]
ContainsNull {
kind: &'static str,
},
}
fn validate(value: &str, kind: &'static str) -> Result<(), IdentityError> {
if value.is_empty() {
return Err(IdentityError::Empty { kind });
}
if value.len() > MAX_ID_LEN {
return Err(IdentityError::TooLong { kind, max: MAX_ID_LEN });
}
if value.contains('\0') {
return Err(IdentityError::ContainsNull { kind });
}
Ok(())
}
macro_rules! define_id {
(
$(#[$meta:meta])*
$name:ident, $kind:literal
) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn new(value: impl Into<String>) -> Result<Self, IdentityError> {
let value = value.into();
validate(&value, $kind)?;
Ok(Self(value))
}
pub fn new_unchecked(value: impl Into<String>) -> Self {
Self(value.into())
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for $name {
fn borrow(&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 = IdentityError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
validate(s, $kind)?;
Ok(Self(s.to_owned()))
}
}
impl TryFrom<&str> for $name {
type Error = IdentityError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
validate(s, $kind)?;
Ok(Self(s.to_owned()))
}
}
impl TryFrom<String> for $name {
type Error = IdentityError;
fn try_from(s: String) -> Result<Self, Self::Error> {
validate(&s, $kind)?;
Ok(Self(s))
}
}
};
}
define_id! {
AppName, "AppName"
}
define_id! {
UserId, "UserId"
}
define_id! {
SessionId, "SessionId"
}
define_id! {
InvocationId, "InvocationId"
}
impl SessionId {
pub fn generate() -> Self {
Self(Uuid::new_v4().to_string())
}
}
impl InvocationId {
pub fn generate() -> Self {
Self(Uuid::new_v4().to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AdkIdentity {
pub app_name: AppName,
pub user_id: UserId,
pub session_id: SessionId,
}
impl AdkIdentity {
pub fn new(app_name: AppName, user_id: UserId, session_id: SessionId) -> Self {
Self { app_name, user_id, session_id }
}
}
impl fmt::Display for AdkIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"AdkIdentity(app=\"{}\", user=\"{}\", session=\"{}\")",
self.app_name, self.user_id, self.session_id
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionIdentity {
pub adk: AdkIdentity,
pub invocation_id: InvocationId,
pub branch: String,
pub agent_name: String,
}
impl From<IdentityError> for crate::AdkError {
fn from(err: IdentityError) -> Self {
crate::AdkError::config(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_name_valid() {
let app = AppName::try_from("my-app").unwrap();
assert_eq!(app.as_ref(), "my-app");
assert_eq!(app.to_string(), "my-app");
}
#[test]
fn test_app_name_with_special_chars() {
let app = AppName::try_from("org:team/app@v2").unwrap();
assert_eq!(app.as_ref(), "org:team/app@v2");
}
#[test]
fn test_empty_rejected() {
let err = AppName::try_from("").unwrap_err();
assert_eq!(err, IdentityError::Empty { kind: "AppName" });
}
#[test]
fn test_null_byte_rejected() {
let err = UserId::try_from("user\0id").unwrap_err();
assert_eq!(err, IdentityError::ContainsNull { kind: "UserId" });
}
#[test]
fn test_too_long_rejected() {
let long = "x".repeat(MAX_ID_LEN + 1);
let err = SessionId::try_from(long.as_str()).unwrap_err();
assert_eq!(err, IdentityError::TooLong { kind: "SessionId", max: MAX_ID_LEN });
}
#[test]
fn test_max_length_accepted() {
let exact = "a".repeat(MAX_ID_LEN);
assert!(SessionId::try_from(exact.as_str()).is_ok());
}
#[test]
fn test_from_str() {
let app: AppName = "hello".parse().unwrap();
assert_eq!(app.as_ref(), "hello");
}
#[test]
fn test_try_from_string() {
let s = String::from("owned-value");
let uid = UserId::try_from(s).unwrap();
assert_eq!(uid.as_ref(), "owned-value");
}
#[test]
fn test_borrow_str() {
let sid = SessionId::try_from("sess-1").unwrap();
let borrowed: &str = sid.borrow();
assert_eq!(borrowed, "sess-1");
}
#[test]
fn test_ord() {
let a = AppName::try_from("aaa").unwrap();
let b = AppName::try_from("bbb").unwrap();
assert!(a < b);
}
#[test]
fn test_session_id_generate() {
let a = SessionId::generate();
let b = SessionId::generate();
assert_ne!(a, b);
assert!(!a.as_ref().is_empty());
}
#[test]
fn test_invocation_id_generate() {
let a = InvocationId::generate();
let b = InvocationId::generate();
assert_ne!(a, b);
assert!(!a.as_ref().is_empty());
}
#[test]
fn test_adk_identity_new() {
let identity = AdkIdentity::new(
AppName::try_from("app").unwrap(),
UserId::try_from("user").unwrap(),
SessionId::try_from("sess").unwrap(),
);
assert_eq!(identity.app_name.as_ref(), "app");
assert_eq!(identity.user_id.as_ref(), "user");
assert_eq!(identity.session_id.as_ref(), "sess");
}
#[test]
fn test_adk_identity_display() {
let identity = AdkIdentity::new(
AppName::try_from("weather-app").unwrap(),
UserId::try_from("alice").unwrap(),
SessionId::try_from("abc-123").unwrap(),
);
let display = identity.to_string();
assert!(display.contains("weather-app"));
assert!(display.contains("alice"));
assert!(display.contains("abc-123"));
assert!(display.starts_with("AdkIdentity("));
}
#[test]
fn test_adk_identity_equality() {
let a = AdkIdentity::new(
AppName::try_from("app").unwrap(),
UserId::try_from("user").unwrap(),
SessionId::try_from("sess").unwrap(),
);
let b = a.clone();
assert_eq!(a, b);
let c = AdkIdentity::new(
AppName::try_from("app").unwrap(),
UserId::try_from("other-user").unwrap(),
SessionId::try_from("sess").unwrap(),
);
assert_ne!(a, c);
}
#[test]
fn test_adk_identity_hash() {
use std::collections::HashSet;
let a = AdkIdentity::new(
AppName::try_from("app").unwrap(),
UserId::try_from("user").unwrap(),
SessionId::try_from("sess").unwrap(),
);
let b = a.clone();
let mut set = HashSet::new();
set.insert(a);
set.insert(b);
assert_eq!(set.len(), 1);
}
#[test]
fn test_execution_identity() {
let exec = ExecutionIdentity {
adk: AdkIdentity::new(
AppName::try_from("app").unwrap(),
UserId::try_from("user").unwrap(),
SessionId::try_from("sess").unwrap(),
),
invocation_id: InvocationId::try_from("inv-1").unwrap(),
branch: String::new(),
agent_name: "root".to_string(),
};
assert_eq!(exec.adk.app_name.as_ref(), "app");
assert_eq!(exec.invocation_id.as_ref(), "inv-1");
assert_eq!(exec.branch, "");
assert_eq!(exec.agent_name, "root");
}
#[test]
fn test_serde_round_trip_leaf() {
let app = AppName::try_from("my-app").unwrap();
let json = serde_json::to_string(&app).unwrap();
assert_eq!(json, "\"my-app\"");
let deserialized: AppName = serde_json::from_str(&json).unwrap();
assert_eq!(app, deserialized);
}
#[test]
fn test_serde_round_trip_adk_identity() {
let identity = AdkIdentity::new(
AppName::try_from("app").unwrap(),
UserId::try_from("user").unwrap(),
SessionId::try_from("sess").unwrap(),
);
let json = serde_json::to_string(&identity).unwrap();
let deserialized: AdkIdentity = serde_json::from_str(&json).unwrap();
assert_eq!(identity, deserialized);
}
#[test]
fn test_serde_round_trip_execution_identity() {
let exec = ExecutionIdentity {
adk: AdkIdentity::new(
AppName::try_from("app").unwrap(),
UserId::try_from("user").unwrap(),
SessionId::try_from("sess").unwrap(),
),
invocation_id: InvocationId::try_from("inv-1").unwrap(),
branch: "main".to_string(),
agent_name: "agent".to_string(),
};
let json = serde_json::to_string(&exec).unwrap();
let deserialized: ExecutionIdentity = serde_json::from_str(&json).unwrap();
assert_eq!(exec, deserialized);
}
#[test]
fn test_identity_error_display() {
let err = IdentityError::Empty { kind: "AppName" };
assert_eq!(err.to_string(), "AppName must not be empty");
let err = IdentityError::TooLong { kind: "UserId", max: 512 };
assert_eq!(err.to_string(), "UserId exceeds maximum length of 512 bytes");
let err = IdentityError::ContainsNull { kind: "SessionId" };
assert_eq!(err.to_string(), "SessionId must not contain null bytes");
}
#[test]
fn test_identity_error_to_adk_error() {
let err = IdentityError::Empty { kind: "AppName" };
let adk_err: crate::AdkError = err.into();
assert!(adk_err.is_config());
assert!(adk_err.to_string().contains("AppName must not be empty"));
}
}