use crate::core::config::{GrrConfig, embedded_device_client, embedded_oauth_client};
use crate::core::error::{GrrError, Result};
use figment::{
Figment, Provider,
providers::Format,
providers::{Env, Toml},
};
use std::path::PathBuf;
use tracing::info;
pub struct ConfigLoader;
impl ConfigLoader {
pub async fn load() -> Result<GrrConfig> {
let config_path = Self::config_path()?;
info!("Loading config from: {:?}", config_path);
let mut config = tokio::task::spawn_blocking(move || {
Figment::new()
.merge(Toml::file(&config_path))
.merge(Self::env_provider())
.extract::<GrrConfig>()
.map_err(|e| e.to_string())
})
.await
.map_err(|e| GrrError::Config(e.to_string()))?
.map_err(|e| GrrError::Config(e.to_string()))?;
config = Self::apply_embedded_default(config);
info!("Loaded config: client_id={}", config.oauth.client_id);
match config.oauth.client_secret.as_deref() {
Some(s) if !s.is_empty() => info!("client_secret: configured"),
_ => info!("client_secret: absent (PKCE-only)"),
}
match config.oauth.device_client_id.as_deref().map(str::trim) {
Some(id) if !id.is_empty() => info!("device-flow client: configured"),
_ => info!(
"device-flow client: absent (`grr auth login --device` explains the client type Google requires)"
),
}
Ok(config)
}
fn apply_embedded_default(config: GrrConfig) -> GrrConfig {
let config = Self::apply_client(config, embedded_oauth_client());
Self::apply_device_client(config, embedded_device_client())
}
fn apply_client(mut config: GrrConfig, embedded: Option<(&str, &str)>) -> GrrConfig {
if !config.oauth.client_id.trim().is_empty() {
return config;
}
let Some((client_id, client_secret)) = embedded else {
return config;
};
info!("Using the OAuth client built into this binary");
config.oauth.client_id = client_id.to_owned();
config.oauth.client_secret = Some(client_secret.to_owned());
config
}
fn apply_device_client(mut config: GrrConfig, embedded: Option<(&str, &str)>) -> GrrConfig {
let explicit = config
.oauth
.device_client_id
.as_deref()
.map(str::trim)
.is_some_and(|id| !id.is_empty());
if explicit {
return config;
}
let Some((client_id, client_secret)) = embedded else {
return config;
};
info!("Using the device-flow OAuth client built into this binary");
config.oauth.device_client_id = Some(client_id.to_owned());
config.oauth.device_client_secret = Some(client_secret.to_owned());
config
}
fn env_provider() -> impl Provider {
Env::raw().filter_map(|key| {
let key = key.as_str();
if key.eq_ignore_ascii_case("TYPESAFE_API_KEY") {
return Some("systemone.api-key".into());
}
if let Some(stripped) = key.strip_prefix("GRR_") {
let key = stripped.replace("__", ".");
let key = key.replace('_', "-");
let key = key.to_ascii_lowercase();
Some(key.into())
} else {
None
}
})
}
pub fn config_path() -> Result<PathBuf> {
if let Ok(path) = std::env::var("GRR_CONFIG_PATH") {
return Ok(PathBuf::from(path));
}
let home = dirs::home_dir()
.ok_or_else(|| GrrError::Config("Could not find home directory".into()))?;
Ok(home.join(".grr").join("config.toml"))
}
}
#[cfg(test)]
mod resolution_order_tests {
use super::*;
use crate::core::config::OAuthConfig;
fn config_with(id: &str, secret: Option<&str>) -> GrrConfig {
GrrConfig {
oauth: OAuthConfig {
client_id: id.to_owned(),
client_secret: secret.map(str::to_owned),
..OAuthConfig::default()
},
..GrrConfig::default()
}
}
const EMBEDDED: (&str, &str) = ("embedded-id.apps.googleusercontent.com", "embedded-secret");
#[test]
fn embedded_client_fills_an_empty_config() {
let resolved = ConfigLoader::apply_client(config_with("", None), Some(EMBEDDED));
assert_eq!(resolved.oauth.client_id, EMBEDDED.0);
assert_eq!(resolved.oauth.client_secret.as_deref(), Some(EMBEDDED.1));
}
#[test]
fn a_configured_client_id_is_never_overwritten() {
let resolved = ConfigLoader::apply_client(
config_with("mine.apps.googleusercontent.com", None),
Some(EMBEDDED),
);
assert_eq!(resolved.oauth.client_id, "mine.apps.googleusercontent.com");
assert!(resolved.oauth.client_secret.is_none());
}
#[test]
fn a_whitespace_only_client_id_still_falls_back_to_embedded() {
let resolved = ConfigLoader::apply_client(config_with(" ", None), Some(EMBEDDED));
assert_eq!(resolved.oauth.client_id, EMBEDDED.0);
}
#[test]
fn without_an_embedded_client_the_config_is_left_alone() {
let resolved = ConfigLoader::apply_client(config_with("", None), None);
assert!(resolved.oauth.client_id.is_empty());
assert!(resolved.oauth.client_secret.is_none());
}
#[test]
fn embedded_defaults_are_all_or_nothing() {
if let Some((id, secret)) = crate::core::config::embedded_oauth_client() {
assert!(!id.is_empty(), "id must not be blank");
assert!(!secret.is_empty(), "secret must not be blank");
}
}
#[test]
fn the_no_client_message_names_every_route() {
let help = crate::core::config::NO_CLIENT_HELP;
assert!(help.contains("release"));
assert!(help.contains(".env"));
assert!(help.contains("auth setup"));
}
}