use crate::config;
use anyhow::Result;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tier {
Own,
Shared,
Foreign,
}
impl Tier {
pub fn as_str(&self) -> &'static str {
match self {
Tier::Own => "own",
Tier::Shared => "shared",
Tier::Foreign => "foreign",
}
}
pub fn parse(s: &str) -> Option<Tier> {
match s.trim().to_lowercase().as_str() {
"own" => Some(Tier::Own),
"shared" => Some(Tier::Shared),
"foreign" => Some(Tier::Foreign),
_ => None,
}
}
pub fn caution(&self) -> &'static str {
match self {
Tier::Own => "high-trust",
Tier::Shared => "co-owned",
Tier::Foreign => "LOW-TRUST",
}
}
pub fn is_untrusted(&self) -> bool {
matches!(self, Tier::Foreign)
}
}
fn dir() -> Result<PathBuf> {
Ok(config::home()?.join(".confer"))
}
fn file(base: &Path) -> PathBuf {
base.join("tiers.json")
}
fn load_all(base: &Path) -> BTreeMap<String, String> {
std::fs::read_to_string(file(base))
.ok()
.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or_default()
}
fn save_all(base: &Path, map: &BTreeMap<String, String>) -> Result<()> {
std::fs::create_dir_all(base)?;
let p = file(base);
let tmp = p.with_extension("tmp");
std::fs::write(&tmp, serde_json::to_string_pretty(map)?)?;
std::fs::rename(&tmp, &p)?;
Ok(())
}
fn get_in(base: &Path, hub_key: &str) -> Option<Tier> {
load_all(base).get(hub_key).and_then(|s| Tier::parse(s))
}
fn set_in(base: &Path, hub_key: &str, tier: Tier) -> Result<()> {
let guard = crate::config::state_lock(&base.join("tiers.lock"));
if guard.is_none() {
return Err(anyhow::anyhow!(
"could not lock tiers.json (clone busy) — trust tier not written; retry"
));
}
let mut map = load_all(base);
map.insert(hub_key.to_string(), tier.as_str().to_string());
save_all(base, &map)
}
fn set_default_in(base: &Path, hub_key: &str, tier: Tier) -> Result<()> {
let guard = crate::config::state_lock(&base.join("tiers.lock"));
if guard.is_none() {
return Err(anyhow::anyhow!(
"could not lock tiers.json (clone busy) — default tier not written; will re-derive"
));
}
let mut map = load_all(base);
if map.get(hub_key).and_then(|s| Tier::parse(s)).is_none() {
map.insert(hub_key.to_string(), tier.as_str().to_string());
save_all(base, &map)?;
}
Ok(())
}
pub fn get(hub_key: &str) -> Option<Tier> {
dir().ok().and_then(|d| get_in(&d, hub_key))
}
pub fn set(hub_key: &str, tier: Tier) -> Result<()> {
set_in(&dir()?, hub_key, tier)
}
pub fn set_default(hub_key: &str, tier: Tier) -> Result<()> {
set_default_in(&dir()?, hub_key, tier)
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp() -> PathBuf {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
let d = std::env::temp_dir().join(format!("confer-tiers-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
d
}
#[test]
fn parse_and_roundtrip() {
assert_eq!(Tier::parse("own"), Some(Tier::Own));
assert_eq!(Tier::parse("FOREIGN"), Some(Tier::Foreign));
assert_eq!(Tier::parse("nope"), None);
assert_eq!(Tier::Own.as_str(), "own");
assert!(Tier::Foreign.is_untrusted() && !Tier::Own.is_untrusted());
}
#[test]
fn get_set_and_default() {
let d = tmp();
assert_eq!(get_in(&d, "hub"), None);
set_in(&d, "hub", Tier::Foreign).unwrap();
assert_eq!(get_in(&d, "hub"), Some(Tier::Foreign));
set_default_in(&d, "hub", Tier::Own).unwrap();
assert_eq!(get_in(&d, "hub"), Some(Tier::Foreign));
set_default_in(&d, "fresh", Tier::Own).unwrap();
assert_eq!(get_in(&d, "fresh"), Some(Tier::Own));
}
#[test]
fn tiers_are_per_hub() {
let d = tmp();
set_in(&d, "a", Tier::Own).unwrap();
set_in(&d, "b", Tier::Foreign).unwrap();
assert_eq!(get_in(&d, "a"), Some(Tier::Own));
assert_eq!(get_in(&d, "b"), Some(Tier::Foreign));
}
}