use collection_literals::btree;
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::overlay_store::{OverlayStore, RedirectMode, StoreBox};
use crate::{path, Error, Path, Reader, Record, Value, Writer};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
#[non_exhaustive]
pub enum MountConfig {
Memory,
Local { path: String },
Http { url: String },
HttpBroker,
AsyncHttpBroker,
Structfs { url: String },
Help,
Sys,
Repl,
Registers,
Log { path: String },
Recording { path: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MountInfo {
pub path: String,
pub config: MountConfig,
}
pub trait StoreFactory: Send + Sync {
fn create(&self, config: &MountConfig) -> Result<StoreBox, Error>;
}
pub struct MountStore<F: StoreFactory> {
overlay: OverlayStore,
mounts: BTreeMap<String, MountConfig>,
factory: F,
}
const MOUNTS_PREFIX: [&str; 2] = ["ctx", "mounts"];
impl<F: StoreFactory> MountStore<F> {
pub fn new(factory: F) -> Self {
Self {
overlay: OverlayStore::new(),
mounts: BTreeMap::new(),
factory,
}
}
pub fn mount(&mut self, name: &str, config: MountConfig) -> Result<(), Error> {
let store = self.factory.create(&config)?;
let mount_path = Path::parse(name).map_err(Error::Path)?;
self.overlay.mount(mount_path.clone(), store);
self.mounts.insert(name.to_string(), config);
self.discover_and_redirect_docs(name, &mount_path);
Ok(())
}
fn discover_and_redirect_docs(&mut self, name: &str, mount_path: &Path) {
let docs_path = mount_path.join(&path!("docs"));
if self.overlay.read(&docs_path).ok().flatten().is_some() {
if let Ok(help_suffix) = Path::parse(name) {
let help_path = path!("ctx/help").join(&help_suffix);
self.overlay.add_redirect(
help_path,
docs_path,
RedirectMode::ReadOnly,
Some(name.to_string()),
);
}
}
}
pub fn mount_store(&mut self, name: &str, store: StoreBox) -> Result<(), Error> {
let mount_path = Path::parse(name).map_err(Error::Path)?;
self.overlay.mount(mount_path.clone(), store);
self.discover_and_redirect_docs(name, &mount_path);
Ok(())
}
pub fn unmount(&mut self, name: &str) -> Result<(), Error> {
if !self.mounts.contains_key(name) {
return Err(Error::store(
"mount_store",
"unmount",
format!("No mount at '{}'", name),
));
}
let mount_path = Path::parse(name)?;
self.overlay.unmount(&mount_path);
self.overlay.remove_redirects_for_mount(name);
self.mounts.remove(name);
Ok(())
}
pub fn list_redirects(&self) -> Vec<(Path, Path, RedirectMode)> {
self.overlay.list_redirects()
}
pub fn list_mounts(&self) -> Vec<MountInfo> {
self.mounts
.iter()
.map(|(path, config)| MountInfo {
path: path.clone(),
config: config.clone(),
})
.collect()
}
fn is_mounts_path(path: &Path) -> bool {
path.len() >= 2 && &path[0] == MOUNTS_PREFIX[0] && &path[1] == MOUNTS_PREFIX[1]
}
fn get_mount_name(path: &Path) -> Option<String> {
if path.len() >= 3 && &path[0] == MOUNTS_PREFIX[0] && &path[1] == MOUNTS_PREFIX[1] {
Some(path.slice(2, path.len()).to_string())
} else {
None
}
}
fn mounts_to_value(&self) -> Value {
let mounts = self.list_mounts();
let arr: Vec<Value> = mounts
.into_iter()
.map(|info| {
Value::Map(btree! {
"path".to_string() => Value::String(info.path),
"config".to_string() => config_to_value(&info.config),
})
})
.collect();
Value::Array(arr)
}
fn config_to_value(config: &MountConfig) -> Value {
config_to_value(config)
}
}
fn config_to_value(config: &MountConfig) -> Value {
Value::Map(match config {
MountConfig::Memory => btree! {
"type".to_string() => Value::String("memory".to_string()),
},
MountConfig::Local { path } => btree! {
"type".to_string() => Value::String("local".to_string()),
"path".to_string() => Value::String(path.clone()),
},
MountConfig::Http { url } => btree! {
"type".to_string() => Value::String("http".to_string()),
"url".to_string() => Value::String(url.clone()),
},
MountConfig::HttpBroker => btree! {
"type".to_string() => Value::String("httpbroker".to_string()),
},
MountConfig::AsyncHttpBroker => btree! {
"type".to_string() => Value::String("asynchttpbroker".to_string()),
},
MountConfig::Structfs { url } => btree! {
"type".to_string() => Value::String("structfs".to_string()),
"url".to_string() => Value::String(url.clone()),
},
MountConfig::Help => btree! {
"type".to_string() => Value::String("help".to_string()),
},
MountConfig::Sys => btree! {
"type".to_string() => Value::String("sys".to_string()),
},
MountConfig::Repl => btree! {
"type".to_string() => Value::String("repl".to_string()),
},
MountConfig::Registers => btree! {
"type".to_string() => Value::String("registers".to_string()),
},
MountConfig::Log { path } => btree! {
"type".to_string() => Value::String("log".to_string()),
"path".to_string() => Value::String(path.clone()),
},
MountConfig::Recording { path } => btree! {
"type".to_string() => Value::String("recording".to_string()),
"path".to_string() => Value::String(path.clone()),
},
})
}
fn value_to_config(value: &Value) -> Result<MountConfig, Error> {
match value {
Value::Map(map) => {
let type_str = map
.get("type")
.and_then(|v| match v {
Value::String(s) => Some(s.as_str()),
_ => None,
})
.ok_or_else(|| {
Error::decode(crate::Format::VALUE, "Missing 'type' field in mount config")
})?;
match type_str {
"memory" => Ok(MountConfig::Memory),
"local" => {
let path = map
.get("path")
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
})
.ok_or_else(|| {
Error::decode(
crate::Format::VALUE,
"Missing 'path' field for local mount",
)
})?;
Ok(MountConfig::Local { path })
}
"http" => {
let url = map
.get("url")
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
})
.ok_or_else(|| {
Error::decode(
crate::Format::VALUE,
"Missing 'url' field for http mount",
)
})?;
Ok(MountConfig::Http { url })
}
"httpbroker" => Ok(MountConfig::HttpBroker),
"asynchttpbroker" => Ok(MountConfig::AsyncHttpBroker),
"structfs" => {
let url = map
.get("url")
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
})
.ok_or_else(|| {
Error::decode(
crate::Format::VALUE,
"Missing 'url' field for structfs mount",
)
})?;
Ok(MountConfig::Structfs { url })
}
"help" => Ok(MountConfig::Help),
"sys" => Ok(MountConfig::Sys),
"repl" => Ok(MountConfig::Repl),
"registers" => Ok(MountConfig::Registers),
"log" | "recording" => {
let path = map
.get("path")
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
})
.ok_or_else(|| {
Error::decode(
crate::Format::VALUE,
format!("Missing 'path' field for {type_str} mount"),
)
})?;
Ok(match type_str {
"log" => MountConfig::Log { path },
_ => MountConfig::Recording { path },
})
}
other => Err(Error::decode(
crate::Format::VALUE,
format!("Unknown mount type: {}", other),
)),
}
}
_ => Err(Error::decode(
crate::Format::VALUE,
"Mount config must be a map",
)),
}
}
impl<F: StoreFactory> Reader for MountStore<F> {
fn read(&mut self, from: &Path) -> Result<Option<Record>, Error> {
if Self::is_mounts_path(from) {
if from.len() == 2 {
let value = self.mounts_to_value();
return Ok(Some(Record::parsed(value)));
} else if let Some(name) = Self::get_mount_name(from) {
if let Some(config) = self.mounts.get(&name) {
let value = Self::config_to_value(config);
return Ok(Some(Record::parsed(value)));
} else {
return Ok(None);
}
}
}
self.overlay.read(from)
}
}
impl<F: StoreFactory> Writer for MountStore<F> {
fn write(&mut self, destination: &Path, data: Record) -> Result<Path, Error> {
if Self::is_mounts_path(destination) {
if let Some(name) = Self::get_mount_name(destination) {
let value = data.into_value(&crate::NoCodec)?;
if value == Value::Null {
self.unmount(&name)?;
} else {
let config = value_to_config(&value)?;
self.mount(&name, config)?;
}
return Ok(destination.clone());
} else {
return Err(Error::store(
"mount_store",
"write",
"Cannot write directly to /ctx/mounts",
));
}
}
self.overlay.write(destination, data)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{path, NoCodec};
use std::collections::HashMap;
struct TestStore {
data: HashMap<Path, Record>,
}
impl TestStore {
fn new() -> Self {
Self {
data: HashMap::new(),
}
}
}
impl Reader for TestStore {
fn read(&mut self, from: &Path) -> Result<Option<Record>, Error> {
Ok(self.data.get(from).cloned())
}
}
impl Writer for TestStore {
fn write(&mut self, to: &Path, data: Record) -> Result<Path, Error> {
self.data.insert(to.clone(), data);
Ok(to.clone())
}
}
struct TestFactory;
impl StoreFactory for TestFactory {
fn create(&self, _config: &MountConfig) -> Result<StoreBox, Error> {
Ok(Box::new(TestStore::new()))
}
}
#[test]
fn mount_and_access() {
let mut store = MountStore::new(TestFactory);
store.mount("data", MountConfig::Memory).unwrap();
store
.write(&path!("data/test"), Record::parsed(Value::from("hello")))
.unwrap();
let record = store.read(&path!("data/test")).unwrap().unwrap();
let value = record.into_value(&NoCodec).unwrap();
assert_eq!(value, Value::from("hello"));
}
#[test]
fn list_mounts() {
let mut store = MountStore::new(TestFactory);
store.mount("data", MountConfig::Memory).unwrap();
store
.mount(
"local",
MountConfig::Local {
path: "/tmp".to_string(),
},
)
.unwrap();
let record = store.read(&path!("ctx/mounts")).unwrap().unwrap();
let value = record.into_value(&NoCodec).unwrap();
match value {
Value::Array(arr) => {
assert_eq!(arr.len(), 2);
}
_ => panic!("expected array"),
}
}
#[test]
fn mount_via_write() {
let mut store = MountStore::new(TestFactory);
let config = config_to_value(&MountConfig::Memory);
store
.write(&path!("ctx/mounts/data"), Record::parsed(config))
.unwrap();
let mounts = store.list_mounts();
assert_eq!(mounts.len(), 1);
assert_eq!(mounts[0].path, "data");
}
#[test]
fn unmount_via_write_null() {
let mut store = MountStore::new(TestFactory);
store.mount("data", MountConfig::Memory).unwrap();
assert_eq!(store.list_mounts().len(), 1);
store
.write(&path!("ctx/mounts/data"), Record::parsed(Value::Null))
.unwrap();
assert_eq!(store.list_mounts().len(), 0);
}
#[test]
fn config_conversion_roundtrip() {
let configs = vec![
MountConfig::Memory,
MountConfig::Local {
path: "/tmp/test".to_string(),
},
MountConfig::Http {
url: "https://api.example.com".to_string(),
},
MountConfig::HttpBroker,
MountConfig::AsyncHttpBroker,
MountConfig::Structfs {
url: "https://fs.example.com".to_string(),
},
MountConfig::Help,
MountConfig::Sys,
MountConfig::Repl,
MountConfig::Registers,
];
for config in configs {
let value = config_to_value(&config);
let back = value_to_config(&value).unwrap();
assert_eq!(config, back);
}
}
#[test]
fn mount_store_directly() {
let mut store = MountStore::new(TestFactory);
let test_store = Box::new(TestStore::new());
store.mount_store("direct", test_store).unwrap();
store
.write(
&path!("direct/test"),
Record::parsed(Value::from("direct_value")),
)
.unwrap();
let record = store.read(&path!("direct/test")).unwrap().unwrap();
let value = record.into_value(&NoCodec).unwrap();
assert_eq!(value, Value::from("direct_value"));
}
#[test]
fn unmount_nonexistent_fails() {
let mut store = MountStore::new(TestFactory);
let result = store.unmount("nonexistent");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("No mount"));
}
#[test]
fn read_specific_mount_config() {
let mut store = MountStore::new(TestFactory);
store
.mount(
"mydata",
MountConfig::Local {
path: "/my/path".to_string(),
},
)
.unwrap();
let record = store.read(&path!("ctx/mounts/mydata")).unwrap().unwrap();
let value = record.into_value(&NoCodec).unwrap();
match value {
Value::Map(map) => {
assert_eq!(map.get("type"), Some(&Value::String("local".to_string())));
assert_eq!(
map.get("path"),
Some(&Value::String("/my/path".to_string()))
);
}
_ => panic!("expected map"),
}
}
#[test]
fn read_nonexistent_mount_config() {
let mut store = MountStore::new(TestFactory);
let result = store.read(&path!("ctx/mounts/nonexistent")).unwrap();
assert!(result.is_none());
}
#[test]
fn write_directly_to_mounts_fails() {
let mut store = MountStore::new(TestFactory);
let result = store.write(&path!("ctx/mounts"), Record::parsed(Value::Null));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Cannot write"));
}
#[test]
fn value_to_config_unknown_type_fails() {
let mut map = BTreeMap::new();
map.insert("type".to_string(), Value::String("unknown".to_string()));
let result = value_to_config(&Value::Map(map));
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Unknown mount type"));
}
#[test]
fn value_to_config_non_map_fails() {
let result = value_to_config(&Value::String("not a map".to_string()));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("must be a map"));
}
#[test]
fn value_to_config_missing_type_fails() {
let map = BTreeMap::new();
let result = value_to_config(&Value::Map(map));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Missing 'type'"));
}
#[test]
fn value_to_config_type_not_string_fails() {
let mut map = BTreeMap::new();
map.insert("type".to_string(), Value::Integer(123));
let result = value_to_config(&Value::Map(map));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Missing 'type'"));
}
#[test]
fn value_to_config_local_missing_path_fails() {
let mut map = BTreeMap::new();
map.insert("type".to_string(), Value::String("local".to_string()));
let result = value_to_config(&Value::Map(map));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Missing 'path'"));
}
#[test]
fn value_to_config_http_missing_url_fails() {
let mut map = BTreeMap::new();
map.insert("type".to_string(), Value::String("http".to_string()));
let result = value_to_config(&Value::Map(map));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Missing 'url'"));
}
#[test]
fn value_to_config_structfs_missing_url_fails() {
let mut map = BTreeMap::new();
map.insert("type".to_string(), Value::String("structfs".to_string()));
let result = value_to_config(&Value::Map(map));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Missing 'url'"));
}
struct FailingFactory;
impl StoreFactory for FailingFactory {
fn create(&self, _config: &MountConfig) -> Result<StoreBox, Error> {
Err(Error::store("factory", "create", "Factory failed"))
}
}
#[test]
fn mount_with_failing_factory() {
let mut store = MountStore::new(FailingFactory);
let result = store.mount("data", MountConfig::Memory);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Factory failed"));
}
#[test]
fn mount_info_serialization() {
let info = MountInfo {
path: "/test".to_string(),
config: MountConfig::Memory,
};
let debug = format!("{:?}", info);
assert!(debug.contains("/test"));
assert!(debug.contains("Memory"));
let cloned = info.clone();
assert_eq!(cloned.path, "/test");
}
#[test]
fn mount_config_debug_clone() {
let config = MountConfig::Http {
url: "https://test.com".to_string(),
};
let debug = format!("{:?}", config);
assert!(debug.contains("https://test.com"));
let cloned = config.clone();
assert_eq!(cloned, config);
}
#[test]
fn nested_mount_path() {
let mut store = MountStore::new(TestFactory);
let config = config_to_value(&MountConfig::Memory);
store
.write(&path!("ctx/mounts/nested/path"), Record::parsed(config))
.unwrap();
let mounts = store.list_mounts();
assert_eq!(mounts.len(), 1);
assert_eq!(mounts[0].path, "nested/path");
}
#[test]
fn delegate_to_overlay_read() {
let mut store = MountStore::new(TestFactory);
let result = store.read(&path!("unmounted/path"));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("no route"));
}
#[test]
fn is_mounts_path_variations() {
assert!(MountStore::<TestFactory>::is_mounts_path(&path!(
"ctx/mounts"
)));
assert!(MountStore::<TestFactory>::is_mounts_path(&path!(
"ctx/mounts/foo"
)));
assert!(MountStore::<TestFactory>::is_mounts_path(&path!(
"ctx/mounts/foo/bar"
)));
assert!(!MountStore::<TestFactory>::is_mounts_path(&path!("ctx")));
assert!(!MountStore::<TestFactory>::is_mounts_path(&path!(
"ctx/other"
)));
assert!(!MountStore::<TestFactory>::is_mounts_path(&path!("other")));
}
#[test]
fn get_mount_name_variations() {
assert_eq!(
MountStore::<TestFactory>::get_mount_name(&path!("ctx/mounts/foo")),
Some("foo".to_string())
);
assert_eq!(
MountStore::<TestFactory>::get_mount_name(&path!("ctx/mounts/foo/bar")),
Some("foo/bar".to_string())
);
assert_eq!(
MountStore::<TestFactory>::get_mount_name(&path!("ctx/mounts")),
None
);
assert_eq!(
MountStore::<TestFactory>::get_mount_name(&path!("ctx")),
None
);
}
#[test]
fn unmount_removes_from_overlay() {
let mut store = MountStore::new(TestFactory);
store.mount("data", MountConfig::Memory).unwrap();
store
.write(&path!("data/key"), Record::parsed(Value::Integer(42)))
.unwrap();
let result = store.read(&path!("data/key")).unwrap();
assert!(result.is_some());
store.unmount("data").unwrap();
let result = store.read(&path!("data/key"));
assert!(result.is_err());
}
#[test]
fn unmount_allows_remount() {
let mut store = MountStore::new(TestFactory);
store.mount("data", MountConfig::Memory).unwrap();
store
.write(&path!("data/key"), Record::parsed(Value::Integer(1)))
.unwrap();
store.unmount("data").unwrap();
store.mount("data", MountConfig::Memory).unwrap();
let result = store.read(&path!("data/key")).unwrap();
assert!(result.is_none());
}
#[test]
fn unmount_priority_preserved() {
let mut store = MountStore::new(TestFactory);
store.mount("data", MountConfig::Memory).unwrap();
store.mount("data/nested", MountConfig::Memory).unwrap();
store
.write(&path!("data/nested/key"), Record::parsed(Value::Integer(1)))
.unwrap();
store.unmount("data/nested").unwrap();
store
.write(&path!("data/other"), Record::parsed(Value::Integer(2)))
.unwrap();
let result = store.read(&path!("data/other")).unwrap();
assert!(result.is_some());
}
}