use crate::error::{PfpError, Result};
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug)]
pub struct Config {
pub api_url: String,
pub auth_header: Option<String>,
}
#[derive(Deserialize, Debug)]
struct ProfilesFile {
active: Option<String>,
profiles: Option<std::collections::HashMap<String, Profile>>,
}
#[derive(Deserialize, Debug)]
struct Profile {
#[serde(rename = "PREFECT_API_URL")]
api_url: Option<String>,
}
impl Config {
pub fn load() -> Result<Self> {
let api_url = Self::resolve_api_url()?;
let auth_header = Self::resolve_auth();
Ok(Config {
api_url,
auth_header,
})
}
fn resolve_api_url() -> Result<String> {
if let Ok(url) = std::env::var("PREFECT_API_URL") {
return Ok(url);
}
let profiles = Self::read_profiles()?;
let active = profiles.active.unwrap_or_else(|| "default".to_string());
let profile = profiles
.profiles
.and_then(|p| p.into_iter().find(|(k, _)| *k == active).map(|(_, v)| v))
.ok_or_else(|| PfpError::Config(format!("Profile '{}' not found", active)))?;
profile.api_url.ok_or(PfpError::NoApiUrl)
}
fn resolve_auth() -> Option<String> {
let auth_string = std::env::var("PREFECT_API_AUTH_STRING").ok()?;
use base64::Engine;
let encoded = base64::engine::general_purpose::STANDARD.encode(auth_string.as_bytes());
Some(format!("Basic {}", encoded))
}
fn profiles_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".prefect")
.join("profiles.toml")
}
fn read_profiles() -> Result<ProfilesFile> {
let path = Self::profiles_path();
let content = std::fs::read_to_string(&path)
.map_err(|e| PfpError::Config(format!("Cannot read {}: {}", path.display(), e)))?;
toml::from_str(&content)
.map_err(|e| PfpError::Config(format!("Cannot parse {}: {}", path.display(), e)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn resolve_api_url_from_env() {
unsafe {
std::env::set_var("PREFECT_API_URL", "https://test.example.com/api");
}
let result = Config::resolve_api_url();
unsafe {
std::env::remove_var("PREFECT_API_URL");
}
assert_eq!(result.unwrap(), "https://test.example.com/api");
}
#[test]
#[serial]
fn resolve_auth_encodes_basic_auth() {
unsafe {
std::env::set_var("PREFECT_API_AUTH_STRING", "user:pass");
}
let result = Config::resolve_auth();
unsafe {
std::env::remove_var("PREFECT_API_AUTH_STRING");
}
let header = result.unwrap();
assert!(header.starts_with("Basic "));
assert_eq!(header, "Basic dXNlcjpwYXNz");
}
#[test]
#[serial]
fn resolve_auth_missing_returns_none() {
unsafe {
std::env::remove_var("PREFECT_API_AUTH_STRING");
}
let result = Config::resolve_auth();
assert!(result.is_none());
}
#[test]
fn profiles_path_ends_with_expected() {
let path = Config::profiles_path();
assert!(path.ends_with(".prefect/profiles.toml"));
}
}