use std::collections::HashMap;
use std::sync::Arc;
use super::BoxedStorage;
#[derive(Clone, Default)]
pub struct StorageRegistry {
inner: Arc<RegistryInner>,
}
#[derive(Default)]
struct RegistryInner {
disks: HashMap<String, BoxedStorage>,
cdns: HashMap<String, String>, default_name: Option<String>,
}
impl StorageRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn set(self, name: impl Into<String>, storage: BoxedStorage) -> Self {
let mut inner = (*self.inner).rebuild();
inner.disks.insert(name.into(), storage);
Self { inner: Arc::new(inner) }
}
#[must_use]
pub fn cdn(self, disk: impl Into<String>, base: impl Into<String>) -> Self {
let mut inner = (*self.inner).rebuild();
let base = base.into().trim_end_matches('/').to_owned();
inner.cdns.insert(disk.into(), base);
Self { inner: Arc::new(inner) }
}
#[must_use]
pub fn with_default(self, name: impl Into<String>) -> Self {
let mut inner = (*self.inner).rebuild();
inner.default_name = Some(name.into());
Self { inner: Arc::new(inner) }
}
#[must_use]
pub fn disk(&self, name: &str) -> Option<BoxedStorage> {
self.inner.disks.get(name).cloned()
}
#[must_use]
pub fn default_disk(&self) -> Option<BoxedStorage> {
self.inner
.default_name
.as_deref()
.and_then(|n| self.disk(n))
}
#[must_use]
pub fn default_name(&self) -> Option<&str> {
self.inner.default_name.as_deref()
}
#[must_use]
pub fn names(&self) -> Vec<String> {
let mut out: Vec<String> = self.inner.disks.keys().cloned().collect();
out.sort();
out
}
#[must_use]
pub fn has(&self, name: &str) -> bool {
self.inner.disks.contains_key(name)
}
#[must_use]
pub fn cdn_url(&self, disk: &str, key: &str) -> Option<String> {
if let Some(base) = self.inner.cdns.get(disk) {
return Some(format!("{base}/{}", key.trim_start_matches('/')));
}
self.inner.disks.get(disk).and_then(|s| s.url(key))
}
#[must_use]
pub fn origin_url(&self, disk: &str, key: &str) -> Option<String> {
self.inner.disks.get(disk).and_then(|s| s.url(key))
}
#[must_use]
pub fn cdn_base(&self, disk: &str) -> Option<&str> {
self.inner.cdns.get(disk).map(String::as_str)
}
}
impl RegistryInner {
fn rebuild(&self) -> Self {
Self {
disks: self.disks.clone(),
cdns: self.cdns.clone(),
default_name: self.default_name.clone(),
}
}
}
impl std::fmt::Debug for StorageRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StorageRegistry")
.field("disks", &self.names())
.field("default", &self.default_name())
.field("cdns", &self.inner.cdns.keys().collect::<Vec<_>>())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::{InMemoryStorage, LocalStorage};
use std::path::PathBuf;
use std::sync::Arc as StdArc;
fn mem() -> BoxedStorage {
StdArc::new(InMemoryStorage::new())
}
fn local() -> BoxedStorage {
StdArc::new(LocalStorage::new(PathBuf::from("/tmp")))
}
#[test]
fn empty_registry_resolves_nothing() {
let r = StorageRegistry::new();
assert!(r.disk("avatars").is_none());
assert!(r.default_disk().is_none());
assert!(r.names().is_empty());
assert!(!r.has("anything"));
}
#[test]
fn set_then_resolve_by_name() {
let r = StorageRegistry::new()
.set("avatars", mem())
.set("docs", mem());
assert!(r.disk("avatars").is_some());
assert!(r.disk("docs").is_some());
assert!(r.disk("missing").is_none());
assert!(r.has("avatars"));
assert_eq!(r.names(), vec!["avatars".to_owned(), "docs".to_owned()]);
}
#[test]
fn set_replaces_existing_disk() {
let s1 = mem();
let s2 = mem();
let r = StorageRegistry::new()
.set("k", s1.clone())
.set("k", s2.clone());
let resolved = r.disk("k").unwrap();
assert!(StdArc::ptr_eq(&resolved, &s2));
assert!(!StdArc::ptr_eq(&resolved, &s1));
}
#[test]
fn default_disk_resolves_when_set() {
let r = StorageRegistry::new()
.set("avatars", mem())
.set("docs", mem())
.with_default("docs");
assert_eq!(r.default_name(), Some("docs"));
assert!(r.default_disk().is_some());
}
#[test]
fn default_disk_returns_none_when_target_unregistered() {
let r = StorageRegistry::new().with_default("missing");
assert_eq!(r.default_name(), Some("missing"));
assert!(r.default_disk().is_none());
}
#[test]
fn cdn_url_uses_configured_prefix() {
let r = StorageRegistry::new()
.set("avatars", mem())
.cdn("avatars", "https://cdn.example.com/avatars");
assert_eq!(
r.cdn_url("avatars", "alice.png").as_deref(),
Some("https://cdn.example.com/avatars/alice.png")
);
}
#[test]
fn cdn_url_strips_leading_slash_on_key() {
let r = StorageRegistry::new()
.set("a", mem())
.cdn("a", "https://cdn.example.com");
assert_eq!(
r.cdn_url("a", "/foo.png").as_deref(),
Some("https://cdn.example.com/foo.png")
);
}
#[test]
fn cdn_url_strips_trailing_slash_from_base() {
let r = StorageRegistry::new()
.set("a", mem())
.cdn("a", "https://cdn.example.com/");
assert_eq!(
r.cdn_url("a", "k.png").as_deref(),
Some("https://cdn.example.com/k.png")
);
}
#[test]
fn cdn_url_falls_back_to_backend_url_when_no_cdn() {
let r = StorageRegistry::new().set("local", local());
assert!(r.cdn_url("local", "k.txt").is_none());
}
#[test]
fn cdn_url_unknown_disk_returns_none() {
let r = StorageRegistry::new();
assert!(r.cdn_url("missing", "k").is_none());
}
#[test]
fn origin_url_bypasses_cdn() {
let local_with_url: BoxedStorage = StdArc::new(
LocalStorage::new(PathBuf::from("/tmp"))
.with_base_url("https://internal.example.com/files"),
);
let r = StorageRegistry::new()
.set("a", local_with_url)
.cdn("a", "https://cdn.example.com");
assert_eq!(
r.cdn_url("a", "k.txt").as_deref(),
Some("https://cdn.example.com/k.txt")
);
assert_eq!(
r.origin_url("a", "k.txt").as_deref(),
Some("https://internal.example.com/files/k.txt")
);
}
#[test]
fn cdn_base_returns_configured_value() {
let r = StorageRegistry::new()
.set("a", mem())
.cdn("a", "https://cdn.example.com");
assert_eq!(r.cdn_base("a"), Some("https://cdn.example.com"));
assert!(r.cdn_base("b").is_none());
}
#[test]
fn names_are_sorted() {
let r = StorageRegistry::new()
.set("z", mem())
.set("a", mem())
.set("m", mem());
assert_eq!(r.names(), vec!["a".to_owned(), "m".to_owned(), "z".to_owned()]);
}
#[test]
fn debug_renders_disk_names_and_default() {
let r = StorageRegistry::new()
.set("a", mem())
.set("b", mem())
.cdn("a", "https://cdn")
.with_default("a");
let s = format!("{r:?}");
assert!(s.contains("\"a\""));
assert!(s.contains("\"b\""));
assert!(s.contains("Some(\"a\")"));
}
#[test]
fn registry_clone_shares_arc_state() {
let r = StorageRegistry::new().set("a", mem());
let r2 = r.clone();
assert!(StdArc::ptr_eq(
&r.disk("a").unwrap(),
&r2.disk("a").unwrap()
));
}
}