use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::OnceLock;
use serde::Deserialize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PeerEntry {
Inproc,
Http { url: String },
Lambda { function_url: String },
}
#[derive(Debug, Deserialize)]
#[serde(tag = "mode", rename_all = "lowercase")]
enum RawEntry {
Inproc,
Http { url: String },
Lambda { function_url: String },
}
impl From<RawEntry> for PeerEntry {
fn from(raw: RawEntry) -> Self {
match raw {
RawEntry::Inproc => PeerEntry::Inproc,
RawEntry::Http { url } => PeerEntry::Http { url },
RawEntry::Lambda { function_url } => PeerEntry::Lambda { function_url },
}
}
}
pub fn parse(raw: &str) -> Result<HashMap<String, PeerEntry>, serde_json::Error> {
let trimmed = raw.trim();
if trimmed.is_empty() || trimmed == "null" {
return Ok(HashMap::new());
}
let parsed: HashMap<String, RawEntry> = serde_json::from_str(trimmed)?;
Ok(parsed.into_iter().map(|(k, v)| (k, v.into())).collect())
}
fn cached_table() -> &'static HashMap<String, PeerEntry> {
static TABLE: OnceLock<HashMap<String, PeerEntry>> = OnceLock::new();
TABLE.get_or_init(|| match std::env::var("CARAVAN_RPC_PEERS") {
Ok(raw) => parse(&raw).unwrap_or_else(|e| {
panic!(
"CARAVAN_RPC_PEERS is not valid JSON ({e}). \
Expected shape: {{\"InterfaceName\":{{\"mode\":\"inproc|http|lambda\",...}}}}"
)
}),
Err(_) => HashMap::new(),
})
}
thread_local! {
static PEER_TABLE_OVERRIDE: RefCell<Option<HashMap<String, PeerEntry>>> = const { RefCell::new(None) };
}
pub fn peer_for(interface: &str) -> Option<PeerEntry> {
let from_override =
PEER_TABLE_OVERRIDE.with(|cell| cell.borrow().as_ref().map(|t| t.get(interface).cloned()));
match from_override {
Some(maybe_entry) => maybe_entry,
None => cached_table().get(interface).cloned(),
}
}
#[doc(hidden)]
pub fn __set_table_override_for_tests(table: HashMap<String, PeerEntry>) {
PEER_TABLE_OVERRIDE.with(|cell| {
*cell.borrow_mut() = Some(table);
});
}
#[doc(hidden)]
pub fn __clear_table_override_for_tests() {
PEER_TABLE_OVERRIDE.with(|cell| {
*cell.borrow_mut() = None;
});
}
pub fn shared_secret() -> Option<String> {
std::env::var("CARAVAN_RPC_SHARED_SECRET").ok()
}
#[doc(hidden)]
pub fn parse_from_env() -> HashMap<String, PeerEntry> {
match std::env::var("CARAVAN_RPC_PEERS") {
Ok(raw) => parse(&raw).unwrap_or_default(),
Err(_) => HashMap::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_empty_string_yields_empty_map() {
let t = parse("").unwrap();
assert!(t.is_empty());
}
#[test]
fn parse_null_yields_empty_map() {
let t = parse("null").unwrap();
assert!(t.is_empty());
}
#[test]
fn parse_inproc_entry() {
let raw = r#"{"Embedder":{"mode":"inproc"}}"#;
let t = parse(raw).unwrap();
assert_eq!(t.get("Embedder"), Some(&PeerEntry::Inproc));
}
#[test]
fn parse_http_entry() {
let raw = r#"{"Embedder":{"mode":"http","url":"http://embedder:8080"}}"#;
let t = parse(raw).unwrap();
assert_eq!(
t.get("Embedder"),
Some(&PeerEntry::Http {
url: "http://embedder:8080".into()
})
);
}
#[test]
fn parse_lambda_entry() {
let raw = r#"{"FraudCheck":{"mode":"lambda","function_url":"https://x.lambda-url.us-east-1.on.aws/"}}"#;
let t = parse(raw).unwrap();
assert_eq!(
t.get("FraudCheck"),
Some(&PeerEntry::Lambda {
function_url: "https://x.lambda-url.us-east-1.on.aws/".into()
})
);
}
#[test]
fn parse_mixed_modes() {
let raw = r#"{
"Embedder": {"mode": "inproc"},
"Billing": {"mode": "http", "url": "http://billing:8080"},
"FraudCheck": {"mode": "lambda", "function_url": "https://abc.lambda-url.us-east-1.on.aws/"}
}"#;
let t = parse(raw).unwrap();
assert_eq!(t.len(), 3);
assert!(matches!(t.get("Embedder"), Some(PeerEntry::Inproc)));
assert!(matches!(t.get("Billing"), Some(PeerEntry::Http { .. })));
assert!(matches!(
t.get("FraudCheck"),
Some(PeerEntry::Lambda { .. })
));
}
#[test]
fn parse_rejects_unknown_mode() {
let raw = r#"{"X":{"mode":"telepathy"}}"#;
assert!(parse(raw).is_err());
}
#[test]
fn parse_rejects_http_missing_url() {
let raw = r#"{"X":{"mode":"http"}}"#;
assert!(parse(raw).is_err());
}
}