tftio_lib/prompt/
config.rs1use super::{
3 PrompterError, collect_profiles, library_dir, load_config_bundle, resolve_config_path,
4};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone)]
10pub struct ProfileDef {
11 pub(crate) deps: Vec<String>,
13 pub(crate) library_root: PathBuf,
15}
16
17#[derive(Debug)]
24pub struct Config {
25 pub(crate) profiles: HashMap<String, ProfileDef>,
27 pub(crate) post_prompt: Option<String>,
29}
30
31impl Config {
32 #[must_use]
35 pub fn library_roots(&self) -> Vec<PathBuf> {
36 let mut roots: Vec<PathBuf> = Vec::new();
37 for def in self.profiles.values() {
38 if !roots.contains(&def.library_root) {
39 roots.push(def.library_root.clone());
40 }
41 }
42 roots.sort();
43 roots
44 }
45}
46
47pub(crate) fn load_bundle(
48 config_override: Option<&Path>,
49) -> Result<(PathBuf, Config), PrompterError> {
50 let cfg_path = resolve_config_path(config_override)?;
51 let default_library = if config_override.is_some() {
52 None
53 } else {
54 Some(library_dir()?)
55 };
56 let cfg = load_config_bundle(&cfg_path, default_library.as_deref())?;
57 Ok((cfg_path, cfg))
58}
59
60#[derive(Debug)]
66pub(crate) struct RawConfigFile {
67 pub(crate) profiles: HashMap<String, Vec<String>>,
69 pub(crate) imports: Vec<String>,
71 pub(crate) library: Option<String>,
74 pub(crate) post_prompt: Option<String>,
76}
77
78pub(crate) fn parse_config_file(input: &str) -> Result<RawConfigFile, PrompterError> {
90 let table: toml::Table = toml::from_str(input)?;
91
92 let mut raw = RawConfigFile {
93 profiles: HashMap::new(),
94 imports: Vec::new(),
95 library: None,
96 post_prompt: None,
97 };
98
99 for (key, value) in &table {
100 match key.as_str() {
101 "import" => {
102 let arr = value.as_array().ok_or_else(|| {
103 PrompterError::ConfigField("`import` must be an array of strings".to_string())
104 })?;
105 for entry in arr {
106 let s = entry.as_str().ok_or_else(|| {
107 PrompterError::ConfigField("`import` entries must be strings".to_string())
108 })?;
109 raw.imports.push(s.to_string());
110 }
111 }
112 "library" => {
113 let s = value.as_str().ok_or_else(|| {
114 PrompterError::ConfigField("`library` must be a string".to_string())
115 })?;
116 raw.library = Some(s.to_string());
117 }
118 "post_prompt" => {
119 let s = value.as_str().ok_or_else(|| {
120 PrompterError::ConfigField("`post_prompt` must be a string".to_string())
121 })?;
122 raw.post_prompt = Some(s.to_string());
123 }
124 _ => collect_profiles(key, value, &mut raw.profiles)?,
125 }
126 }
127
128 Ok(raw)
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn parse_config_rejects_non_string_import_entry() {
137 let err = parse_config_file("import = [123]\n").unwrap_err();
140 assert!(matches!(err, PrompterError::ConfigField(_)), "err={err}");
141 assert!(
142 err.to_string().contains("`import` entries must be strings"),
143 "err={err}"
144 );
145 }
146
147 #[test]
148 fn parse_config_rejects_non_string_library() {
149 let err = parse_config_file("library = 123\n").unwrap_err();
151 assert!(matches!(err, PrompterError::ConfigField(_)), "err={err}");
152 assert!(
153 err.to_string().contains("`library` must be a string"),
154 "err={err}"
155 );
156 }
157
158 #[test]
159 fn parse_config_rejects_non_string_post_prompt() {
160 let err = parse_config_file("post_prompt = true\n").unwrap_err();
162 assert!(matches!(err, PrompterError::ConfigField(_)), "err={err}");
163 assert!(
164 err.to_string().contains("`post_prompt` must be a string"),
165 "err={err}"
166 );
167 }
168}