use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
}
pub const DEFAULT_DB_FILE_NAME: &str = "aimem.db";
pub fn default_db_path() -> PathBuf {
dirs_next().join(DEFAULT_DB_FILE_NAME)
}
pub fn default_identity_path() -> PathBuf {
dirs_next().join("identity.txt")
}
fn dirs_next() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".aimem")
}
fn resolve_db_override(raw: &str) -> PathBuf {
let path = PathBuf::from(shellexpand(raw));
if path.extension().is_some() {
path
} else {
path.join(DEFAULT_DB_FILE_NAME)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub db_path: PathBuf,
pub identity_path: PathBuf,
#[serde(default = "default_collection_name")]
pub collection_name: String,
#[serde(default = "default_topic_wings")]
pub topic_wings: Vec<String>,
}
fn default_collection_name() -> String {
"aimem_drawers".to_string()
}
fn default_topic_wings() -> Vec<String> {
vec![
"emotions".into(),
"consciousness".into(),
"memory".into(),
"technical".into(),
"identity".into(),
"family".into(),
"creative".into(),
]
}
impl Default for Config {
fn default() -> Self {
Self {
db_path: default_db_path(),
identity_path: default_identity_path(),
collection_name: default_collection_name(),
topic_wings: default_topic_wings(),
}
}
}
impl Config {
pub fn load() -> Result<Self, ConfigError> {
let mut cfg = Self::default();
let config_file = dirs_next().join("config.json");
if config_file.exists() {
let raw = std::fs::read_to_string(&config_file)?;
let file_cfg: serde_json::Value = serde_json::from_str(&raw)?;
if let Some(p) = file_cfg["db_path"].as_str() {
cfg.db_path = PathBuf::from(shellexpand(p));
}
if let Some(p) = file_cfg["identity_path"].as_str() {
cfg.identity_path = PathBuf::from(shellexpand(p));
}
if let Some(n) = file_cfg["collection_name"].as_str() {
cfg.collection_name = n.to_string();
}
}
if let Ok(p) = std::env::var("AIMEM_DB_PATH") {
cfg.db_path = resolve_db_override(&p);
}
if let Ok(p) = std::env::var("AIMEM_IDENTITY_PATH") {
cfg.identity_path = PathBuf::from(shellexpand(&p));
}
Ok(cfg)
}
pub fn init(&self) -> Result<(), ConfigError> {
let dir = dirs_next();
std::fs::create_dir_all(&dir)?;
let config_file = dir.join("config.json");
if !config_file.exists() {
let json = serde_json::to_string_pretty(self)?;
std::fs::write(&config_file, json)?;
}
Ok(())
}
}
fn shellexpand(s: &str) -> String {
if s.starts_with("~/") {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
format!("{}/{}", home, &s[2..])
} else {
s.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_db_path_uses_aimem_db_name() {
let resolved = default_db_path();
assert_eq!(
resolved.file_name().and_then(|name| name.to_str()),
Some(DEFAULT_DB_FILE_NAME)
);
}
#[test]
fn db_override_uses_aimem_db_for_directory_inputs() {
let dir = tempfile::tempdir().expect("failed to create temp dir");
let raw = dir.path().to_string_lossy();
assert_eq!(
resolve_db_override(&raw),
dir.path().join(DEFAULT_DB_FILE_NAME)
);
}
}