use std::error::Error;
use std::path::PathBuf;
use std::str::FromStr;
use topodb::{IndexSpec, Scope, ScopeId};
pub use topodb_json::{ENTITY_LABEL, ENTITY_NAME_PROP, MEMORY_CONTENT_PROP, MEMORY_LABEL};
#[derive(Debug, Clone)]
pub struct Config {
pub db_path: PathBuf,
pub default_scope: Scope,
pub spec: IndexSpec,
}
pub use topodb_json::default_spec;
pub use topodb_json::scope_label;
fn parse_scope(s: &str) -> Result<Scope, Box<dyn Error>> {
if s.eq_ignore_ascii_case("shared") {
Ok(Scope::Shared)
} else {
let id = ScopeId::from_str(s).map_err(|e| {
format!("invalid --scope value {s:?} (expected \"shared\" or a ULID): {e}")
})?;
Ok(Scope::Id(id))
}
}
impl Config {
pub fn from_args<I>(args: I) -> Result<Self, Box<dyn Error>>
where
I: IntoIterator<Item = String>,
{
let mut db_path: Option<PathBuf> = None;
let mut scope: Option<String> = None;
let mut spec_path: Option<PathBuf> = None;
let mut it = args.into_iter();
while let Some(arg) = it.next() {
match arg.as_str() {
"--db" => {
db_path = Some(it.next().ok_or("--db requires a <path> value")?.into());
}
"--scope" => {
scope = Some(it.next().ok_or("--scope requires a <ulid|shared> value")?);
}
"--spec" => {
spec_path = Some(
it.next()
.ok_or("--spec requires a <spec.json> value")?
.into(),
);
}
other => {
return Err(format!(
"unknown argument {other:?}; usage: topodb-mcp --db <path> [--scope <ulid|shared>] [--spec <spec.json>]"
)
.into());
}
}
}
let db_path = db_path.ok_or("missing required --db <path>")?;
let default_scope = match scope {
Some(s) => parse_scope(&s)?,
None => Scope::Shared,
};
let spec = match spec_path {
Some(p) => {
let text = std::fs::read_to_string(&p)
.map_err(|e| format!("reading --spec {}: {e}", p.display()))?;
serde_json::from_str(&text)
.map_err(|e| format!("parsing --spec {} as IndexSpec JSON: {e}", p.display()))?
}
None => default_spec(),
};
Ok(Config {
db_path,
default_scope,
spec,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[test]
fn defaults_scope_shared_and_builtin_spec() {
let cfg = Config::from_args(argv(&["--db", "t.redb"])).unwrap();
assert_eq!(cfg.db_path, PathBuf::from("t.redb"));
assert!(matches!(cfg.default_scope, Scope::Shared));
assert_eq!(cfg.spec, default_spec());
}
#[test]
fn scope_shared_is_case_insensitive() {
let cfg = Config::from_args(argv(&["--db", "t.redb", "--scope", "SHARED"])).unwrap();
assert!(matches!(cfg.default_scope, Scope::Shared));
}
#[test]
fn scope_ulid_parses_to_id_and_round_trips_label() {
let id = ScopeId::new();
let s = id.to_string();
let cfg = Config::from_args(argv(&["--db", "t.redb", "--scope", &s])).unwrap();
match cfg.default_scope {
Scope::Id(got) => assert_eq!(got, id),
other => panic!("expected Scope::Id, got {other:?}"),
}
assert_eq!(scope_label(&cfg.default_scope), s);
}
#[test]
fn scope_label_shared() {
assert_eq!(scope_label(&Scope::Shared), "shared");
}
#[test]
fn bad_scope_is_rejected() {
assert!(Config::from_args(argv(&["--db", "t.redb", "--scope", "not-a-ulid"])).is_err());
}
#[test]
fn missing_db_is_rejected() {
assert!(Config::from_args(argv(&["--scope", "shared"])).is_err());
}
#[test]
fn unknown_flag_is_rejected() {
assert!(Config::from_args(argv(&["--db", "t.redb", "--nope"])).is_err());
}
#[test]
fn missing_value_is_rejected() {
assert!(Config::from_args(argv(&["--db"])).is_err());
}
}