use crate::config_schema::{KeyTarget, parse_key};
use crate::runtime::SUPPORTED_PROVIDERS;
use crate::settings::{ApprovalMode, Settings, SettingsStore};
use serde_json::Value;
pub trait ConfigService: Send + Sync {
fn snapshot(&self) -> Settings;
fn approval_mode(&self) -> ApprovalMode {
self.snapshot().approval_mode()
}
fn attribution_enabled(&self) -> bool {
self.snapshot().attribution_enabled()
}
fn current(&self, key: &str) -> Result<Value, String> {
let target = parse_key(key)?;
Ok(current_value(&self.snapshot(), &target))
}
}
impl ConfigService for SettingsStore {
fn snapshot(&self) -> Settings {
SettingsStore::snapshot(self)
}
}
pub(crate) fn current_value(settings: &Settings, target: &KeyTarget) -> Value {
match target {
KeyTarget::DefaultProvider => settings
.default_provider
.clone()
.map(Value::String)
.unwrap_or(Value::Null),
KeyTarget::DefaultModel => settings
.default_model()
.map(|s| Value::String(s.to_string()))
.unwrap_or(Value::Null),
KeyTarget::Attribution => Value::Bool(settings.attribution_enabled()),
KeyTarget::ApprovalMode => Value::String(settings.approval_mode().as_str().to_string()),
KeyTarget::ProactiveWake => Value::Bool(settings.proactive_wake_enabled()),
KeyTarget::Worktrees => Value::String(settings.worktrees_mode().as_str().to_string()),
KeyTarget::Model(p) => settings
.model_for(p)
.map(|s| Value::String(s.to_string()))
.unwrap_or(Value::Null),
KeyTarget::Token(p) => Value::String(
if settings.has_token(p) {
"stored"
} else {
"unset"
}
.to_string(),
),
KeyTarget::BaseUrl(p) => settings
.base_url_for(p)
.map(|s| Value::String(s.to_string()))
.unwrap_or(Value::Null),
KeyTarget::Capabilities => {
crate::capability_settings::overrides_to_json(&settings.capabilities)
}
KeyTarget::CapabilityRef(cap_ref) => {
let stored: Vec<Value> = settings
.capability_overrides_for(cap_ref)
.into_iter()
.map(|(index, entry)| {
crate::capability_settings::stored_override_json(index, entry)
})
.collect();
Value::Array(stored)
}
}
}
pub(crate) fn scoped_current(settings: &Settings, key: &str) -> Value {
let mut map = serde_json::Map::new();
let supported = |provider: &String| SUPPORTED_PROVIDERS.contains(&provider.as_str());
match key {
"tokens" => {
for provider in settings.tokens.keys().filter(|p| supported(p)) {
map.insert(provider.clone(), Value::String("stored".to_string()));
}
}
"models" => {
for (provider, spec) in settings.models.iter().filter(|(p, _)| supported(p)) {
map.insert(provider.clone(), Value::String(spec.clone()));
}
}
"base_urls" => {
for (provider, url) in settings.base_urls.iter().filter(|(p, _)| supported(p)) {
map.insert(provider.clone(), Value::String(url.clone()));
}
}
_ => {}
}
Value::Object(map)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn service_reads_semantic_getters_and_by_key() {
let tmp = tempfile::tempdir().expect("tmp");
let store = Arc::new(SettingsStore::open(tmp.path().join("settings.toml")));
store
.set_default_provider(Some("anthropic".to_string()))
.unwrap();
store
.set_token("openai".to_string(), "sk-secret".to_string())
.unwrap();
store
.set_approval_mode(crate::settings::ApprovalMode::Protective)
.unwrap();
let svc: Arc<dyn ConfigService> = store.clone();
assert!(svc.attribution_enabled());
assert_eq!(
svc.approval_mode(),
crate::settings::ApprovalMode::Protective
);
assert_eq!(svc.current("default_provider").unwrap(), "anthropic");
assert_eq!(svc.current("approval_mode").unwrap(), "protective");
assert_eq!(svc.current("tokens.openai").unwrap(), "stored");
assert!(
!svc.current("tokens.openai")
.unwrap()
.to_string()
.contains("sk-secret"),
"secret value must never be returned"
);
assert!(svc.current("frobnicate").is_err());
}
}