use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use serde_json::{json, Map, Value};
use crate::emit::Emitter;
use crate::envelope::{Envelope, Kind};
use crate::filter::{Filter, Matcher};
use crate::read::{Merge, Reader, Reading};
use crate::redact::Redactor;
use crate::schema::{CheckError, Message, Registry, SchemaId};
use crate::vocabulary::Vocabulary;
#[derive(Debug, Clone)]
pub struct Fixture<V: Vocabulary> {
pub namespace: &'static str,
pub sources: [V::Source; 2],
pub labels: V::Labels,
pub other_labels: V::Labels,
pub matching: V::Fields,
pub dimensions: V::Dimensions,
pub unknown_key: &'static str,
}
#[derive(Debug, Clone)]
pub struct Sample<M: Message> {
pub conforming: M,
pub violating: (Value, &'static str),
}
#[derive(Clone, Default)]
struct Captured(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for Captured {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0
.lock()
.expect("the captured sink")
.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn payload(n: u64) -> Map<String, Value> {
let mut payload = Map::new();
payload.insert("n".to_owned(), json!(n));
payload
}
static SCRATCH: AtomicU64 = AtomicU64::new(0);
struct Scratch(std::path::PathBuf);
impl Scratch {
fn new() -> Self {
let dir = std::env::temp_dir().join(format!(
"onemessagebus-conformance-{}-{}",
std::process::id(),
SCRATCH.fetch_add(1, Ordering::SeqCst)
));
std::fs::create_dir_all(&dir).expect("a scratch directory");
Self(dir)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
pub fn drive<V: Vocabulary, M: Message + PartialEq + std::fmt::Debug>(
fixture: &Fixture<V>,
sample: &Sample<M>,
) {
envelopes_round_trip(fixture);
filters_read_the_reserved_keys(fixture);
streams_emit_read_and_merge(fixture);
messages_register_and_check(fixture, sample);
}
fn envelope<V: Vocabulary>(fixture: &Fixture<V>, which: usize, seq: u64) -> Envelope<V> {
Envelope {
v: V::write_version(&fixture.sources[which]),
ts: format!("2026-09-13T00:00:0{seq}.000Z"),
stream: format!("stream-{which}"),
seq,
source: fixture.sources[which].clone(),
kind: Kind::from("thing-happened"),
dimensions: fixture.dimensions.clone(),
labels: fixture.labels.clone(),
payload: payload(seq),
artifacts: Vec::new(),
}
}
pub fn envelopes_round_trip<V: Vocabulary>(fixture: &Fixture<V>) {
let original = envelope(fixture, 0, 1);
let line = serde_json::to_string(&original).expect("an envelope serializes");
let read: Envelope<V> = serde_json::from_str(&line).expect("an envelope reads back");
assert_eq!(read, original, "an envelope did not round-trip");
assert_eq!(
serde_json::to_string(&read).expect("serializes"),
line,
"re-serializing changed the bytes"
);
let mut document: Value = serde_json::from_str(&line).expect("JSON");
document[fixture.unknown_key] = json!("stray");
let refusal = serde_json::from_value::<Envelope<V>>(document)
.expect_err("an unknown top-level key must be refused");
assert!(
refusal.to_string().contains(fixture.unknown_key),
"the refusal does not name the key: {refusal}"
);
let mut document: Value = serde_json::from_str(&line).expect("JSON");
document["seq"] = json!(1.5);
serde_json::from_value::<Envelope<V>>(document).expect_err("a non-integer seq must be refused");
let mut document: Value = serde_json::from_str(&line).expect("JSON");
document.as_object_mut().expect("an object").remove("ts");
let refusal = serde_json::from_value::<Envelope<V>>(document)
.expect_err("a missing required field must be refused");
assert!(
refusal.to_string().contains("ts"),
"the refusal does not name the field: {refusal}"
);
}
pub fn filters_read_the_reserved_keys<V: Vocabulary>(fixture: &Fixture<V>) {
let stamped = envelope(fixture, 0, 1);
let other = Envelope {
labels: fixture.other_labels.clone(),
..envelope(fixture, 1, 2)
};
let by_label = Filter::<V> {
include: vec![Matcher::new().fields(fixture.matching.clone())],
exclude: Vec::new(),
};
by_label
.validate()
.expect("a matcher naming a reserved key is valid");
assert!(by_label.matches(&stamped), "the reserved key did not match");
assert!(
!by_label.matches(&other),
"a different reserved value matched"
);
let by_source = Filter::<V> {
include: vec![Matcher::new().source(fixture.sources[1].clone())],
exclude: Vec::new(),
};
assert!(!by_source.matches(&stamped));
assert!(by_source.matches(&other));
let excluded = Filter::<V> {
include: vec![Matcher::new().kind("thing-*")],
exclude: vec![Matcher::new().fields(fixture.matching.clone())],
};
assert!(!excluded.matches(&stamped), "exclude did not win");
assert!(excluded.matches(&other));
assert!(Filter::<V>::everything().matches(&stamped));
let empty = Filter::<V> {
include: vec![Matcher::new()],
exclude: Vec::new(),
};
let refusal = empty
.validate()
.expect_err("a field-less matcher is refused");
assert!(refusal.starts_with("include[0]"), "{refusal}");
let spec = serde_json::to_string(&by_label).expect("a filter serializes");
let parsed = Filter::<V>::parse(&spec).expect("a filter reads back");
assert_eq!(parsed, by_label);
}
pub fn streams_emit_read_and_merge<V: Vocabulary>(fixture: &Fixture<V>) {
let scratch = Scratch::new();
let mut paths = Vec::new();
for which in 0..2 {
let captured = Captured::default();
let emitter = Emitter::<V>::new(
format!("stream-{which}"),
fixture.sources[which].clone(),
Box::new(captured.clone()),
)
.with_redactor(Redactor::new())
.with_labels(fixture.labels.clone())
.with_dimensions(fixture.dimensions.clone());
for n in 1..=3 {
let written = emitter.emit("thing-happened", payload(n));
assert_eq!(written.seq, n, "seq is not monotonic from 1");
assert_eq!(written.v, V::write_version(&fixture.sources[which]));
assert_eq!(written.labels, fixture.labels);
}
let path = scratch.0.join(format!("stream-{which}.ndjson"));
std::fs::write(&path, captured.0.lock().expect("the sink").as_slice())
.expect("the stream is written");
paths.push(path);
}
let readings: Vec<Reading<V>> = Reader::open(&paths[0]).expect("opens").collect();
assert_eq!(readings.len(), 3);
let mut resume = 0;
for (index, reading) in readings.iter().enumerate() {
let Reading::Record(record) = reading else {
panic!("a whole stream read as {reading:?}");
};
assert_eq!(record.envelope.seq, index as u64 + 1);
assert!(record.position > resume);
resume = record.position;
}
let after_first = match &readings[0] {
Reading::Record(record) => record.position,
other => panic!("{other:?}"),
};
let rest: Vec<Reading<V>> = Reader::open_at(&paths[0], after_first)
.expect("resumes")
.collect();
assert_eq!(rest.len(), 2, "a resumed reader re-read the first record");
let merged = Merge::<V>::open(&paths).expect("merges");
assert!(merged.torn().is_empty() && merged.refused().is_empty());
let keys: Vec<(String, String, u64)> = merged
.records()
.iter()
.map(|envelope| {
let (ts, stream, seq) = envelope.order_key();
(ts.to_owned(), stream.to_owned(), seq)
})
.collect();
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(keys, sorted, "the merge is not in (ts, stream, seq) order");
assert_eq!(keys.len(), 6);
}
pub fn messages_register_and_check<V: Vocabulary, M: Message + PartialEq + std::fmt::Debug>(
fixture: &Fixture<V>,
sample: &Sample<M>,
) {
assert_eq!(
M::SCHEMA.namespace(),
fixture.namespace,
"the sample message is not in the vocabulary's namespace"
);
let mut registry = Registry::new();
registry.register::<M>().expect("registers");
assert_eq!(
registry.read_set(&M::SCHEMA.family()),
vec![M::SCHEMA.version()]
);
assert_eq!(
registry.writes(&M::SCHEMA.family()),
Some(M::SCHEMA.version())
);
registry
.register::<M>()
.expect("registering the same document twice is fine");
let conforming = serde_json::to_value(&sample.conforming).expect("serializes");
registry
.check(&M::SCHEMA, &conforming)
.expect("a conforming instance passes");
let read: M = serde_json::from_value(conforming).expect("reads back");
assert_eq!(read, sample.conforming);
let (document, pointer) = &sample.violating;
match registry.check(&M::SCHEMA, document) {
Err(CheckError::Violation(violation)) => {
assert_eq!(violation.id, M::SCHEMA);
assert_eq!(violation.pointer, *pointer, "{violation}");
}
other => panic!("a violating document was not refused as one: {other:?}"),
}
let elsewhere: SchemaId = format!("{}.{}@{}", fixture.namespace, "nothing", 1)
.parse()
.expect("an id");
match registry.check(&elsewhere, &json!({})) {
Err(CheckError::Registry(refusal)) => {
assert!(refusal.to_string().contains(&elsewhere.to_string()));
}
other => panic!("an unregistered id was not refused: {other:?}"),
}
}