use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::LineageStore;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Owner {
pub user_id: String,
pub display_name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OwnerGate {
AbortConfigMismatch,
AbortMismatch,
Repin,
Proceed,
FirstUse,
}
impl OwnerGate {
pub fn is_additive(self) -> bool {
matches!(self, OwnerGate::Repin)
}
}
pub fn owner_gate(
store_owner: Option<&Owner>,
configured_id: Option<&str>,
authed_user_id: &str,
allow_change: bool,
) -> OwnerGate {
if let Some(configured) = configured_id
&& configured != authed_user_id
{
return OwnerGate::AbortConfigMismatch;
}
match store_owner {
None => OwnerGate::FirstUse,
Some(owner) if owner.user_id == authed_user_id => OwnerGate::Proceed,
Some(_) if allow_change => OwnerGate::Repin,
Some(_) => OwnerGate::AbortMismatch,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdoptDecision {
PinFresh,
PinAdopt,
AdoptForced,
Abort,
SkipPin,
}
impl AdoptDecision {
pub fn is_additive(self) -> bool {
matches!(self, AdoptDecision::AdoptForced)
}
}
pub fn adopt_decision(
listed: &[&str],
owned: &BTreeSet<&str>,
enumerated: bool,
allow_change: bool,
) -> AdoptDecision {
if owned.is_empty() {
return AdoptDecision::PinFresh;
}
if !enumerated {
return AdoptDecision::SkipPin;
}
if listed.iter().any(|id| owned.contains(id)) {
AdoptDecision::PinAdopt
} else if allow_change {
AdoptDecision::AdoptForced
} else {
AdoptDecision::Abort
}
}
impl LineageStore {
pub fn owner(&self) -> Option<&Owner> {
self.owner.as_ref()
}
pub fn pin_owner(&mut self, owner: Owner) {
self.owner = Some(owner);
}
pub fn refresh_display_name(&mut self, display_name: &str) -> bool {
match &mut self.owner {
Some(owner) if owner.display_name != display_name => {
owner.display_name = display_name.to_owned();
true
}
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn owner(id: &str, name: &str) -> Owner {
Owner {
user_id: id.to_owned(),
display_name: name.to_owned(),
}
}
#[test]
fn refresh_display_name_only_when_changed_and_never_when_unpinned() {
let mut store = LineageStore::new();
assert!(!store.refresh_display_name("Alice"));
assert!(store.owner().is_none());
store.pin_owner(owner("user_a", "Alice"));
assert!(!store.refresh_display_name("Alice"));
assert!(store.refresh_display_name("Alice Cooper"));
assert_eq!(store.owner().unwrap().display_name, "Alice Cooper");
assert_eq!(store.owner().unwrap().user_id, "user_a");
}
#[test]
fn owner_gate_covers_the_full_matrix() {
let alice = owner("user_a", "Alice");
assert_eq!(owner_gate(None, None, "user_a", false), OwnerGate::FirstUse);
assert_eq!(owner_gate(None, None, "user_a", true), OwnerGate::FirstUse);
assert_eq!(
owner_gate(Some(&alice), None, "user_a", false),
OwnerGate::Proceed
);
assert_eq!(
owner_gate(Some(&alice), None, "user_b", false),
OwnerGate::AbortMismatch
);
assert_eq!(
owner_gate(Some(&alice), None, "user_b", true),
OwnerGate::Repin
);
assert_eq!(
owner_gate(Some(&alice), Some("user_c"), "user_a", true),
OwnerGate::AbortConfigMismatch
);
assert_eq!(
owner_gate(None, Some("user_c"), "user_a", true),
OwnerGate::AbortConfigMismatch
);
assert_eq!(
owner_gate(Some(&alice), Some("user_a"), "user_a", false),
OwnerGate::Proceed
);
assert!(OwnerGate::Repin.is_additive());
for gate in [
OwnerGate::AbortConfigMismatch,
OwnerGate::AbortMismatch,
OwnerGate::Proceed,
OwnerGate::FirstUse,
] {
assert!(!gate.is_additive());
}
}
#[test]
fn adopt_decision_covers_every_branch() {
let owned: BTreeSet<&str> = ["c1", "c2"].into_iter().collect();
let empty: BTreeSet<&str> = BTreeSet::new();
assert_eq!(
adopt_decision(&["x", "y"], &empty, true, false),
AdoptDecision::PinFresh
);
assert_eq!(
adopt_decision(&["c1"], &owned, false, false),
AdoptDecision::SkipPin
);
assert_eq!(
adopt_decision(&["c1"], &owned, false, true),
AdoptDecision::SkipPin
);
assert_eq!(
adopt_decision(&["c1", "z"], &owned, true, false),
AdoptDecision::PinAdopt
);
assert_eq!(
adopt_decision(&["z1", "z2"], &owned, true, false),
AdoptDecision::Abort
);
assert_eq!(
adopt_decision(&["z1", "z2"], &owned, true, true),
AdoptDecision::AdoptForced
);
assert!(AdoptDecision::AdoptForced.is_additive());
for decision in [
AdoptDecision::PinFresh,
AdoptDecision::PinAdopt,
AdoptDecision::Abort,
AdoptDecision::SkipPin,
] {
assert!(!decision.is_additive());
}
}
#[test]
fn older_store_without_owner_loads_as_none_and_pinned_roundtrips() {
let json = r#"{"nodes":{},"edges":[]}"#;
let store: LineageStore = serde_json::from_str(json).unwrap();
assert!(store.owner().is_none());
let value = serde_json::to_value(&store).unwrap();
assert!(value.get("owner").is_none());
let mut pinned = LineageStore::new();
pinned.pin_owner(owner("user_a", "Alice"));
let back: LineageStore =
serde_json::from_str(&serde_json::to_string(&pinned).unwrap()).unwrap();
assert_eq!(back, pinned);
assert_eq!(back.owner().unwrap().user_id, "user_a");
}
}