use secrecy::SecretBox;
use serde::{Deserialize, Serialize};
mod api_keys;
mod permissions;
mod registry;
mod settings;
mod store;
pub use api_keys::{compute_fingerprint, mask_key, resolve_api_key};
pub use registry::{is_known_setting, nearest_setting_key, setting_key_names, SETTING_KEYS};
pub use settings::{get_setting, list_settings, set_setting, unset_setting};
pub use store::{config_file_path, load_config, save_config};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub schema_version: u32,
#[serde(default)]
pub keys: Vec<ApiKeyEntry>,
#[serde(default)]
pub settings: std::collections::BTreeMap<String, String>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct ApiKeyEntry {
pub provider: String,
pub value: String,
pub added_at: String,
pub fingerprint: String,
}
impl std::fmt::Debug for ApiKeyEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApiKeyEntry")
.field("provider", &self.provider)
.field("value", &mask_key(&self.value))
.field("added_at", &self.added_at)
.field("fingerprint", &self.fingerprint)
.finish()
}
}
impl Default for AppConfig {
fn default() -> Self {
Self {
schema_version: 1,
keys: vec![],
settings: std::collections::BTreeMap::new(),
}
}
}
pub struct SettingKey {
pub key: &'static str,
pub default: Option<&'static str>,
pub kind: ValueKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueKind {
Bool,
Unsigned,
Float,
Tz,
Url,
Path,
Text,
LogDirective,
OneOf(&'static [&'static str]),
}
impl ValueKind {
pub fn expectation(&self) -> Option<String> {
match self {
ValueKind::Text => None,
ValueKind::OneOf(options) => Some(options.join("|")),
ValueKind::Bool => Some("true|false (also 1|0, yes|no, on|off)".to_string()),
other => Some(crate::i18n::validation::config_value_expectation(*other)),
}
}
pub fn accepts(&self, value: &str) -> bool {
let trimmed = value.trim();
match self {
ValueKind::Text => true,
ValueKind::Bool => matches!(
trimmed.to_ascii_lowercase().as_str(),
"true" | "false" | "1" | "0" | "yes" | "no" | "on" | "off"
),
ValueKind::Unsigned => !trimmed.is_empty() && trimmed.parse::<u64>().is_ok(),
ValueKind::Float => trimmed
.parse::<f64>()
.is_ok_and(|parsed| parsed.is_finite()),
ValueKind::Tz => trimmed.parse::<chrono_tz::Tz>().is_ok(),
ValueKind::Url => {
(trimmed.starts_with("http://") || trimmed.starts_with("https://"))
&& trimmed.len() > "https://".len()
}
ValueKind::Path => !trimmed.is_empty(),
ValueKind::LogDirective => {
!trimmed.is_empty() && tracing_subscriber::EnvFilter::try_new(trimmed).is_ok()
}
ValueKind::OneOf(options) => options.contains(&trimmed),
}
}
}
pub struct ResolvedKey {
pub value: SecretBox<String>,
pub source: &'static str,
}