macro_rules! string_id {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
serde::Serialize, serde::Deserialize, specta::Type,
)]
#[serde(transparent)]
pub struct $name(pub String);
impl $name {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
};
}
macro_rules! u64_decimal_string {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name(pub u64);
impl $name {
pub const fn new(value: u64) -> Self {
Self(value)
}
pub const fn value(self) -> u64 {
self.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl serde::Serialize for $name {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(&self.0)
}
}
impl<'de> serde::Deserialize<'de> for $name {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let raw = <std::borrow::Cow<'de, str>>::deserialize(d)?;
let canonical = !raw.is_empty()
&& raw.len() <= 20
&& raw.bytes().all(|b| b.is_ascii_digit())
&& (raw.len() == 1 || !raw.starts_with('0'));
if !canonical {
return Err(serde::de::Error::custom(concat!(
stringify!($name),
" must be a canonical decimal u64 string"
)));
}
raw.parse::<u64>().map(Self).map_err(serde::de::Error::custom)
}
}
impl specta::Type for $name {
fn definition(types: &mut specta::Types) -> specta::datatype::DataType {
<String as specta::Type>::definition(types)
}
}
};
}
string_id!(
EventId
);
string_id!(
CommandId
);
string_id!(
ProjectId
);
string_id!(
AggregateId
);
string_id!(TaskId);
string_id!(AttemptId);
string_id!(MessageId);
string_id!(LeaseId);
string_id!(GateId);
string_id!(ReceiptId);
string_id!(WorktreeId);
string_id!(DispatchNodeId);
string_id!(AttentionItemId);
string_id!(AuthorityGrantId);
string_id!(EvidenceId);
string_id!(
IngestedRecordId
);
string_id!(EngineSessionId);
string_id!(
CorrelationId
);
string_id!(
IdempotencyKey
);
string_id!(
EngineId
);
string_id!(
RequestId
);
string_id!(
BlobUploadId
);
u64_decimal_string!(
Seq
);
u64_decimal_string!(
FenceToken
);
u64_decimal_string!(
ByteCount
);
u64_decimal_string!(
WriterEpoch
);
u64_decimal_string!(
EventCount
);
u64_decimal_string!(
CostMicros
);
#[derive(
Debug,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
specta::Type,
)]
#[serde(transparent)]
pub struct Timestamp(pub String);
impl Timestamp {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Timestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seq_round_trips_as_decimal_string() {
let seq = Seq::new(u64::MAX);
let json = serde_json::to_string(&seq).expect("serialize");
assert_eq!(json, "\"18446744073709551615\"");
let back: Seq = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, seq);
}
#[test]
fn seq_rejects_non_canonical_input() {
for bad in [
"\"\"",
"\"01\"",
"\"1e3\"",
"\"-1\"",
"\" 1\"",
"\"18446744073709551616\"",
"7",
] {
assert!(
serde_json::from_str::<Seq>(bad).is_err(),
"accepted non-canonical {bad}"
);
}
let zero: Seq = serde_json::from_str("\"0\"").expect("bare zero is canonical");
assert_eq!(zero, Seq::new(0));
}
}