use std::collections::BTreeMap;
use std::path::Path;
use serde::Deserialize;
use vta_sdk::session::{SessionInfo, SessionStore};
#[derive(Debug, Default, Deserialize)]
struct PnmConfigView {
#[serde(default)]
vtas: BTreeMap<String, PnmVtaView>,
}
#[derive(Debug, Deserialize)]
struct PnmVtaView {
#[serde(default)]
name: Option<String>,
#[serde(default)]
vta_did: Option<String>,
#[serde(default)]
url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PnmMatch {
pub slug: String,
pub name: String,
pub url: Option<String>,
}
pub fn matches_in(config_toml: &str, vta_did: &str) -> Vec<PnmMatch> {
let Ok(view) = toml::from_str::<PnmConfigView>(config_toml) else {
return Vec::new();
};
view.vtas
.into_iter()
.filter(|(_, v)| v.vta_did.as_deref().map(str::trim) == Some(vta_did.trim()))
.map(|(slug, v)| PnmMatch {
name: v.name.unwrap_or_else(|| slug.clone()),
slug,
url: v.url,
})
.collect()
}
pub fn find(vta_did: &str) -> Vec<PnmMatch> {
let Ok(dir) = vta_sdk::agent_connect::pnm_profile_dir() else {
return Vec::new();
};
find_in(&dir, vta_did)
}
fn find_in(dir: &Path, vta_did: &str) -> Vec<PnmMatch> {
match std::fs::read_to_string(dir.join("config.toml")) {
Ok(s) => matches_in(&s, vta_did),
Err(_) => Vec::new(),
}
}
pub fn store() -> Option<SessionStore> {
let dir = vta_sdk::agent_connect::pnm_profile_dir().ok()?;
Some(SessionStore::new(
&vta_sdk::agent_connect::pnm_service_name(),
dir,
))
}
pub fn session_for(store: &SessionStore, slug: &str, vta_did: &str) -> Option<SessionInfo> {
let info = store.loaded_session(&vta_sdk::agent_connect::pnm_session_key(slug))?;
(info.vta_did.as_deref() == Some(vta_did)).then_some(info)
}
#[cfg(test)]
mod tests {
use super::*;
use vta_sdk::session::testing::InMemorySessionBackend;
const PNM_TOML: &str = r#"
default_vta = "home"
[vtas.home]
name = "Home VTA"
vta_did = "did:webvh:QmHome:vta.example.com"
[vtas.work]
name = "Work VTA"
vta_did = "did:webvh:QmWork:work.example.com"
[vtas.pending]
name = "Not yet"
[vtas.local]
name = "Local"
vta_did = "did:key:z6MkLocal"
url = "http://localhost:7001"
"#;
#[test]
fn a_pnm_profile_for_the_same_vta_is_found() {
let found = matches_in(PNM_TOML, "did:webvh:QmHome:vta.example.com");
assert_eq!(
found,
vec![PnmMatch {
slug: "home".into(),
name: "Home VTA".into(),
url: None,
}]
);
}
#[test]
fn another_vta_or_a_pending_pnm_setup_is_not_offered() {
assert!(matches_in(PNM_TOML, "did:webvh:QmOther:x.example.com").is_empty());
assert!(matches_in(PNM_TOML, "").is_empty());
}
#[test]
fn the_explicit_url_pnm_holds_for_a_did_key_vta_is_kept() {
let found = matches_in(PNM_TOML, "did:key:z6MkLocal");
assert_eq!(found[0].url.as_deref(), Some("http://localhost:7001"));
}
#[test]
fn an_unparseable_or_missing_profile_is_none() {
assert!(matches_in("this is [not toml", "did:key:z").is_empty());
let dir = tempfile::tempdir().unwrap();
assert!(find_in(dir.path(), "did:key:z").is_empty());
std::fs::write(dir.path().join("config.toml"), PNM_TOML).unwrap();
assert_eq!(
find_in(dir.path(), "did:webvh:QmWork:work.example.com")[0].slug,
"work"
);
}
#[test]
fn only_a_session_bound_to_the_same_vta_is_lent() {
let store = SessionStore::with_backend(Box::new(InMemorySessionBackend::new()));
store
.store_direct("vta:home", "did:key:z6MkPnm", "zKey", "did:webvh:QmHome")
.unwrap();
store
.store_pending_vta_binding("vta:pending", "did:key:z6MkTemp", "zKey")
.unwrap();
assert_eq!(
session_for(&store, "home", "did:webvh:QmHome")
.unwrap()
.client_did,
"did:key:z6MkPnm"
);
assert!(session_for(&store, "home", "did:webvh:QmOther").is_none());
assert!(session_for(&store, "pending", "did:webvh:QmHome").is_none());
assert!(session_for(&store, "absent", "did:webvh:QmHome").is_none());
}
}