use serde::{Deserialize, Serialize};
use crate::wac::conditions::{ConditionOutcome, RequestContext};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AnchorMode {
#[default]
Epoch,
Inline,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ProvenanceAnchorBody {
#[serde(
rename = "acl:anchorMode",
default,
skip_serializing_if = "Option::is_none"
)]
pub anchor_mode: Option<String>,
#[serde(
rename = "acl:anchorTicker",
default,
skip_serializing_if = "Option::is_none"
)]
pub ticker: Option<String>,
}
impl ProvenanceAnchorBody {
#[must_use]
pub fn mode(&self) -> AnchorMode {
match self.anchor_mode.as_deref().map(str::trim) {
Some(m) if m.eq_ignore_ascii_case("always") || m.eq_ignore_ascii_case("inline")
|| m.eq_ignore_ascii_case("highvalue")
|| m.eq_ignore_ascii_case("high-value") =>
{
AnchorMode::Inline
}
_ => AnchorMode::Epoch,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ProvenanceAnchorEvaluator;
impl ProvenanceAnchorEvaluator {
#[must_use]
pub fn evaluate(
&self,
_body: &ProvenanceAnchorBody,
_ctx: &RequestContext<'_>,
) -> ConditionOutcome {
ConditionOutcome::Satisfied
}
}
#[must_use]
pub fn anchor_mode_of(conditions: &[crate::wac::conditions::Condition]) -> Option<AnchorMode> {
conditions.iter().find_map(|c| match c {
crate::wac::conditions::Condition::ProvenanceAnchor(body) => Some(body.mode()),
_ => None,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx() -> RequestContext<'static> {
RequestContext {
web_id: Some("did:nostr:alice"),
client_id: None,
issuer: None,
payment_balance_sats: None,
}
}
#[test]
fn evaluator_always_satisfied_even_anonymous() {
let anon = RequestContext {
web_id: None,
client_id: None,
issuer: None,
payment_balance_sats: None,
};
assert_eq!(
ProvenanceAnchorEvaluator.evaluate(&ProvenanceAnchorBody::default(), &anon),
ConditionOutcome::Satisfied,
"a provenance marker must never block a write"
);
assert_eq!(
ProvenanceAnchorEvaluator.evaluate(&ProvenanceAnchorBody::default(), &ctx()),
ConditionOutcome::Satisfied
);
}
#[test]
fn mode_defaults_to_epoch() {
assert_eq!(ProvenanceAnchorBody::default().mode(), AnchorMode::Epoch);
let b = ProvenanceAnchorBody {
anchor_mode: Some("epoch".into()),
ticker: None,
};
assert_eq!(b.mode(), AnchorMode::Epoch);
}
#[test]
fn mode_inline_synonyms() {
for s in ["always", "ALWAYS", "inline", "Inline", "highValue", "high-value"] {
let b = ProvenanceAnchorBody {
anchor_mode: Some(s.into()),
ticker: None,
};
assert_eq!(b.mode(), AnchorMode::Inline, "mode {s} ⇒ inline");
}
}
#[test]
fn mode_unknown_falls_back_to_epoch() {
let b = ProvenanceAnchorBody {
anchor_mode: Some("frobnicate".into()),
ticker: None,
};
assert_eq!(b.mode(), AnchorMode::Epoch);
}
#[test]
fn deserialize_from_json_empty_body() {
let json = r#"{"@type":"acl:ProvenanceAnchor"}"#;
let body: ProvenanceAnchorBody = serde_json::from_str(json).unwrap();
assert!(body.anchor_mode.is_none());
assert_eq!(body.mode(), AnchorMode::Epoch);
}
#[test]
fn deserialize_from_json_with_mode_and_ticker() {
let json = r#"{"@type":"acl:ProvenanceAnchor","acl:anchorMode":"always","acl:anchorTicker":"RCPT"}"#;
let body: ProvenanceAnchorBody = serde_json::from_str(json).unwrap();
assert_eq!(body.mode(), AnchorMode::Inline);
assert_eq!(body.ticker.as_deref(), Some("RCPT"));
}
#[test]
fn serialize_roundtrip() {
let body = ProvenanceAnchorBody {
anchor_mode: Some("epoch".into()),
ticker: Some("PROV".into()),
};
let json = serde_json::to_string(&body).unwrap();
assert!(json.contains("acl:anchorMode"));
let back: ProvenanceAnchorBody = serde_json::from_str(&json).unwrap();
assert_eq!(back.mode(), AnchorMode::Epoch);
assert_eq!(back.ticker.as_deref(), Some("PROV"));
}
#[test]
fn anchor_mode_of_finds_marker() {
use crate::wac::conditions::Condition;
let conds = vec![
Condition::Payment(crate::wac::payment::PaymentConditionBody { cost_sats: 10 }),
Condition::ProvenanceAnchor(ProvenanceAnchorBody {
anchor_mode: Some("inline".into()),
ticker: None,
}),
];
assert_eq!(anchor_mode_of(&conds), Some(AnchorMode::Inline));
}
#[test]
fn anchor_mode_of_absent_is_none() {
use crate::wac::conditions::Condition;
let conds = vec![Condition::Payment(
crate::wac::payment::PaymentConditionBody { cost_sats: 10 },
)];
assert_eq!(anchor_mode_of(&conds), None);
}
}