use std::collections::BTreeSet;
use std::env;
use std::path::Path;
use toml::Value;
use super::{ConfigError, LlmConfig};
pub(super) fn disabled_provider_indices(tree: &Value) -> BTreeSet<usize> {
let default_enabled = LlmConfig::default().enabled;
tree.get("llm")
.and_then(Value::as_array)
.map(|entries| {
entries
.iter()
.enumerate()
.filter(|(_, entry)| {
!entry
.get("enabled")
.and_then(Value::as_bool)
.unwrap_or(default_enabled)
})
.map(|(index, _)| index)
.collect()
})
.unwrap_or_default()
}
pub(super) fn expand_env_except(
tree: &mut Value,
source: &Path,
skip: &BTreeSet<usize>,
) -> Result<(), ConfigError> {
if skip.is_empty() {
return expand_env_in(tree, source);
}
let Some(table) = tree.as_table_mut() else {
return expand_env_in(tree, source);
};
for (key, value) in table.iter_mut() {
if key != "llm" {
expand_env_in(value, source)?;
continue;
}
let Some(entries) = value.as_array_mut() else {
expand_env_in(value, source)?;
continue;
};
for (index, entry) in entries.iter_mut().enumerate() {
if !skip.contains(&index) {
expand_env_in(entry, source)?;
}
}
}
Ok(())
}
pub(super) fn expand_env_in(value: &mut Value, source: &Path) -> Result<(), ConfigError> {
match value {
Value::String(s) => {
*s = expand_string(s, source)?;
}
Value::Table(table) => {
for (_, inner) in table.iter_mut() {
expand_env_in(inner, source)?;
}
}
Value::Array(items) => {
for inner in items.iter_mut() {
expand_env_in(inner, source)?;
}
}
_ => {}
}
Ok(())
}
pub fn env_var_refs(s: &str) -> Vec<String> {
let mut refs = Vec::new();
let mut rest = s;
while let Some((_, after_open)) = rest.split_once("${") {
let Some((name, after_close)) = after_open.split_once('}') else {
break;
};
refs.push(name.to_owned());
rest = after_close;
}
refs
}
pub fn required_env_var_refs(value: &Value) -> Vec<String> {
let disabled = disabled_provider_indices(value);
if disabled.is_empty() {
return env_var_refs_in(value);
}
let mut seen = BTreeSet::new();
let mut out = Vec::new();
let Some(table) = value.as_table() else {
return env_var_refs_in(value);
};
for (key, inner) in table {
if key != "llm" {
collect_env_refs(inner, &mut seen, &mut out);
continue;
}
let Some(entries) = inner.as_array() else {
collect_env_refs(inner, &mut seen, &mut out);
continue;
};
for (index, entry) in entries.iter().enumerate() {
if !disabled.contains(&index) {
collect_env_refs(entry, &mut seen, &mut out);
}
}
}
out
}
pub fn env_var_refs_in(value: &Value) -> Vec<String> {
let mut seen = BTreeSet::new();
let mut out = Vec::new();
collect_env_refs(value, &mut seen, &mut out);
out
}
fn collect_env_refs(value: &Value, seen: &mut BTreeSet<String>, out: &mut Vec<String>) {
match value {
Value::String(s) => {
for name in env_var_refs(s) {
if seen.insert(name.clone()) {
out.push(name);
}
}
}
Value::Table(table) => {
for (_, inner) in table {
collect_env_refs(inner, seen, out);
}
}
Value::Array(items) => {
for inner in items {
collect_env_refs(inner, seen, out);
}
}
_ => {}
}
}
fn expand_string(s: &str, source: &Path) -> Result<String, ConfigError> {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c != '$' {
out.push(c);
continue;
}
if chars.peek() != Some(&'{') {
out.push(c);
continue;
}
chars.next();
let mut name = String::new();
let mut closed = false;
for next in chars.by_ref() {
if next == '}' {
closed = true;
break;
}
name.push(next);
}
if !closed {
return Err(ConfigError::Parse(
source.to_path_buf(),
"unterminated environment variable reference `${`".to_owned(),
));
}
if name.is_empty() {
return Err(ConfigError::Parse(
source.to_path_buf(),
"empty environment variable reference `${}`".to_owned(),
));
}
let value = match env::var(&name) {
Ok(value) => value,
Err(env::VarError::NotPresent) => {
return Err(ConfigError::EnvVarUnset(name, source.display().to_string()));
}
Err(env::VarError::NotUnicode(_)) => {
return Err(ConfigError::EnvVarNotUnicode(
name,
source.display().to_string(),
));
}
};
out.push_str(&value);
}
Ok(out)
}