#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::{
borrow::ToOwned,
string::{String, ToString},
};
use core::{fmt, ops::Deref, str::FromStr};
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum CorrelationIdError {
#[error("correlation ID must not be empty")]
Empty,
#[error("correlation ID must not exceed 255 characters")]
TooLong,
#[error("correlation ID may only contain printable ASCII characters (0x20–0x7E)")]
InvalidChars,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CorrelationId(String);
impl CorrelationId {
pub fn new(s: impl AsRef<str>) -> Result<Self, CorrelationIdError> {
let s = s.as_ref();
if s.is_empty() {
return Err(CorrelationIdError::Empty);
}
if s.len() > 255 {
return Err(CorrelationIdError::TooLong);
}
if !s.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
return Err(CorrelationIdError::InvalidChars);
}
Ok(Self(s.to_owned()))
}
#[must_use]
pub fn new_uuid() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
#[must_use]
pub fn header_name(&self) -> &'static str {
"X-Correlation-Id"
}
}
#[cfg(feature = "std")]
impl crate::header_id::HeaderId for CorrelationId {
const HEADER_NAME: &'static str = "X-Correlation-Id";
fn as_str(&self) -> std::borrow::Cow<'_, str> {
std::borrow::Cow::Borrowed(&self.0)
}
}
#[cfg(all(not(feature = "std"), feature = "alloc"))]
impl crate::header_id::HeaderId for CorrelationId {
const HEADER_NAME: &'static str = "X-Correlation-Id";
fn as_str(&self) -> alloc::borrow::Cow<'_, str> {
alloc::borrow::Cow::Borrowed(&self.0)
}
}
impl Deref for CorrelationId {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for CorrelationId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for CorrelationId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for CorrelationId {
type Err = CorrelationIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl TryFrom<String> for CorrelationId {
type Error = CorrelationIdError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl TryFrom<&str> for CorrelationId {
type Error = CorrelationIdError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::new(s)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for CorrelationId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::new(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_id_is_accepted() {
assert!(CorrelationId::new("flow-abc").is_ok());
assert!(CorrelationId::new("x").is_ok());
assert!(CorrelationId::new("abc 123").is_ok()); }
#[test]
fn empty_is_rejected() {
assert_eq!(CorrelationId::new(""), Err(CorrelationIdError::Empty));
}
#[test]
fn too_long_is_rejected() {
let s: String = "a".repeat(256);
assert_eq!(CorrelationId::new(&s), Err(CorrelationIdError::TooLong));
}
#[test]
fn exactly_255_chars_is_accepted() {
let s: String = "a".repeat(255);
assert!(CorrelationId::new(&s).is_ok());
}
#[test]
fn control_char_is_rejected() {
assert_eq!(
CorrelationId::new("ab\x00c"),
Err(CorrelationIdError::InvalidChars)
);
}
#[test]
fn non_ascii_is_rejected() {
assert_eq!(
CorrelationId::new("héllo"),
Err(CorrelationIdError::InvalidChars)
);
}
#[test]
fn new_uuid_produces_valid_id() {
let id = CorrelationId::new_uuid();
assert_eq!(id.as_str().len(), 36);
assert!(CorrelationId::new(id.as_str()).is_ok());
}
#[test]
fn header_name() {
let id = CorrelationId::new("x").unwrap();
assert_eq!(id.header_name(), "X-Correlation-Id");
}
#[test]
fn display() {
let id = CorrelationId::new("corr-01").unwrap();
assert_eq!(format!("{id}"), "corr-01");
}
#[test]
fn deref_to_str() {
let id = CorrelationId::new("abc").unwrap();
let s: &str = &id;
assert_eq!(s, "abc");
}
#[test]
fn from_str() {
let id: CorrelationId = "corr-abc".parse().unwrap();
assert_eq!(id.as_str(), "corr-abc");
}
#[test]
fn try_from_str() {
assert!(CorrelationId::try_from("valid").is_ok());
assert!(CorrelationId::try_from("").is_err());
}
#[test]
fn try_from_string() {
assert!(CorrelationId::try_from("valid".to_owned()).is_ok());
}
#[test]
fn into_string() {
let id = CorrelationId::new("abc").unwrap();
assert_eq!(id.into_string(), "abc");
}
#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip() {
let id = CorrelationId::new("corr-xyz-789").unwrap();
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, r#""corr-xyz-789""#);
let back: CorrelationId = serde_json::from_str(&json).unwrap();
assert_eq!(back, id);
}
#[cfg(feature = "serde")]
#[test]
fn serde_deserialize_invalid_rejects() {
let result: Result<CorrelationId, _> = serde_json::from_str(r#""""#);
assert!(result.is_err());
}
#[test]
fn as_ref_str() {
let id = CorrelationId::new("corr-ref").unwrap();
let s: &str = id.as_ref();
assert_eq!(s, "corr-ref");
}
#[test]
fn error_display_all_variants() {
assert!(!CorrelationIdError::Empty.to_string().is_empty());
assert!(!CorrelationIdError::TooLong.to_string().is_empty());
assert!(!CorrelationIdError::InvalidChars.to_string().is_empty());
}
}