use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OkfDecision {
Trust,
Acknowledge,
Ignore,
}
impl OkfDecision {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Trust => "trust",
Self::Acknowledge => "acknowledge",
Self::Ignore => "ignore",
}
}
#[must_use]
pub fn from_token(token: &str) -> Option<Self> {
match token {
"trust" => Some(Self::Trust),
"acknowledge" => Some(Self::Acknowledge),
"ignore" => Some(Self::Ignore),
_ => None,
}
}
#[must_use]
pub fn imports(self) -> bool {
matches!(self, Self::Trust | Self::Acknowledge)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OkfConsent {
pub peer: String,
pub decision: OkfDecision,
pub root: String,
pub screen_classes: String,
pub decided_at: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConsentState {
Unasked,
Holds(OkfDecision),
Moved {
was: String,
},
Lapsed {
was: String,
now: String,
},
}
impl ConsentState {
#[must_use]
pub fn decision(&self) -> Option<OkfDecision> {
match self {
Self::Holds(d) => Some(*d),
_ => None,
}
}
#[must_use]
pub fn why_asking(&self) -> Option<String> {
match self {
Self::Holds(_) => None,
Self::Unasked => Some("not seen before".to_owned()),
Self::Moved { was } => Some(format!("the bundle moved (it was at {was})")),
Self::Lapsed { was, now } => Some(format!(
"the bundle now screens differently (it was [{was}], it is now [{now}])"
)),
}
}
}
#[must_use]
pub fn screen_fingerprint(classes: &[&str]) -> String {
let mut sorted: Vec<&str> = classes.to_vec();
sorted.sort_unstable();
sorted.dedup();
sorted.join(",")
}
#[must_use]
pub fn screen_regressed(was: &str, now: &str) -> bool {
let before: Vec<&str> = was.split(',').filter(|s| !s.is_empty()).collect();
now.split(',')
.filter(|s| !s.is_empty())
.any(|class| !before.contains(&class))
}
#[cfg(test)]
mod tests {
use super::{OkfDecision, screen_fingerprint, screen_regressed};
#[test]
fn tokens_round_trip_and_are_a_closed_set() {
for d in [
OkfDecision::Trust,
OkfDecision::Acknowledge,
OkfDecision::Ignore,
] {
assert_eq!(OkfDecision::from_token(d.as_str()), Some(d));
}
assert_eq!(OkfDecision::from_token("external-inferred"), None);
}
#[test]
fn only_ignore_declines_to_import() {
assert!(OkfDecision::Trust.imports());
assert!(OkfDecision::Acknowledge.imports());
assert!(!OkfDecision::Ignore.imports());
}
#[test]
fn the_fingerprint_is_sorted_and_deduplicated() {
assert_eq!(
screen_fingerprint(&["model-directive", "invisible-characters", "model-directive"]),
"invisible-characters,model-directive"
);
assert_eq!(screen_fingerprint(&[]), "");
}
#[test]
fn a_new_finding_class_regresses_and_a_removed_one_does_not() {
assert!(screen_regressed("", "invisible-characters"));
assert!(screen_regressed(
"hidden-presentation",
"hidden-presentation,model-directive"
));
assert!(!screen_regressed("hidden-presentation", ""));
assert!(!screen_regressed(
"hidden-presentation,model-directive",
"model-directive"
));
assert!(!screen_regressed("model-directive", "model-directive"));
}
}