use crate::pair_decision::{InboundMode, OrgPolicy};
use anyhow::Result;
use serde_json::json;
use std::collections::HashMap;
use std::path::Path;
const FILE: &str = "org_policies.json";
#[derive(Debug, Clone, Default)]
pub struct FileOrgPolicy {
orgs: HashMap<String, InboundMode>,
}
impl FileOrgPolicy {
pub fn load() -> Self {
match crate::config::config_dir() {
Ok(dir) => Self::load_path(&dir.join(FILE)),
Err(_) => Self::default(),
}
}
pub fn load_path(path: &Path) -> Self {
let Ok(bytes) = std::fs::read(path) else {
return Self::default();
};
let Ok(json) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
return Self::default();
};
let mut orgs = HashMap::new();
if let Some(map) = json.get("orgs").and_then(|v| v.as_object()) {
for (org_did, entry) in map {
if let Some(mode) = entry
.get("inbound")
.and_then(|v| v.as_str())
.and_then(parse_mode)
{
orgs.insert(org_did.clone(), mode);
}
}
}
Self { orgs }
}
pub fn set(&mut self, org_did: &str, mode: InboundMode) {
self.orgs.insert(org_did.to_string(), mode);
}
pub fn remove(&mut self, org_did: &str) {
self.orgs.remove(org_did);
}
pub fn len(&self) -> usize {
self.orgs.len()
}
pub fn entries(&self) -> impl Iterator<Item = (&String, &InboundMode)> {
self.orgs.iter()
}
pub fn is_empty(&self) -> bool {
self.orgs.is_empty()
}
pub fn save(&self) -> Result<()> {
let dir = crate::config::config_dir()?;
std::fs::create_dir_all(&dir)?;
self.save_path(&dir.join(FILE))?;
Ok(())
}
pub fn save_path(&self, path: &Path) -> std::io::Result<()> {
std::fs::write(path, self.to_json())
}
fn to_json(&self) -> String {
let orgs: serde_json::Map<String, serde_json::Value> = self
.orgs
.iter()
.map(|(k, v)| (k.clone(), json!({ "inbound": mode_str(*v) })))
.collect();
serde_json::to_string_pretty(&json!({ "version": 1, "orgs": orgs }))
.unwrap_or_else(|_| "{}".into())
}
}
impl OrgPolicy for FileOrgPolicy {
fn inbound_mode(&self, org_did: &str) -> Option<InboundMode> {
self.orgs.get(org_did).copied()
}
}
fn parse_mode(s: &str) -> Option<InboundMode> {
match s {
"auto" => Some(InboundMode::Auto),
"notify" => Some(InboundMode::Notify),
_ => None,
}
}
fn mode_str(m: InboundMode) -> &'static str {
match m {
InboundMode::Auto => "auto",
InboundMode::Notify => "notify",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp(name: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!("wire-orgpol-{}-{name}.json", std::process::id()))
}
#[test]
fn missing_file_is_default_deny() {
let p = tmp("missing");
let _ = std::fs::remove_file(&p);
let pol = FileOrgPolicy::load_path(&p);
assert!(pol.is_empty());
assert_eq!(pol.inbound_mode("did:wire:org:slanchaai-1"), None);
}
#[test]
fn malformed_file_is_default_deny() {
let p = tmp("malformed");
std::fs::write(&p, b"not json {{{").unwrap();
let pol = FileOrgPolicy::load_path(&p);
assert!(pol.is_empty(), "malformed policy must fail closed to empty");
let _ = std::fs::remove_file(&p);
}
#[test]
fn set_save_load_roundtrip() {
let p = tmp("roundtrip");
let mut pol = FileOrgPolicy::default();
pol.set("did:wire:org:slanchaai-1", InboundMode::Auto);
pol.set("did:wire:org:contractor-2", InboundMode::Notify);
pol.save_path(&p).unwrap();
let loaded = FileOrgPolicy::load_path(&p);
assert_eq!(
loaded.inbound_mode("did:wire:org:slanchaai-1"),
Some(InboundMode::Auto)
);
assert_eq!(
loaded.inbound_mode("did:wire:org:contractor-2"),
Some(InboundMode::Notify)
);
assert_eq!(loaded.inbound_mode("did:wire:org:unknown-9"), None);
let _ = std::fs::remove_file(&p);
}
#[test]
fn unknown_mode_string_is_skipped() {
let p = tmp("badmode");
std::fs::write(
&p,
br#"{"version":1,"orgs":{"did:wire:org:x-1":{"inbound":"superuser"}}}"#,
)
.unwrap();
let pol = FileOrgPolicy::load_path(&p);
assert_eq!(pol.inbound_mode("did:wire:org:x-1"), None);
let _ = std::fs::remove_file(&p);
}
#[test]
fn remove_drops_org() {
let mut pol = FileOrgPolicy::default();
pol.set("did:wire:org:x-1", InboundMode::Auto);
pol.remove("did:wire:org:x-1");
assert_eq!(pol.inbound_mode("did:wire:org:x-1"), None);
}
}