use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::digest::{Digest, DigestParseError};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct StepId(Digest);
impl StepId {
pub const fn new(digest: Digest) -> Self {
Self(digest)
}
pub const fn digest(&self) -> &Digest {
&self.0
}
}
impl fmt::Display for StepId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "step:{}", self.0)
}
}
impl fmt::Debug for StepId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "StepId({}…)", self.0.short(12))
}
}
impl FromStr for StepId {
type Err = DigestParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(Digest::from_str(
s.strip_prefix("step:").unwrap_or(s),
)?))
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RunId(Ulid);
impl RunId {
pub fn new() -> Self {
Self(Ulid::new())
}
pub const fn from_ulid(ulid: Ulid) -> Self {
Self(ulid)
}
pub const fn ulid(&self) -> Ulid {
self.0
}
}
impl Default for RunId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for RunId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "run:{}", self.0)
}
}
impl fmt::Debug for RunId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "RunId({})", self.0)
}
}
impl FromStr for RunId {
type Err = ulid::DecodeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(Ulid::from_string(
s.strip_prefix("run:").unwrap_or(s),
)?))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::canonical::Canonical;
#[test]
fn step_id_is_content_addressed_and_stable() {
let spec = ("reasoner-a", vec!["tool-x"], "checker-y");
let a = StepId::new(spec.digest().unwrap());
let b = StepId::new(spec.digest().unwrap());
assert_eq!(a, b, "the same step specification must yield the same id");
}
#[test]
fn different_specs_give_different_step_ids() {
let a = StepId::new(("reasoner-a",).digest().unwrap());
let b = StepId::new(("reasoner-b",).digest().unwrap());
assert_ne!(a, b);
}
#[test]
fn run_ids_are_unique() {
let first = RunId::new();
let second = RunId::new();
assert_ne!(first, second);
}
#[test]
fn run_ids_sort_chronologically() {
use std::time::{Duration, SystemTime};
let t0 = SystemTime::now();
let earlier = RunId::from_ulid(Ulid::from_datetime(t0));
let later = RunId::from_ulid(Ulid::from_datetime(t0 + Duration::from_millis(1)));
assert!(
earlier < later,
"a later run must sort after an earlier one"
);
}
#[test]
fn run_ids_never_go_backwards_in_time() {
let first = RunId::new();
let second = RunId::new();
assert!(first.ulid().timestamp_ms() <= second.ulid().timestamp_ms());
}
#[test]
fn ids_round_trip_through_strings() {
let step = StepId::new(Digest::of(b"x"));
assert_eq!(StepId::from_str(&step.to_string()).unwrap(), step);
let run = RunId::new();
assert_eq!(RunId::from_str(&run.to_string()).unwrap(), run);
}
#[test]
fn ids_are_prefixed_so_they_cannot_be_confused_in_logs() {
assert!(
StepId::new(Digest::of(b"x"))
.to_string()
.starts_with("step:")
);
assert!(RunId::new().to_string().starts_with("run:"));
}
}