use faucet_core::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Role {
Source,
Sink,
}
impl Role {
pub fn as_str(self) -> &'static str {
match self {
Role::Source => "source",
Role::Sink => "sink",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Trigger {
Always,
Capability(&'static str),
WriteMode(&'static str),
Overrides(&'static str),
BufferedFlush,
Declares(&'static str),
Driver,
}
impl Trigger {
fn to_json(self) -> Value {
let (kind, arg) = match self {
Trigger::Always => ("always", None),
Trigger::Capability(f) => ("capability", Some(f)),
Trigger::WriteMode(m) => ("write_mode", Some(m)),
Trigger::Overrides(m) => ("overrides", Some(m)),
Trigger::BufferedFlush => ("buffered_flush", None),
Trigger::Declares(v) => ("declares", Some(v)),
Trigger::Driver => ("driver", None),
};
match arg {
Some(a) => serde_json::json!({ "kind": kind, "arg": a }),
None => serde_json::json!({ "kind": kind }),
}
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct Requirement {
pub id: &'static str,
pub role: Role,
pub triggers: &'static [Trigger],
pub assertions: &'static [&'static str],
pub summary: &'static str,
}
impl Requirement {
pub fn to_json(&self) -> Value {
serde_json::json!({
"id": self.id,
"role": self.role.as_str(),
"triggers": self.triggers.iter().map(|t| t.to_json()).collect::<Vec<_>>(),
"assertions": self.assertions,
"summary": self.summary,
})
}
}
pub const REQUIREMENTS: &[Requirement] = &[
Requirement {
id: "descriptor",
role: Role::Sink,
triggers: &[Trigger::Always],
assertions: &["assert_descriptor_matches_sink"],
summary: "the static descriptor answers what the built sink declares",
},
Requirement {
id: "idempotent_writes",
role: Role::Sink,
triggers: &[Trigger::Capability("idempotent_writes")],
assertions: &["assert_idempotent_replay", "assert_capabilities_truthful"],
summary: "a committed page's token is durable, so a replay is skipped",
},
Requirement {
id: "keyed_write_modes",
role: Role::Sink,
triggers: &[Trigger::WriteMode("upsert"), Trigger::WriteMode("delete")],
assertions: &["assert_write_modes_truthful"],
summary: "upsert converges by key, delete removes, a missing key fails the row",
},
Requirement {
id: "replay_safe",
role: Role::Sink,
triggers: &[
Trigger::Capability("keyed_upsert"),
Trigger::Overrides("write_batch_is_replay_safe"),
],
assertions: &["assert_replay_safe"],
summary: "a plain write retried after a lost response does not duplicate rows",
},
Requirement {
id: "overwrite",
role: Role::Sink,
triggers: &[Trigger::WriteMode("overwrite")],
assertions: &["assert_overwrite_lifecycle"],
summary: "staged rows stay invisible until commit; abort leaves the destination as it was",
},
Requirement {
id: "schema_evolution",
role: Role::Sink,
triggers: &[Trigger::Capability("schema_evolution")],
assertions: &["assert_schema_evolution_effective"],
summary: "evolve_schema makes the added column appear in current_schema",
},
Requirement {
id: "buffered_flush",
role: Role::Sink,
triggers: &[Trigger::BufferedFlush],
assertions: &["assert_cancellation_flushes"],
summary: "a cancelled run flushes, so buffered output is not lost",
},
Requirement {
id: "partial_outcomes",
role: Role::Sink,
triggers: &[Trigger::Overrides("write_batch_partial")],
assertions: &["assert_partial_outcomes_truthful"],
summary: "one outcome per row, and an Ok row is in the destination",
},
Requirement {
id: "row_accounting",
role: Role::Sink,
triggers: &[Trigger::Overrides("write_batch_reported")],
assertions: &["assert_reported_rows_truthful"],
summary: "the write report covers the page and accepted equals what landed",
},
Requirement {
id: "atomic_batches",
role: Role::Sink,
triggers: &[Trigger::Declares("Atomic")],
assertions: &["assert_batch_atomicity_truthful"],
summary: "a failed write of a batch declared atomic lands no row",
},
Requirement {
id: "call_deadline",
role: Role::Sink,
triggers: &[Trigger::Always],
assertions: &["assert_sink_call_deadline"],
summary: "a write to a hung backend ends at the call deadline and on cancel",
},
Requirement {
id: "driver_declarations",
role: Role::Sink,
triggers: &[Trigger::Driver],
assertions: &["assert_driver_declarations_truthful"],
summary: "an undeclared lifecycle is refused by its hook and a declared one answers",
},
Requirement {
id: "driver_row_paths",
role: Role::Sink,
triggers: &[Trigger::Driver],
assertions: &["assert_driver_row_paths_agree"],
summary: "the plain, reported, per-row and governed writes of a valid page agree",
},
Requirement {
id: "descriptor",
role: Role::Source,
triggers: &[Trigger::Always],
assertions: &["assert_descriptor_matches_source"],
summary: "the static descriptor answers what the built source declares",
},
Requirement {
id: "exactly_once",
role: Role::Source,
triggers: &[Trigger::Capability("exactly_once")],
assertions: &["assert_bookmark_roundtrip"],
summary: "a resumed run picks up after the emitted bookmark",
},
Requirement {
id: "discover",
role: Role::Source,
triggers: &[Trigger::Capability("discover")],
assertions: &["assert_discover_roundtrips"],
summary: "every discovered dataset can be selected and read",
},
Requirement {
id: "call_deadline",
role: Role::Source,
triggers: &[Trigger::Always],
assertions: &["assert_source_call_deadline"],
summary: "a read from a hung backend ends at the call deadline and on cancel",
},
];
pub fn requirements_for(role: Role) -> impl Iterator<Item = &'static Requirement> {
REQUIREMENTS.iter().filter(move |r| r.role == role)
}
pub fn requirements_json() -> Value {
Value::Array(REQUIREMENTS.iter().map(Requirement::to_json).collect())
}
#[cfg(test)]
mod tests {
use super::*;
const LIB: &str = include_str!("lib.rs");
const CHECKS: &str = include_str!("checks.rs");
const CALLS: &str = include_str!("calls.rs");
#[test]
fn every_named_assertion_exists() {
for r in REQUIREMENTS {
for a in r.assertions {
let def = format!("pub async fn {a}");
let def_sync = format!("pub fn {a}");
assert!(
[LIB, CHECKS, CALLS]
.iter()
.any(|src| src.contains(&def) || src.contains(&def_sync)),
"requirement `{}` names `{a}`, which faucet-conformance does not define",
r.id
);
}
}
}
#[test]
fn ids_are_unique_per_role() {
for role in [Role::Source, Role::Sink] {
let mut ids: Vec<&str> = requirements_for(role).map(|r| r.id).collect();
let n = ids.len();
ids.sort_unstable();
ids.dedup();
assert_eq!(
ids.len(),
n,
"duplicate requirement id for {}",
role.as_str()
);
}
}
#[test]
fn capability_triggers_name_real_capability_fields() {
let fields = serde_json::to_value(faucet_core::Capabilities::default()).unwrap();
for r in REQUIREMENTS {
for t in r.triggers {
if let Trigger::Capability(f) = t {
assert!(
fields.get(*f).is_some_and(Value::is_boolean),
"`{}` triggers on `{f}`, which is not a Capabilities flag",
r.id
);
}
}
}
}
#[test]
fn renders_every_trigger_kind() {
let json = requirements_json();
let text = json.to_string();
for kind in [
"always",
"capability",
"write_mode",
"overrides",
"buffered_flush",
"declares",
"driver",
] {
assert!(text.contains(&format!("\"kind\":\"{kind}\"")), "{kind}");
}
assert_eq!(json.as_array().unwrap().len(), REQUIREMENTS.len());
assert_eq!(Role::Source.as_str(), "source");
assert_eq!(Role::Sink.as_str(), "sink");
}
}