use super::{
PrompterError, collect_profiles, library_dir, load_config_bundle, resolve_config_path,
};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct ProfileDef {
pub(crate) deps: Vec<String>,
pub(crate) library_root: PathBuf,
}
#[derive(Debug)]
pub struct Config {
pub(crate) profiles: HashMap<String, ProfileDef>,
pub(crate) post_prompt: Option<String>,
}
impl Config {
#[must_use]
pub fn library_roots(&self) -> Vec<PathBuf> {
let mut roots: Vec<PathBuf> = Vec::new();
for def in self.profiles.values() {
if !roots.contains(&def.library_root) {
roots.push(def.library_root.clone());
}
}
roots.sort();
roots
}
}
pub(crate) fn load_bundle(
config_override: Option<&Path>,
) -> Result<(PathBuf, Config), PrompterError> {
let cfg_path = resolve_config_path(config_override)?;
let default_library = if config_override.is_some() {
None
} else {
Some(library_dir()?)
};
let cfg = load_config_bundle(&cfg_path, default_library.as_deref())?;
Ok((cfg_path, cfg))
}
#[derive(Debug)]
pub(crate) struct RawConfigFile {
pub(crate) profiles: HashMap<String, Vec<String>>,
pub(crate) imports: Vec<String>,
pub(crate) library: Option<String>,
pub(crate) post_prompt: Option<String>,
}
pub(crate) fn parse_config_file(input: &str) -> Result<RawConfigFile, PrompterError> {
let table: toml::Table = toml::from_str(input)?;
let mut raw = RawConfigFile {
profiles: HashMap::new(),
imports: Vec::new(),
library: None,
post_prompt: None,
};
for (key, value) in &table {
match key.as_str() {
"import" => {
let arr = value.as_array().ok_or_else(|| {
PrompterError::ConfigField("`import` must be an array of strings".to_string())
})?;
for entry in arr {
let s = entry.as_str().ok_or_else(|| {
PrompterError::ConfigField("`import` entries must be strings".to_string())
})?;
raw.imports.push(s.to_string());
}
}
"library" => {
let s = value.as_str().ok_or_else(|| {
PrompterError::ConfigField("`library` must be a string".to_string())
})?;
raw.library = Some(s.to_string());
}
"post_prompt" => {
let s = value.as_str().ok_or_else(|| {
PrompterError::ConfigField("`post_prompt` must be a string".to_string())
})?;
raw.post_prompt = Some(s.to_string());
}
_ => collect_profiles(key, value, &mut raw.profiles)?,
}
}
Ok(raw)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_config_rejects_non_string_import_entry() {
let err = parse_config_file("import = [123]\n").unwrap_err();
assert!(matches!(err, PrompterError::ConfigField(_)), "err={err}");
assert!(
err.to_string().contains("`import` entries must be strings"),
"err={err}"
);
}
#[test]
fn parse_config_rejects_non_string_library() {
let err = parse_config_file("library = 123\n").unwrap_err();
assert!(matches!(err, PrompterError::ConfigField(_)), "err={err}");
assert!(
err.to_string().contains("`library` must be a string"),
"err={err}"
);
}
#[test]
fn parse_config_rejects_non_string_post_prompt() {
let err = parse_config_file("post_prompt = true\n").unwrap_err();
assert!(matches!(err, PrompterError::ConfigField(_)), "err={err}");
assert!(
err.to_string().contains("`post_prompt` must be a string"),
"err={err}"
);
}
}