use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
impl Default for EnvironmentConfig {
fn default() -> Self {
Self {
environment: "sandbox".to_string(),
token: String::new(),
recurring_token: None,
transfer_token: None,
edi_user: None,
edi_token: None,
client_id: None,
client_secret: None,
}
}
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct PbConfig {
pub default: EnvironmentConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub production: Option<EnvironmentConfig>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EnvironmentConfig {
#[serde(default = "default_environment")]
pub environment: String,
#[serde(default)]
pub token: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub recurring_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transfer_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edi_user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edi_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_secret: Option<String>,
}
fn default_environment() -> String {
"sandbox".to_string()
}
#[cfg(unix)]
fn set_restrictive_perms(path: &std::path::Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
Ok(())
}
#[cfg(not(unix))]
fn set_restrictive_perms(_path: &std::path::Path) -> Result<()> {
Ok(())
}
impl PbConfig {
pub fn config_dir() -> Result<PathBuf> {
if let Ok(dir) = std::env::var("PB_CONFIG_DIR") {
if !dir.trim().is_empty() {
return Ok(PathBuf::from(dir));
}
}
let config_dir = dirs::config_dir().ok_or_else(|| {
anyhow::anyhow!("não foi possÃvel determinar o diretório de configuração")
})?;
Ok(config_dir.join("pb"))
}
pub fn config_path() -> Result<PathBuf> {
Ok(Self::config_dir()?.join("config.toml"))
}
pub fn load() -> Result<Self> {
let path = Self::config_path()?;
if !path.exists() {
return Ok(Self::default());
}
let content = fs::read_to_string(&path)?;
let config: PbConfig = toml::from_str(&content)?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
let dir = Self::config_dir()?;
fs::create_dir_all(&dir)?;
let path = Self::config_path()?;
let content = toml::to_string_pretty(self)?;
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&path)?;
set_restrictive_perms(&path)?;
std::io::Write::write_all(&mut file, content.as_bytes())?;
Ok(())
}
pub fn get_active_config(&self, env_override: Option<&str>) -> &EnvironmentConfig {
match env_override {
Some("production") => self.production.as_ref().unwrap_or(&self.default),
_ => &self.default,
}
}
pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
match key {
"token" => self.default.token = value.to_string(),
"environment" => self.default.environment = value.to_string(),
"recurring_token" => self.default.recurring_token = Some(value.to_string()),
"transfer_token" => self.default.transfer_token = Some(value.to_string()),
"edi_user" => self.default.edi_user = Some(value.to_string()),
"edi_token" => self.default.edi_token = Some(value.to_string()),
"client_id" => self.default.client_id = Some(value.to_string()),
"client_secret" => self.default.client_secret = Some(value.to_string()),
_ => anyhow::bail!("chave desconhecida: {key}"),
}
self.save()
}
pub fn get_value(&self, key: &str) -> Result<String> {
let cfg = &self.default;
match key {
"token" => Ok(cfg.token.clone()),
"environment" => Ok(cfg.environment.clone()),
"recurring_token" => Ok(cfg.recurring_token.clone().unwrap_or_default()),
"transfer_token" => Ok(cfg.transfer_token.clone().unwrap_or_default()),
"edi_user" => Ok(cfg.edi_user.clone().unwrap_or_default()),
"edi_token" => Ok(cfg.edi_token.clone().unwrap_or_default()),
"client_id" => Ok(cfg.client_id.clone().unwrap_or_default()),
"client_secret" => Ok(cfg.client_secret.clone().unwrap_or_default()),
_ => anyhow::bail!("chave desconhecida: {key}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_default() {
let config = PbConfig::default();
assert_eq!(config.default.environment, "sandbox");
assert_eq!(config.default.token, "");
assert!(config.production.is_none());
}
#[test]
fn config_toml_roundtrip() {
let config = PbConfig {
default: EnvironmentConfig {
environment: "sandbox".to_string(),
token: "abc123".to_string(),
recurring_token: Some("def456".to_string()),
transfer_token: Some("transfer".to_string()),
edi_user: Some("123456789".to_string()),
edi_token: Some("edi_token".to_string()),
client_id: None,
client_secret: None,
},
production: Some(EnvironmentConfig {
environment: "production".to_string(),
token: "prod_token".to_string(),
recurring_token: None,
transfer_token: None,
edi_user: None,
edi_token: None,
client_id: Some("app_id".to_string()),
client_secret: Some("secret".to_string()),
}),
};
let toml_str = toml::to_string_pretty(&config).unwrap();
let parsed: PbConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(parsed.default.environment, "sandbox");
assert_eq!(parsed.default.token, "abc123");
assert_eq!(parsed.default.recurring_token, Some("def456".to_string()));
assert_eq!(parsed.default.transfer_token, Some("transfer".to_string()));
assert_eq!(parsed.default.edi_user, Some("123456789".to_string()));
assert!(parsed.default.client_id.is_none());
let prod = parsed.production.unwrap();
assert_eq!(prod.client_id, Some("app_id".to_string()));
}
#[test]
fn config_minimal_toml() {
let toml_str = r#"
[default]
token = "my_token"
"#;
let config: PbConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.default.token, "my_token");
assert_eq!(config.default.environment, "sandbox");
}
#[test]
fn test_get_active_config_default() {
let config = PbConfig::default();
let active = config.get_active_config(None);
assert_eq!(active.environment, "sandbox");
}
#[test]
fn test_get_active_config_production_without_override() {
let config = PbConfig::default();
let active = config.get_active_config(Some("sandbox"));
assert_eq!(active.environment, "sandbox");
}
#[test]
fn test_get_active_config_production_fallsback_to_default() {
let config = PbConfig::default();
let active = config.get_active_config(Some("production"));
assert_eq!(active.environment, "sandbox");
}
#[test]
fn test_get_value_returns_empty_for_unset() {
let config = PbConfig::default();
assert_eq!(config.get_value("token").unwrap(), "");
assert_eq!(config.get_value("recurring_token").unwrap(), "");
}
#[test]
fn test_get_value_unknown_key() {
let config = PbConfig::default();
assert!(config.get_value("unknown").is_err());
}
#[test]
fn config_dir_respects_env_override() {
let _guard = env_guard();
let dir = std::env::temp_dir().join(format!("pb-env-dir-test-{}", std::process::id()));
std::env::set_var("PB_CONFIG_DIR", &dir);
assert_eq!(PbConfig::config_dir().unwrap(), dir);
assert_eq!(PbConfig::config_path().unwrap(), dir.join("config.toml"));
std::env::remove_var("PB_CONFIG_DIR");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn config_roundtrip_via_env_dir() {
let _guard = env_guard();
let dir = std::env::temp_dir().join(format!("pb-rt-roundtrip-test-{}", std::process::id()));
std::env::set_var("PB_CONFIG_DIR", &dir);
let mut config = PbConfig::default();
config.default.token = "token_roundtrip".to_string();
config.default.recurring_token = Some("rec".to_string());
config.save().unwrap();
let loaded = PbConfig::load().unwrap();
assert_eq!(loaded.default.token, "token_roundtrip");
assert_eq!(loaded.default.recurring_token.as_deref(), Some("rec"));
std::env::remove_var("PB_CONFIG_DIR");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn set_value_updates_and_persists() {
let _guard = env_guard();
let dir = std::env::temp_dir().join(format!("pb-set-value-test-{}", std::process::id()));
std::env::set_var("PB_CONFIG_DIR", &dir);
let mut config = PbConfig::default();
config.set_value("token", "novo_token").unwrap();
config.set_value("environment", "production").unwrap();
let loaded = PbConfig::load().unwrap();
assert_eq!(loaded.default.token, "novo_token");
assert_eq!(loaded.default.environment, "production");
std::env::remove_var("PB_CONFIG_DIR");
let _ = std::fs::remove_dir_all(&dir);
}
fn env_guard() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn set_value_unknown_key_errors() {
let mut config = PbConfig::default();
assert!(config.set_value("foo", "bar").is_err());
}
#[test]
fn set_and_get_transfer_and_edi_credentials() {
let _guard = env_guard();
let dir = std::env::temp_dir().join(format!("pb-credentials-test-{}", std::process::id()));
std::env::set_var("PB_CONFIG_DIR", &dir);
let mut config = PbConfig::default();
config.set_value("transfer_token", "transfer-123").unwrap();
config.set_value("edi_user", "126984538").unwrap();
config.set_value("edi_token", "edi-123").unwrap();
let loaded = PbConfig::load().unwrap();
assert_eq!(loaded.get_value("transfer_token").unwrap(), "transfer-123");
assert_eq!(loaded.get_value("edi_user").unwrap(), "126984538");
assert_eq!(loaded.get_value("edi_token").unwrap(), "edi-123");
std::env::remove_var("PB_CONFIG_DIR");
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn save_sets_restrictive_permissions() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join(format!("pb-config-test-{}", std::process::id()));
let path = dir.join("config.toml");
fs::create_dir_all(&dir).unwrap();
fs::write(&path, "test").unwrap();
set_restrictive_perms(&path).unwrap();
let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600);
let _ = fs::remove_dir_all(&dir);
}
}