use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;
use uuid::Uuid;
pub const MAX_IDENTITY_BYTES: usize = 256;
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum IdentityError {
#[error("identity must be non-empty")]
Empty,
#[error("identity exceeds maximum length of {MAX_IDENTITY_BYTES} bytes")]
TooLong,
#[error("identity must not contain control characters")]
ControlCharacter,
}
pub fn validate_identity_string(value: &str) -> Result<(), IdentityError> {
if value.is_empty() {
return Err(IdentityError::Empty);
}
if value.len() > MAX_IDENTITY_BYTES {
return Err(IdentityError::TooLong);
}
if value.chars().any(|c| c.is_control()) {
return Err(IdentityError::ControlCharacter);
}
Ok(())
}
fn validated_string(value: impl Into<String>) -> Result<String, IdentityError> {
let s = value.into();
validate_identity_string(&s)?;
Ok(s)
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ConnectionId(String);
impl ConnectionId {
pub fn new(value: impl Into<String>) -> Self {
Self::try_new(value).expect("ConnectionId::new requires a valid identity string")
}
pub fn try_new(value: impl Into<String>) -> Result<Self, IdentityError> {
Ok(Self(validated_string(value)?))
}
pub fn generate() -> Self {
Self(Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ConnectionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MonoloopRunId(String);
impl MonoloopRunId {
pub fn new(value: impl Into<String>) -> Self {
Self::try_new(value).expect("MonoloopRunId::new requires a valid identity string")
}
pub fn try_new(value: impl Into<String>) -> Result<Self, IdentityError> {
Ok(Self(validated_string(value)?))
}
pub fn generate() -> Self {
Self(Uuid::new_v4().to_string())
}
pub fn from_transaction(id: &TransactionId) -> Self {
Self(format!("txn:{}", id.as_uuid()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for MonoloopRunId {
fn default() -> Self {
Self::generate()
}
}
impl fmt::Display for MonoloopRunId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ExternalSessionId(String);
impl ExternalSessionId {
pub fn new(value: impl Into<String>) -> Self {
Self::try_new(value).expect("ExternalSessionId::new requires a valid identity string")
}
pub fn try_new(value: impl Into<String>) -> Result<Self, IdentityError> {
Ok(Self(validated_string(value)?))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ExternalSessionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<external-session>")
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GrokSessionId(ExternalSessionId);
impl GrokSessionId {
pub fn new(value: impl Into<String>) -> Self {
Self::try_new(value).expect("GrokSessionId::new requires a valid identity string")
}
pub fn try_new(value: impl Into<String>) -> Result<Self, IdentityError> {
Ok(Self(ExternalSessionId::try_new(value)?))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn as_external(&self) -> &ExternalSessionId {
&self.0
}
pub fn into_external(self) -> ExternalSessionId {
self.0
}
}
impl From<GrokSessionId> for ExternalSessionId {
fn from(value: GrokSessionId) -> Self {
value.0
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RequestId(String);
impl RequestId {
pub fn new(value: impl Into<String>) -> Self {
Self::try_new(value).expect("RequestId::new requires a valid identity string")
}
pub fn try_new(value: impl Into<String>) -> Result<Self, IdentityError> {
Ok(Self(validated_string(value)?))
}
pub fn generate() -> Self {
Self(Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TransactionId(Uuid);
impl TransactionId {
pub fn generate() -> Self {
Self(Uuid::new_v4())
}
pub fn from_uuid(id: Uuid) -> Self {
Self(id)
}
pub fn as_uuid(&self) -> Uuid {
self.0
}
pub fn as_str(&self) -> String {
self.0.to_string()
}
}
impl fmt::Display for TransactionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ExchangeId(Uuid);
impl ExchangeId {
pub fn generate() -> Self {
Self(Uuid::new_v4())
}
pub fn from_uuid(id: Uuid) -> Self {
Self(id)
}
pub fn as_uuid(&self) -> Uuid {
self.0
}
}
impl fmt::Display for ExchangeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SessionId(String);
impl SessionId {
pub fn try_new(value: impl Into<String>) -> Result<Self, IdentityError> {
Ok(Self(validated_string(value)?))
}
pub fn generate() -> Self {
Self(Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_external(&self) -> ExternalSessionId {
ExternalSessionId(self.0.clone())
}
pub fn into_external(self) -> ExternalSessionId {
ExternalSessionId(self.0)
}
pub fn from_external(id: &ExternalSessionId) -> Self {
Self(id.0.clone())
}
}
impl fmt::Display for SessionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<session>")
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChannelId(String);
impl ChannelId {
pub fn try_new(value: impl Into<String>) -> Result<Self, IdentityError> {
Ok(Self(validated_string(value)?))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ChannelId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SessionKey {
pub channel_id: ChannelId,
pub session_id: SessionId,
}
impl SessionKey {
pub fn new(channel_id: ChannelId, session_id: SessionId) -> Self {
Self {
channel_id,
session_id,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ToolId(String);
impl ToolId {
pub fn try_new(value: impl Into<String>) -> Result<Self, IdentityError> {
Ok(Self(validated_string(value)?))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ToolId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ToolName(String);
impl ToolName {
pub fn try_new(value: impl Into<String>) -> Result<Self, IdentityError> {
Ok(Self(validated_string(value)?))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ToolName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_empty_and_control() {
assert_eq!(SessionId::try_new(""), Err(IdentityError::Empty));
assert_eq!(
ChannelId::try_new("a\nb"),
Err(IdentityError::ControlCharacter)
);
assert!(ToolId::try_new("x".repeat(MAX_IDENTITY_BYTES + 1)).is_err());
}
#[test]
fn session_key_isolates_channels() {
let a = SessionKey::new(
ChannelId::try_new("ch-a").unwrap(),
SessionId::try_new("same-sess").unwrap(),
);
let b = SessionKey::new(
ChannelId::try_new("ch-b").unwrap(),
SessionId::try_new("same-sess").unwrap(),
);
assert_ne!(a, b);
assert_eq!(a.session_id.as_str(), b.session_id.as_str());
}
#[test]
fn session_external_round_trip_bytes() {
let ext = ExternalSessionId::try_new("provider-abc").unwrap();
let sid = SessionId::from_external(&ext);
assert_eq!(sid.as_str(), ext.as_str());
assert_eq!(sid.into_external().as_str(), "provider-abc");
}
#[test]
fn transaction_id_serializes() {
let id = TransactionId::generate();
let json = serde_json::to_string(&id).unwrap();
let back: TransactionId = serde_json::from_str(&json).unwrap();
assert_eq!(id, back);
}
}