use serde::Serialize;
use crate::context::ResolverContext;
use crate::manifest::{AttributionManifest, Disposition};
use crate::matcher::{select_variant, Selected};
use crate::time::Timestamp;
pub const DEFAULT_REVALIDATE_MS: u64 = 15 * 60 * 1000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Resolution<'m> {
pub disposition: Disposition,
#[serde(skip)]
pub variant: Option<Selected<'m>>,
pub as_of: Timestamp,
pub revalidate_after: Timestamp,
}
#[must_use]
pub fn resolve<'m>(
manifest: &'m AttributionManifest,
ctx: &ResolverContext,
now: Timestamp,
) -> Resolution<'m> {
let disposition = manifest.disposition(now);
let variant = match disposition {
Disposition::Revoked { .. } => None,
_ => select_variant(&manifest.variants, ctx),
};
let window = variant
.and_then(|s| s.variant.revalidate_after_ms)
.unwrap_or(DEFAULT_REVALIDATE_MS);
Resolution {
disposition,
variant,
as_of: now,
revalidate_after: now.plus_ms(window),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CanonicalUrl, Channel, MintOptions, MintSpec, Sharer};
fn minted() -> AttributionManifest {
let mut entropy = |buf: &mut [u8]| {
buf.fill(11);
Ok(())
};
crate::mint(
MintSpec::new(
CanonicalUrl::new("ws://a/report.md").unwrap(),
Sharer::new("lead").unwrap(),
Channel::subagent_general(),
),
&MintOptions::default(),
&mut entropy,
Timestamp::from_unix_ms(1_000),
)
.unwrap()
}
#[test]
fn g3_resolution_carries_freshness() {
let m = minted();
let now = Timestamp::from_unix_ms(2_000);
let r = resolve(&m, &ResolverContext::anonymous_agent(), now);
assert_eq!(r.as_of, now);
assert_eq!(r.revalidate_after, now.plus_ms(DEFAULT_REVALIDATE_MS));
assert!(
r.variant.is_some(),
"minted manifests always serve (catch-all)"
);
}
#[test]
fn variant_declared_window_overrides_default() {
let mut m = minted();
m.variants[0].revalidate_after_ms = Some(1_000);
let now = Timestamp::from_unix_ms(0);
let r = resolve(&m, &ResolverContext::human(), now);
assert_eq!(r.revalidate_after, Timestamp::from_unix_ms(1_000));
}
#[test]
fn revoked_serves_nothing_superseded_serves_with_pointer() {
let mut m = minted();
let now = Timestamp::from_unix_ms(5_000);
let other = crate::Token::parse("next1").unwrap();
m.superseded_by = Some(other);
let r = resolve(&m, &ResolverContext::human(), now);
assert_eq!(r.disposition, Disposition::Superseded { by: other });
assert!(
r.variant.is_some(),
"superseded still serves; the pointer travels with it"
);
m.revoked_at = Some(now);
let r = resolve(&m, &ResolverContext::human(), now);
assert_eq!(r.disposition, Disposition::Revoked { at: now });
assert!(r.variant.is_none(), "revoked serves nothing");
}
#[test]
fn resolve_is_pure_same_inputs_same_output() {
let m = minted();
let ctx = ResolverContext::anonymous_agent();
let now = Timestamp::from_unix_ms(9);
assert_eq!(resolve(&m, &ctx, now), resolve(&m, &ctx, now));
}
}