use crate::error::OcelError;
use crate::types::{Blake3Hash, ObjectRef, OperationEvent};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SeqCounter {
value: u64,
}
impl SeqCounter {
pub fn new() -> Self {
SeqCounter { value: 0 }
}
pub fn starting_at(value: u64) -> Self {
SeqCounter { value }
}
pub fn next_seq(&mut self) -> u64 {
let current = self.value;
self.value += 1;
current
}
pub fn peek(&self) -> u64 {
self.value
}
}
#[macro_export]
macro_rules! emit {
($evt_type:expr, $objects:expr, $payload:expr) => {{
let mut counter = $crate::ocel::SeqCounter::new();
let parsed_objects: Vec<$crate::types::ObjectRef> = $objects
.iter()
.map(|s| $crate::ocel::parse_object_ref(s).expect("valid object ref"))
.collect();
$crate::ocel::build_event($evt_type, parsed_objects, $payload, &mut counter)
}};
}
pub fn object_ref(id: impl Into<String>, obj_type: impl Into<String>) -> ObjectRef {
ObjectRef {
id: id.into(),
obj_type: obj_type.into(),
qualifier: None,
}
}
pub fn qualified_object_ref(
id: impl Into<String>,
obj_type: impl Into<String>,
qualifier: impl Into<String>,
) -> ObjectRef {
ObjectRef {
id: id.into(),
obj_type: obj_type.into(),
qualifier: Some(qualifier.into()),
}
}
pub fn parse_object_ref(spec: &str) -> std::result::Result<ObjectRef, OcelError> {
let mut parts = spec.splitn(3, ':');
let id = parts.next().unwrap_or("");
let obj_type = parts.next();
let qualifier = parts.next();
match obj_type {
Some(ty) if !id.is_empty() && !ty.is_empty() => Ok(ObjectRef {
id: id.to_string(),
obj_type: ty.to_string(),
qualifier: qualifier.filter(|q| !q.is_empty()).map(|q| q.to_string()),
}),
_ => Err(OcelError::MalformedObjectRef(spec.to_string())),
}
}
pub fn build_event(
event_type: impl Into<String>,
objects: Vec<ObjectRef>,
payload: &[u8],
counter: &mut SeqCounter,
) -> std::result::Result<OperationEvent, OcelError> {
let event_type = event_type.into();
let seq = counter.next_seq();
let event = OperationEvent {
id: format!("evt-{seq}"),
seq,
event_type,
objects,
payload_commitment: Blake3Hash::from_bytes(payload),
};
validate_event(&event)?;
Ok(event)
}
pub fn validate_event(event: &OperationEvent) -> std::result::Result<(), OcelError> {
if event.id.trim().is_empty() {
return Err(OcelError::EmptyEventId);
}
if event.event_type.trim().is_empty() {
return Err(OcelError::EmptyEventType);
}
for (i, obj) in event.objects.iter().enumerate() {
if obj.id.trim().is_empty() {
return Err(OcelError::EmptyObjectId(i));
}
if obj.obj_type.trim().is_empty() {
return Err(OcelError::EmptyObjectType(i));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_event_yields_stable_commitment_for_fixed_bytes() {
let mut c1 = SeqCounter::new();
let mut c2 = SeqCounter::new();
let e1 = build_event("write", vec![object_ref("o1", "file")], b"hello", &mut c1).unwrap();
let e2 = build_event("write", vec![object_ref("o1", "file")], b"hello", &mut c2).unwrap();
assert_eq!(e1.payload_commitment, e2.payload_commitment);
assert_eq!(e1.payload_commitment, Blake3Hash::from_bytes(b"hello"));
assert_ne!(e1.payload_commitment, Blake3Hash::from_bytes(b"world"));
}
#[test]
fn seq_counter_is_monotonic_and_deterministic() {
let mut c = SeqCounter::new();
let a = build_event("a", vec![], b"", &mut c).unwrap();
let b = build_event("b", vec![], b"", &mut c).unwrap();
assert_eq!(a.seq, 0);
assert_eq!(b.seq, 1);
assert_eq!(a.id, "evt-0");
assert_eq!(b.id, "evt-1");
assert_eq!(c.peek(), 2);
}
#[test]
fn validation_catches_empty_event_type() {
let mut c = SeqCounter::new();
let err = build_event("", vec![], b"x", &mut c).unwrap_err();
assert_eq!(err, OcelError::EmptyEventType);
let err2 = build_event(" ", vec![], b"x", &mut SeqCounter::new()).unwrap_err();
assert_eq!(err2, OcelError::EmptyEventType);
}
#[test]
fn validation_catches_empty_object_fields() {
let mut c = SeqCounter::new();
let err = build_event("op", vec![object_ref("", "file")], b"x", &mut c).unwrap_err();
assert_eq!(err, OcelError::EmptyObjectId(0));
let err2 = build_event(
"op",
vec![object_ref("o1", "")],
b"x",
&mut SeqCounter::new(),
)
.unwrap_err();
assert_eq!(err2, OcelError::EmptyObjectType(0));
}
#[test]
fn parse_object_ref_handles_qualifier_and_errors() {
assert_eq!(
parse_object_ref("o1:file").unwrap(),
object_ref("o1", "file")
);
assert_eq!(
parse_object_ref("o1:file:input").unwrap(),
qualified_object_ref("o1", "file", "input")
);
assert_eq!(
parse_object_ref("nope").unwrap_err(),
OcelError::MalformedObjectRef("nope".to_string())
);
assert_eq!(
parse_object_ref(":file").unwrap_err(),
OcelError::MalformedObjectRef(":file".to_string())
);
}
}