#[derive(Debug, Clone, PartialEq)]
pub struct Tick {
pub id: String, pub parent_id: String, pub observe: String, pub decision: String, pub grounds: Vec<Ground>, pub status: String, pub held_since: String, pub blame: String, pub authority: Option<String>, pub jurisdiction: Option<String>, pub source_ref: Option<Value>, pub provenance: Option<String>, }
#[derive(Debug, Clone, PartialEq)]
pub struct Ground {
pub claim: String,
pub supports: String, pub check: Option<Check>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Check {
Person {
reference: String,
}, Test {
reference: String, verified_at_sha: String, counter_test: Option<String>, liveness: Liveness,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct Liveness {
pub platforms: Vec<String>,
pub triggered_by: Vec<String>,
pub surfaces: Vec<String>,
}
use crate::canonical::hashed_value;
use serde_json::{Map, Value};
pub fn full_value(t: &Tick) -> Value {
let mut v = hashed_value(t);
if let Value::Object(map) = &mut v {
map.insert("id".into(), Value::String(t.id.clone()));
map.insert("status".into(), Value::String(t.status.clone()));
map.insert("held_since".into(), Value::String(t.held_since.clone()));
map.insert("blame".into(), Value::String(t.blame.clone()));
if let Some(a) = &t.authority {
map.insert("authority".into(), Value::String(a.clone()));
}
if let Some(j) = &t.jurisdiction {
map.insert("jurisdiction".into(), Value::String(j.clone()));
}
if let Some(r) = &t.source_ref {
map.insert("source_ref".into(), r.clone());
}
if let Some(p) = &t.provenance {
map.insert("provenance".into(), Value::String(p.clone()));
}
}
v
}
pub(crate) fn validate_jurisdiction(val: &str) -> Result<(), String> {
if matches!(val, "A" | "B" | "C" | "D") {
Ok(())
} else {
Err(format!(
"jurisdiction must be one of A, B, C, D (got {val:?})"
))
}
}
pub(crate) fn validate_provenance(val: &str) -> Result<(), String> {
if matches!(val, "imported" | "agent-proposed" | "human-now") {
Ok(())
} else {
Err(format!(
"provenance must be one of imported, agent-proposed, human-now (got {val:?})"
))
}
}
pub(crate) fn validate_source_ref(v: &Value) -> Result<(), String> {
match v {
Value::String(s) if !s.is_empty() => Ok(()),
Value::String(_) => Err("source_ref string is empty".into()),
Value::Object(m) if !m.is_empty() => Ok(()),
Value::Object(_) => Err("source_ref object is empty".into()),
_ => Err("source_ref must be a non-empty string or object".into()),
}
}
pub(crate) fn source_ref_key(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
pub(crate) fn detect_only_carries_test(jurisdiction: Option<&str>, grounds: &[Ground]) -> bool {
matches!(jurisdiction, Some("C") | Some("D"))
&& grounds
.iter()
.any(|g| matches!(g.check, Some(Check::Test { .. })))
}
pub(crate) fn only_keys(
obj: &Map<String, Value>,
allowed: &[&str],
what: &str,
) -> Result<(), String> {
for k in obj.keys() {
if !allowed.contains(&k.as_str()) {
return Err(format!("{what}: field outside closed schema: {k}"));
}
}
Ok(())
}
pub(crate) fn req_str(obj: &Map<String, Value>, k: &str) -> Result<String, String> {
obj.get(k)
.and_then(|x| x.as_str())
.map(|s| s.to_string())
.ok_or(format!("missing or non-string field: {k}"))
}
pub(crate) fn is_40_lower_hex(s: &str) -> bool {
s.len() == 40
&& s.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
fn nonempty_str_set(obj: &Map<String, Value>, k: &str) -> Result<Vec<String>, String> {
let a = obj
.get(k)
.and_then(|x| x.as_array())
.ok_or(format!("liveness.{k} missing/not array"))?;
let mut out = Vec::new();
for e in a {
let s = e
.as_str()
.ok_or(format!("liveness.{k} element not a string"))?;
if s.is_empty() {
return Err(format!("liveness.{k} has an empty element"));
}
out.push(s.to_string());
}
if out.is_empty() {
return Err(format!("liveness.{k} must be non-empty"));
}
Ok(out)
}
fn check_from_value(v: &Value) -> Result<Check, String> {
let obj = v.as_object().ok_or("check is not an object")?;
match obj.get("by").and_then(|x| x.as_str()) {
Some("person") => {
only_keys(obj, &["by", "ref"], "person check")?;
let reference = req_str(obj, "ref")?;
if reference.is_empty() {
return Err("person check ref is empty".into());
}
Ok(Check::Person { reference })
}
Some("test") => {
only_keys(
obj,
&["by", "ref", "verified_at_sha", "counter_test", "liveness"],
"test check",
)?;
let reference = req_str(obj, "ref")?;
if reference.is_empty() {
return Err("test check ref is empty".into());
}
let verified_at_sha = req_str(obj, "verified_at_sha")?;
if !is_40_lower_hex(&verified_at_sha) {
return Err(format!(
"verified_at_sha must be 40 lowercase hex: {verified_at_sha}"
));
}
let counter_test = match obj.get("counter_test") {
None => None,
Some(cv) => {
let s = cv.as_str().ok_or("counter_test present but not a string")?;
if s.is_empty() {
return Err("counter_test present but empty".into());
}
Some(s.to_string())
}
};
let lv = obj
.get("liveness")
.and_then(|x| x.as_object())
.ok_or("liveness missing/not object")?;
only_keys(lv, &["platforms", "triggered_by", "surfaces"], "liveness")?;
let liveness = Liveness {
platforms: nonempty_str_set(lv, "platforms")?,
triggered_by: nonempty_str_set(lv, "triggered_by")?,
surfaces: nonempty_str_set(lv, "surfaces")?,
};
Ok(Check::Test {
reference,
verified_at_sha,
counter_test,
liveness,
})
}
other => Err(format!(
"check.by must be \"test\" or \"person\", got {other:?}"
)),
}
}
pub(crate) fn ground_from_value(v: &Value) -> Result<Ground, String> {
let obj = v.as_object().ok_or("ground is not an object")?;
only_keys(obj, &["claim", "supports", "check"], "ground")?;
let claim = req_str(obj, "claim")?;
if claim.is_empty() {
return Err("ground claim is empty".into());
}
let supports = req_str(obj, "supports")?;
let ok_supports = supports == "chosen"
|| (supports.starts_with("rejected:") && supports.len() > "rejected:".len());
if !ok_supports {
return Err(format!("invalid supports: {supports}"));
}
let check = match obj.get("check") {
None => None,
Some(cv) => Some(check_from_value(cv)?),
};
Ok(Ground {
claim,
supports,
check,
})
}
pub(crate) const HASHED_TOP_LEVEL_KEYS: &[&str] = &[
"id",
"parent_id",
"observe",
"decision",
"grounds",
"status",
"held_since",
"blame",
];
pub(crate) const KNOWN_NON_HASHED_KEYS: &[&str] =
&["authority", "jurisdiction", "source_ref", "provenance"];
pub(crate) fn unknown_top_level_keys(obj: &Map<String, Value>) -> Vec<String> {
obj.keys()
.filter(|k| {
!HASHED_TOP_LEVEL_KEYS.contains(&k.as_str())
&& !KNOWN_NON_HASHED_KEYS.contains(&k.as_str())
})
.cloned()
.collect()
}
pub fn from_value(v: &Value) -> Result<Tick, String> {
let obj = v.as_object().ok_or("tick is not an object")?;
let grounds_v = obj
.get("grounds")
.and_then(|x| x.as_array())
.ok_or("grounds missing/not array")?;
let mut grounds = Vec::new();
for gv in grounds_v {
grounds.push(ground_from_value(gv)?);
}
Ok(Tick {
id: req_str(obj, "id")?,
parent_id: req_str(obj, "parent_id")?,
observe: req_str(obj, "observe")?,
decision: req_str(obj, "decision")?,
grounds,
status: req_str(obj, "status")?,
held_since: req_str(obj, "held_since")?,
blame: req_str(obj, "blame")?,
authority: obj
.get("authority")
.and_then(|x| x.as_str())
.map(String::from),
jurisdiction: match obj.get("jurisdiction").and_then(|x| x.as_str()) {
None => None,
Some(j) => {
validate_jurisdiction(j)?; Some(j.to_string())
}
},
source_ref: match obj.get("source_ref") {
None => None,
Some(rv) => {
validate_source_ref(rv)?; Some(rv.clone())
}
},
provenance: match obj.get("provenance").and_then(|x| x.as_str()) {
None => None,
Some(p) => {
validate_provenance(p)?; Some(p.to_string())
}
},
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn genesis_full() -> serde_json::Value {
json!({
"id": "e2b337f53a1f", "parent_id": "",
"observe": "o", "decision": "d",
"grounds": [{ "claim": "c", "supports": "chosen",
"check": { "by": "person", "ref": "Q3 review" } }],
"status": "live", "held_since": "", "blame": "Wang Yu"
})
}
#[test]
fn from_value_should_round_trip_the_tick_when_it_is_well_formed() {
let v = genesis_full();
let t = from_value(&v).expect("valid");
assert_eq!(t.decision, "d");
assert_eq!(t.grounds.len(), 1);
assert!(matches!(t.grounds[0].check, Some(Check::Person { .. })));
}
#[test]
fn from_value_should_round_trip_an_authority_tag_when_present() {
let mut v = genesis_full();
v.as_object_mut()
.unwrap()
.insert("authority".into(), json!("user-ruled"));
let t = from_value(&v).expect("valid");
assert_eq!(t.authority.as_deref(), Some("user-ruled"));
}
#[test]
fn from_value_should_default_authority_to_none_when_absent() {
let v = genesis_full();
let t = from_value(&v).expect("valid");
assert_eq!(t.authority, None);
}
#[test]
fn from_value_should_reject_the_tick_when_a_hashed_identity_field_is_missing() {
let mut v = genesis_full();
v.as_object_mut().unwrap().remove("decision");
let result = from_value(&v);
assert!(result.is_err());
}
#[test]
fn from_value_should_reject_the_check_when_it_carries_both_test_and_person_shape() {
let mut v = genesis_full();
v["grounds"][0]["check"] = json!({ "by": "person", "ref": "x", "liveness": {} });
let result = from_value(&v);
assert!(result.is_err());
}
#[test]
fn from_value_should_reject_the_test_check_when_its_sha_is_not_40_hex() {
let mut v = genesis_full();
v["grounds"][0]["check"] = json!({
"by": "test", "ref": "r", "verified_at_sha": "ABC", "counter_test": "ct",
"liveness": { "platforms": ["p"], "triggered_by": ["t"], "surfaces": ["s"] }
});
let result = from_value(&v);
assert!(result.is_err());
}
#[test]
fn from_value_should_reject_an_empty_counter_test_when_present() {
let mut v = genesis_full();
v["grounds"][0]["check"] = json!({
"by": "test", "ref": "r", "verified_at_sha": "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901", "counter_test": "",
"liveness": { "platforms": ["p"], "triggered_by": ["t"], "surfaces": ["s"] }
});
let result = from_value(&v);
assert!(result.is_err());
}
#[test]
fn from_value_should_round_trip_a_harvested_test_check_when_counter_test_is_absent() {
let mut v = genesis_full();
v["grounds"][0]["check"] = json!({
"by": "test", "ref": "r", "verified_at_sha": "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
"liveness": { "platforms": ["p"], "triggered_by": ["t"], "surfaces": ["s"] }
});
let t = from_value(&v).expect("valid");
assert!(matches!(
&t.grounds[0].check,
Some(Check::Test {
counter_test: None,
..
})
));
}
#[test]
fn from_value_should_round_trip_a_jurisdiction_tag_when_present() {
let mut v = genesis_full();
v.as_object_mut()
.unwrap()
.insert("jurisdiction".into(), json!("C"));
let t = from_value(&v).expect("valid");
assert_eq!(t.jurisdiction.as_deref(), Some("C"));
}
#[test]
fn from_value_should_default_jurisdiction_to_none_when_absent() {
let v = genesis_full();
let t = from_value(&v).expect("valid");
assert_eq!(t.jurisdiction, None);
}
#[test]
fn from_value_should_round_trip_a_string_source_ref_when_present() {
let mut v = genesis_full();
v.as_object_mut()
.unwrap()
.insert("source_ref".into(), json!("R2289"));
let t = from_value(&v).expect("valid");
assert_eq!(t.source_ref, Some(json!("R2289")));
}
#[test]
fn from_value_should_round_trip_a_structured_source_ref_when_given_an_object() {
let mut v = genesis_full();
v.as_object_mut().unwrap().insert(
"source_ref".into(),
json!({"round": "R2289", "ticket": "#1194"}),
);
let t = from_value(&v).expect("valid");
assert_eq!(
t.source_ref,
Some(json!({"round": "R2289", "ticket": "#1194"}))
);
}
#[test]
fn from_value_should_default_source_ref_to_none_when_absent() {
let v = genesis_full();
let t = from_value(&v).expect("valid");
assert_eq!(t.source_ref, None);
}
#[test]
fn from_value_should_reject_an_empty_source_ref_when_present() {
let mut v = genesis_full();
v.as_object_mut()
.unwrap()
.insert("source_ref".into(), json!(""));
let result = from_value(&v);
assert!(result.is_err());
}
#[test]
fn from_value_should_reject_a_non_string_non_object_source_ref() {
let mut v = genesis_full();
v.as_object_mut()
.unwrap()
.insert("source_ref".into(), json!(42));
let result = from_value(&v);
assert!(result.is_err());
}
#[test]
fn from_value_should_round_trip_provenance_when_present() {
let mut v = genesis_full();
v.as_object_mut()
.unwrap()
.insert("provenance".into(), json!("imported"));
let t = from_value(&v).expect("valid");
assert_eq!(t.provenance.as_deref(), Some("imported"));
}
#[test]
fn from_value_should_default_provenance_to_none_when_absent() {
let v = genesis_full();
let t = from_value(&v).expect("valid");
assert_eq!(t.provenance, None);
}
#[test]
fn from_value_should_reject_an_out_of_vocab_provenance() {
let mut v = genesis_full();
v.as_object_mut()
.unwrap()
.insert("provenance".into(), json!("self-asserted"));
let result = from_value(&v);
assert!(result.is_err());
}
#[test]
fn from_value_should_reject_an_out_of_vocab_jurisdiction() {
let mut v = genesis_full();
v.as_object_mut()
.unwrap()
.insert("jurisdiction".into(), json!("Z"));
let result = from_value(&v);
assert!(result.is_err());
}
#[test]
fn from_value_should_reject_the_test_check_when_its_ref_is_empty() {
let mut v = genesis_full();
v["grounds"][0]["check"] = json!({
"by": "test", "ref": "", "verified_at_sha": "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901", "counter_test": "ct",
"liveness": { "platforms": ["p"], "triggered_by": ["t"], "surfaces": ["s"] }
});
let result = from_value(&v);
assert!(result.is_err());
}
#[test]
fn from_value_should_tolerate_an_unknown_non_hashed_key_when_reading() {
let mut v = genesis_full();
v.as_object_mut()
.unwrap()
.insert("future_field".into(), json!("x"));
let t = from_value(&v).expect("an unknown top-level key is tolerated (parsed-through)");
assert_eq!(t.decision, "d");
assert_eq!(t.observe, "o");
assert_eq!(t.grounds.len(), 1);
}
#[test]
fn from_value_should_still_reject_an_unknown_key_inside_the_hashed_payload() {
let mut v = genesis_full();
v["grounds"][0]
.as_object_mut()
.unwrap()
.insert("future_field".into(), json!("x"));
let result = from_value(&v);
assert!(result.is_err());
}
}