mod migrate;
mod v1;
mod validate;
use std::path::{Path, PathBuf};
use thiserror::Error;
pub use v1::ConfigV1;
pub type Config = ConfigV1;
pub const CONFIG_FILE_NAME: &str = "basemind.toml";
pub const BASEMIND_DIR: &str = ".basemind";
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("config file not found at {0}")]
NotFound(PathBuf),
#[error("failed to read {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("invalid TOML in {path}: {source}")]
Toml {
path: PathBuf,
#[source]
source: toml::de::Error,
},
#[error("config is missing required \"$schema\" field — add `\"$schema\" = \"v1\"`")]
MissingSchema,
#[error("unknown schema version {0:?} — supported: v1")]
UnknownSchema(String),
#[error("schema validation failed:\n{0}")]
SchemaValidation(String),
#[error("config does not match v1 shape after validation: {0}")]
Deserialize(#[source] serde_json::Error),
}
pub fn load(root: &Path) -> Result<Config, ConfigError> {
let path = config_path(root);
if !path.exists() {
return Err(ConfigError::NotFound(path));
}
let raw = std::fs::read_to_string(&path).map_err(|source| ConfigError::Io {
path: path.clone(),
source,
})?;
parse_str(&raw).map_err(|e| annotate_path(e, &path))
}
pub fn config_path(root: &Path) -> PathBuf {
root.join(BASEMIND_DIR).join(CONFIG_FILE_NAME)
}
pub fn parse_str(raw: &str) -> Result<Config, ConfigError> {
let toml_value: toml::Value = toml::from_str(raw).map_err(|source| ConfigError::Toml {
path: PathBuf::new(),
source,
})?;
let json_value: serde_json::Value =
serde_json::to_value(&toml_value).expect("toml::Value → serde_json::Value never fails");
let schema_tag = json_value
.as_object()
.and_then(|o| o.get("$schema"))
.and_then(|v| v.as_str())
.ok_or(ConfigError::MissingSchema)?;
match schema_tag {
"v1" | "https://basemind.dev/schema/v1.json" => {
validate::validate_v1(&json_value)?;
serde_json::from_value::<ConfigV1>(json_value).map_err(ConfigError::Deserialize)
}
other => Err(ConfigError::UnknownSchema(other.to_string())),
}
}
pub fn default_for_root(_root: &Path) -> Config {
ConfigV1::with_defaults()
}
fn annotate_path(err: ConfigError, path: &Path) -> ConfigError {
match err {
ConfigError::Toml { source, .. } => ConfigError::Toml {
path: path.to_path_buf(),
source,
},
other => other,
}
}