use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize};
use crate::error::WyrdError;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(transparent)]
pub struct SessionId(pub uuid::Uuid);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(transparent)]
pub struct RunId(String);
impl RunId {
#[must_use]
pub fn new() -> Self {
Self(crate::ids::uuid7())
}
#[must_use]
pub fn from_string(value: String) -> Self {
Self(value)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn hash_bucket(&self, buckets: u32) -> u32 {
use sha2::{Digest, Sha256};
if buckets == 0 {
return 0;
}
let digest = Sha256::digest(self.0.as_bytes());
let mut prefix = [0_u8; 8];
prefix.copy_from_slice(&digest[..8]);
let value = u64::from_be_bytes(prefix);
u32::try_from(value % u64::from(buckets)).expect("a remainder is below its u32 divisor")
}
}
impl Default for RunId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for RunId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(transparent)]
pub struct RecordId(pub uuid::Uuid);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(transparent)]
pub struct WorkflowUid(pub uuid::Uuid);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "server", schema(value_type = String))]
#[serde(transparent)]
pub struct EntityUid(String);
impl EntityUid {
pub fn new(value: impl Into<String>) -> Result<Self, WyrdError> {
let value = value.into();
if value.is_empty() || value.len() > 512 {
return Err(WyrdError::Validation {
message: format!("entity_uid length must be 1..=512, got {}", value.len()),
details: serde_json::Value::Null,
});
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for EntityUid {
type Err = WyrdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl<'de> Deserialize<'de> for EntityUid {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(serde::de::Error::custom)
}
}
impl schemars::JsonSchema for EntityUid {
fn schema_name() -> String {
"EntityUid".to_string()
}
fn json_schema(_generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec, StringValidation};
SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
string: Some(Box::new(StringValidation {
max_length: Some(512),
min_length: Some(1),
pattern: None,
})),
..Default::default()
}
.into()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "server", schema(value_type = String))]
pub struct TraceId([u8; 16]);
impl TraceId {
pub const ZERO: TraceId = TraceId([0u8; 16]);
pub fn from_hex(value: &str) -> Result<Self, WyrdError> {
if value.len() != 32 {
return Err(WyrdError::Validation {
message: format!("trace_id hex length must be 32, got {}", value.len()),
details: serde_json::Value::Null,
});
}
let mut bytes = [0u8; 16];
for (index, chunk) in value.as_bytes().chunks(2).enumerate() {
let hi = hex_nibble(chunk[0])?;
let lo = hex_nibble(chunk[1])?;
bytes[index] = (hi << 4) | lo;
}
Self::from_bytes(bytes)
}
pub fn from_bytes(bytes: [u8; 16]) -> Result<Self, WyrdError> {
if bytes == [0u8; 16] {
return Err(WyrdError::Validation {
message: "trace_id all-zero is the OTel invalid sentinel".to_string(),
details: serde_json::Value::Null,
});
}
Ok(Self(bytes))
}
#[must_use]
pub fn to_hex(&self) -> String {
let mut out = String::with_capacity(32);
for byte in &self.0 {
out.push(hex_char(byte >> 4));
out.push(hex_char(byte & 0x0f));
}
out
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
}
impl Serialize for TraceId {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_hex())
}
}
impl<'de> Deserialize<'de> for TraceId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Wire {
Hex(String),
Bytes([u8; 16]),
}
match Wire::deserialize(deserializer)? {
Wire::Hex(value) => Self::from_hex(&value).map_err(serde::de::Error::custom),
Wire::Bytes(bytes) => Self::from_bytes(bytes).map_err(serde::de::Error::custom),
}
}
}
impl schemars::JsonSchema for TraceId {
fn schema_name() -> String {
"TraceId".to_string()
}
fn json_schema(_generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec, StringValidation};
SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
string: Some(Box::new(StringValidation {
max_length: Some(32),
min_length: Some(32),
pattern: Some(r"^[0-9a-f]{32}$".to_string()),
})),
..Default::default()
}
.into()
}
}
impl std::fmt::Display for TraceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_hex())
}
}
impl FromStr for TraceId {
type Err = WyrdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_hex(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "server", schema(value_type = String))]
pub struct SpanId([u8; 8]);
impl SpanId {
pub const ZERO: SpanId = SpanId([0u8; 8]);
pub fn from_hex(value: &str) -> Result<Self, WyrdError> {
if value.len() != 16 {
return Err(WyrdError::Validation {
message: format!("span_id hex length must be 16, got {}", value.len()),
details: serde_json::Value::Null,
});
}
let mut bytes = [0u8; 8];
for (index, chunk) in value.as_bytes().chunks(2).enumerate() {
let hi = hex_nibble(chunk[0])?;
let lo = hex_nibble(chunk[1])?;
bytes[index] = (hi << 4) | lo;
}
Self::from_bytes(bytes)
}
pub fn from_bytes(bytes: [u8; 8]) -> Result<Self, WyrdError> {
if bytes == [0u8; 8] {
return Err(WyrdError::Validation {
message: "span_id all-zero is the OTel invalid sentinel".to_string(),
details: serde_json::Value::Null,
});
}
Ok(Self(bytes))
}
#[must_use]
pub fn to_hex(&self) -> String {
let mut out = String::with_capacity(16);
for byte in &self.0 {
out.push(hex_char(byte >> 4));
out.push(hex_char(byte & 0x0f));
}
out
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; 8] {
&self.0
}
}
impl Serialize for SpanId {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_hex())
}
}
impl<'de> Deserialize<'de> for SpanId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Wire {
Hex(String),
Bytes([u8; 8]),
}
match Wire::deserialize(deserializer)? {
Wire::Hex(value) => Self::from_hex(&value).map_err(serde::de::Error::custom),
Wire::Bytes(bytes) => Self::from_bytes(bytes).map_err(serde::de::Error::custom),
}
}
}
impl schemars::JsonSchema for SpanId {
fn schema_name() -> String {
"SpanId".to_string()
}
fn json_schema(_generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec, StringValidation};
SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
string: Some(Box::new(StringValidation {
max_length: Some(16),
min_length: Some(16),
pattern: Some(r"^[0-9a-f]{16}$".to_string()),
})),
..Default::default()
}
.into()
}
}
impl std::fmt::Display for SpanId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_hex())
}
}
impl FromStr for SpanId {
type Err = WyrdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_hex(s)
}
}
fn hex_nibble(byte: u8) -> Result<u8, WyrdError> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
b'A'..=b'F' => Ok(byte - b'A' + 10),
other => Err(WyrdError::Validation {
message: format!("invalid hex character: {:?}", other as char),
details: serde_json::Value::Null,
}),
}
}
fn hex_char(nibble: u8) -> char {
assert!(nibble < 16, "hex_char received >15 nibble; caller bug");
if nibble < 10 {
(b'0' + nibble) as char
} else {
(b'a' + nibble - 10) as char
}
}