Skip to main content

mempal_runtime/core/
config.rs

1use std::env;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8const DEFAULT_DB_PATH: &str = "~/.mempal/palace.db";
9const DEFAULT_EMBED_BACKEND: &str = "model2vec";
10
11#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
12#[serde(default)]
13pub struct Config {
14    pub db_path: String,
15    pub embed: EmbedConfig,
16    pub context: ContextConfig,
17}
18
19impl Default for Config {
20    fn default() -> Self {
21        Self {
22            db_path: DEFAULT_DB_PATH.to_string(),
23            embed: EmbedConfig::default(),
24            context: ContextConfig::default(),
25        }
26    }
27}
28
29impl Config {
30    pub fn load() -> Result<Self, ConfigError> {
31        Self::load_from(&default_config_path())
32    }
33
34    pub fn load_from(path: &Path) -> Result<Self, ConfigError> {
35        match fs::read_to_string(path) {
36            Ok(contents) => Ok(toml::from_str(&contents)?),
37            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
38            Err(source) => Err(ConfigError::Read {
39                path: path.to_path_buf(),
40                source,
41            }),
42        }
43    }
44
45    pub fn save_to(&self, path: &Path) -> Result<(), ConfigError> {
46        if let Some(parent) = path.parent() {
47            fs::create_dir_all(parent).map_err(|source| ConfigError::Write {
48                path: parent.to_path_buf(),
49                source,
50            })?;
51        }
52        let contents = toml::to_string_pretty(self)?;
53        fs::write(path, contents).map_err(|source| ConfigError::Write {
54            path: path.to_path_buf(),
55            source,
56        })
57    }
58
59    pub fn save_default(&self) -> Result<(), ConfigError> {
60        self.save_to(&default_config_path())
61    }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
65#[serde(default)]
66pub struct EmbedConfig {
67    pub backend: String,
68    /// Model identifier (e.g., "minishlab/potion-multilingual-128M" for model2vec).
69    pub model: Option<String>,
70    pub api_endpoint: Option<String>,
71    pub api_model: Option<String>,
72    pub dimensions: Option<usize>,
73}
74
75impl Default for EmbedConfig {
76    fn default() -> Self {
77        Self {
78            backend: DEFAULT_EMBED_BACKEND.to_string(),
79            model: None,
80            api_endpoint: None,
81            api_model: None,
82            dimensions: None,
83        }
84    }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
88#[serde(default)]
89pub struct ContextConfig {
90    pub include_cards_default: bool,
91}
92
93#[derive(Debug, Error)]
94pub enum ConfigError {
95    #[error("failed to read config from {path}")]
96    Read {
97        path: PathBuf,
98        #[source]
99        source: std::io::Error,
100    },
101    #[error("failed to write config to {path}")]
102    Write {
103        path: PathBuf,
104        #[source]
105        source: std::io::Error,
106    },
107    #[error("failed to parse config TOML")]
108    Parse(#[from] toml::de::Error),
109    #[error("failed to serialize config TOML")]
110    Serialize(#[from] toml::ser::Error),
111}
112
113fn default_config_path() -> PathBuf {
114    env::var_os("HOME")
115        .map(PathBuf::from)
116        .map(|home| home.join(".mempal").join("config.toml"))
117        .unwrap_or_else(|| PathBuf::from("~/.mempal/config.toml"))
118}
119
120#[cfg(test)]
121mod tests {
122    use super::Config;
123
124    #[test]
125    fn parses_api_embedding_dimensions() {
126        let config: Config = toml::from_str(
127            r#"
128[embed]
129backend = "api"
130api_endpoint = "http://localhost:11434/api/embeddings"
131api_model = "nomic-embed-text"
132dimensions = 768
133"#,
134        )
135        .expect("parse config");
136
137        assert_eq!(config.embed.dimensions, Some(768));
138    }
139}