use std::{fmt, str::FromStr, sync::Arc};
use serde::{Deserialize, Serialize};
macro_rules! identifier {
($name:ident, $label:literal) => {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
let value = value.into();
validate($label, &value)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl FromStr for $name {
type Err = IdentifierError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
};
}
identifier!(ExecutionId, "execution_id");
identifier!(ConversationId, "conversation_id");
identifier!(RuntimeInstanceId, "runtime_instance_id");
identifier!(WorkspaceId, "workspace_id");
identifier!(ToolCallId, "tool_call_id");
identifier!(OperationId, "operation_id");
identifier!(EventId, "event_id");
identifier!(PackageId, "package_id");
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DelegationLeaseRef(String);
impl DelegationLeaseRef {
pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
let value = value.into();
if !value.starts_with("edl_") {
return Err(IdentifierError {
label: "delegation_lease_ref",
reason: "must be an opaque edl_ reference",
});
}
validate("delegation_lease_ref", &value)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for DelegationLeaseRef {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("DelegationLeaseRef([REDACTED OPAQUE REFERENCE])")
}
}
impl fmt::Display for DelegationLeaseRef {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("[REDACTED OPAQUE REFERENCE]")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CallerScope {
pub subject: String,
pub tenant_id: String,
pub project_id: String,
#[serde(default)]
pub capabilities: Vec<String>,
}
impl CallerScope {
pub fn validate(&self) -> Result<(), IdentifierError> {
validate("subject", &self.subject)?;
validate("tenant_id", &self.tenant_id)?;
validate("project_id", &self.project_id)?;
Ok(())
}
}
#[derive(Clone)]
pub struct CredentialHandle(Arc<str>);
impl CredentialHandle {
pub fn new(value: impl Into<Arc<str>>) -> Result<Self, IdentifierError> {
let value = value.into();
if value.is_empty() || value.len() > 16 * 1024 || value.chars().any(char::is_control) {
return Err(IdentifierError {
label: "credential",
reason: "must contain 1..=16384 non-control bytes",
});
}
Ok(Self(value))
}
pub fn expose(&self) -> &str {
&self.0
}
}
impl fmt::Debug for CredentialHandle {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("CredentialHandle([REDACTED])")
}
}
#[derive(Clone, Debug)]
pub struct RequestAuthority {
pub caller: CallerScope,
pub credential: CredentialHandle,
}
impl ExecutionId {
pub fn random() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
}
impl OperationId {
pub fn random() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
}
impl EventId {
pub fn random() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdentifierError {
label: &'static str,
reason: &'static str,
}
impl fmt::Display for IdentifierError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{} {}", self.label, self.reason)
}
}
impl std::error::Error for IdentifierError {}
fn validate(label: &'static str, value: &str) -> Result<(), IdentifierError> {
if value.is_empty() {
return Err(IdentifierError {
label,
reason: "must not be empty",
});
}
if value.len() > 256 {
return Err(IdentifierError {
label,
reason: "must not exceed 256 bytes",
});
}
if value.chars().any(char::is_control) {
return Err(IdentifierError {
label,
reason: "must not contain control characters",
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identifiers_reject_empty_and_control_characters() {
assert!(ExecutionId::new("").is_err());
assert!(ExecutionId::new("bad\nvalue").is_err());
assert_eq!(
ExecutionId::new("execution-1").unwrap().as_str(),
"execution-1"
);
}
}