use std::env;
use std::path::PathBuf;
use crate::error::MiniAppError;
#[derive(Debug, Clone)]
pub struct Config {
pub schema_path: Option<PathBuf>,
pub db_path: Option<PathBuf>,
pub user_dir: Option<PathBuf>,
pub project_dir: Option<PathBuf>,
pub backup_retention: Option<usize>,
pub snapshot_retention: Option<usize>,
}
impl Config {
pub fn load() -> Result<Self, MiniAppError> {
if let Err(e) = dotenvy::from_filename(".mini-app-mcp.env") {
tracing::debug!(error = %e, "no .mini-app-mcp.env file found, relying on process env");
}
let schema_path = env::var("MINI_APP_SCHEMA").ok().map(PathBuf::from);
let db_path = env::var("MINI_APP_DB").ok().map(PathBuf::from);
let user_dir = match env::var("MINI_APP_USER_DIR") {
Ok(v) => Some(PathBuf::from(v)),
Err(_) => dirs::home_dir().map(|h| h.join(".mini-app")),
};
let project_dir = match env::var("MINI_APP_PROJECT_DIR") {
Ok(v) => Some(PathBuf::from(v)),
Err(_) => Some(PathBuf::from(".mini-app")),
};
let backup_retention = env::var("MINI_APP_BACKUP_RETENTION")
.ok()
.and_then(|v| v.parse::<usize>().ok());
let snapshot_retention = env::var("MINI_APP_SNAPSHOT_RETENTION")
.ok()
.and_then(|v| v.parse::<usize>().ok());
Ok(Config {
schema_path,
db_path,
user_dir,
project_dir,
backup_retention,
snapshot_retention,
})
}
pub fn has_legacy_env(&self) -> bool {
self.schema_path.is_some() && self.db_path.is_some()
}
pub fn backup_retention(&self) -> usize {
self.backup_retention.unwrap_or(10)
}
pub fn snapshot_retention(&self) -> usize {
self.snapshot_retention.unwrap_or(10)
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_env<F: FnOnce()>(vars: &[(&str, Option<&str>)], f: F) {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
let saved: Vec<(&str, Option<String>)> =
vars.iter().map(|(k, _)| (*k, env::var(*k).ok())).collect();
for (k, v) in vars {
match v {
Some(val) => unsafe { env::set_var(k, val) },
None => unsafe { env::remove_var(k) },
}
}
f();
for (k, original) in &saved {
match original {
Some(val) => unsafe { env::set_var(k, val) },
None => unsafe { env::remove_var(k) },
}
}
}
#[test]
fn load_success_legacy_vars() {
with_env(
&[
("MINI_APP_SCHEMA", Some("./schema.yaml")),
("MINI_APP_DB", Some("./mini-app.db")),
("MINI_APP_USER_DIR", None),
("MINI_APP_PROJECT_DIR", None),
],
|| {
let cfg = Config::load().expect("should succeed with legacy vars set");
assert_eq!(cfg.schema_path, Some(PathBuf::from("./schema.yaml")));
assert_eq!(cfg.db_path, Some(PathBuf::from("./mini-app.db")));
assert!(cfg.has_legacy_env());
},
);
}
#[test]
fn load_success_dir_env_vars() {
with_env(
&[
("MINI_APP_SCHEMA", None),
("MINI_APP_DB", None),
("MINI_APP_USER_DIR", Some("/tmp/user-tables")),
("MINI_APP_PROJECT_DIR", Some("/tmp/project-tables")),
],
|| {
let cfg = Config::load().expect("should succeed with dir vars set");
assert_eq!(cfg.schema_path, None);
assert_eq!(cfg.db_path, None);
assert_eq!(cfg.user_dir, Some(PathBuf::from("/tmp/user-tables")));
assert_eq!(cfg.project_dir, Some(PathBuf::from("/tmp/project-tables")));
assert!(!cfg.has_legacy_env());
},
);
}
#[test]
fn load_success_both_legacy_and_dir_vars() {
with_env(
&[
("MINI_APP_SCHEMA", Some("./schema.yaml")),
("MINI_APP_DB", Some("./mini-app.db")),
("MINI_APP_USER_DIR", Some("/tmp/user-tables")),
("MINI_APP_PROJECT_DIR", Some("/tmp/project-tables")),
],
|| {
let cfg = Config::load().expect("should succeed with all vars set");
assert_eq!(cfg.schema_path, Some(PathBuf::from("./schema.yaml")));
assert_eq!(cfg.db_path, Some(PathBuf::from("./mini-app.db")));
assert_eq!(cfg.user_dir, Some(PathBuf::from("/tmp/user-tables")));
assert_eq!(cfg.project_dir, Some(PathBuf::from("/tmp/project-tables")));
assert!(cfg.has_legacy_env());
},
);
}
#[test]
fn load_no_env_vars_succeeds() {
with_env(
&[
("MINI_APP_SCHEMA", None),
("MINI_APP_DB", None),
("MINI_APP_USER_DIR", None),
("MINI_APP_PROJECT_DIR", None),
],
|| {
let cfg = Config::load().expect("load must not fail with no env vars");
assert_eq!(cfg.schema_path, None);
assert_eq!(cfg.db_path, None);
assert!(!cfg.has_legacy_env());
assert_eq!(cfg.project_dir, Some(PathBuf::from(".mini-app")));
},
);
}
#[test]
fn load_only_schema_env_has_legacy_env_false() {
with_env(
&[
("MINI_APP_SCHEMA", Some("./schema.yaml")),
("MINI_APP_DB", None),
],
|| {
let cfg = Config::load().expect("load must not fail");
assert_eq!(cfg.schema_path, Some(PathBuf::from("./schema.yaml")));
assert_eq!(cfg.db_path, None);
assert!(!cfg.has_legacy_env());
},
);
}
#[test]
fn load_does_not_panic_with_no_env_file() {
with_env(
&[
("MINI_APP_SCHEMA", None),
("MINI_APP_DB", None),
("MINI_APP_USER_DIR", None),
("MINI_APP_PROJECT_DIR", None),
],
|| {
let result = Config::load();
assert!(result.is_ok());
},
);
}
#[test]
fn load_success() {
with_env(
&[
("MINI_APP_SCHEMA", Some("./schema.yaml")),
("MINI_APP_DB", Some("./mini-app.db")),
],
|| {
let cfg = Config::load().expect("should succeed with both vars set");
assert_eq!(cfg.schema_path, Some(PathBuf::from("./schema.yaml")));
assert_eq!(cfg.db_path, Some(PathBuf::from("./mini-app.db")));
},
);
}
#[test]
fn load_missing_schema_returns_none() {
with_env(
&[
("MINI_APP_SCHEMA", None),
("MINI_APP_DB", Some("./mini-app.db")),
],
|| {
let cfg =
Config::load().expect("load must succeed even with MINI_APP_SCHEMA absent");
assert_eq!(cfg.schema_path, None);
assert!(!cfg.has_legacy_env());
},
);
}
#[test]
fn load_missing_db_returns_none() {
with_env(
&[
("MINI_APP_SCHEMA", Some("./schema.yaml")),
("MINI_APP_DB", None),
],
|| {
let cfg = Config::load().expect("load must succeed even with MINI_APP_DB absent");
assert_eq!(cfg.db_path, None);
assert!(!cfg.has_legacy_env());
},
);
}
#[test]
fn backup_retention_defaults_to_10() {
with_env(&[("MINI_APP_BACKUP_RETENTION", None)], || {
let cfg = Config::load().expect("load must succeed");
assert_eq!(cfg.backup_retention(), 10);
assert_eq!(cfg.backup_retention, None);
});
}
#[test]
fn backup_retention_reads_from_env() {
with_env(&[("MINI_APP_BACKUP_RETENTION", Some("5"))], || {
let cfg = Config::load().expect("load must succeed");
assert_eq!(cfg.backup_retention, Some(5));
assert_eq!(cfg.backup_retention(), 5);
});
}
#[test]
fn backup_retention_non_parsable_env_falls_back_to_default() {
with_env(
&[("MINI_APP_BACKUP_RETENTION", Some("not-a-number"))],
|| {
let cfg = Config::load().expect("load must succeed even with bad retention value");
assert_eq!(cfg.backup_retention, None);
assert_eq!(cfg.backup_retention(), 10);
},
);
}
#[test]
fn backup_retention_getter_never_zero() {
let cfg = Config {
schema_path: None,
db_path: None,
user_dir: None,
project_dir: None,
backup_retention: None,
snapshot_retention: None,
};
assert!(cfg.backup_retention() >= 1);
}
#[test]
fn snapshot_retention_defaults_to_10() {
with_env(&[("MINI_APP_SNAPSHOT_RETENTION", None)], || {
let cfg = Config::load().expect("load must succeed");
assert_eq!(cfg.snapshot_retention(), 10);
assert_eq!(cfg.snapshot_retention, None);
});
}
#[test]
fn snapshot_retention_reads_from_env() {
with_env(&[("MINI_APP_SNAPSHOT_RETENTION", Some("5"))], || {
let cfg = Config::load().expect("load must succeed");
assert_eq!(cfg.snapshot_retention, Some(5));
assert_eq!(cfg.snapshot_retention(), 5);
});
}
#[test]
fn snapshot_retention_non_parsable_env_falls_back_to_default() {
with_env(
&[("MINI_APP_SNAPSHOT_RETENTION", Some("not-a-number"))],
|| {
let cfg = Config::load().expect("load must succeed even with bad retention value");
assert_eq!(cfg.snapshot_retention, None);
assert_eq!(cfg.snapshot_retention(), 10);
},
);
}
#[test]
fn snapshot_retention_getter_never_zero() {
let cfg = Config {
schema_path: None,
db_path: None,
user_dir: None,
project_dir: None,
backup_retention: None,
snapshot_retention: None,
};
assert!(cfg.snapshot_retention() >= 1);
}
#[test]
fn snapshot_retention_independent_from_backup_retention() {
with_env(
&[
("MINI_APP_BACKUP_RETENTION", Some("3")),
("MINI_APP_SNAPSHOT_RETENTION", Some("7")),
],
|| {
let cfg = Config::load().expect("load must succeed");
assert_eq!(cfg.backup_retention(), 3);
assert_eq!(cfg.snapshot_retention(), 7);
assert_ne!(cfg.backup_retention(), cfg.snapshot_retention());
},
);
}
}