use std::fs;
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
const CONFIG_FILENAME: &str = ".formanatorrc.json";
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
#[serde(rename = "accessToken")]
pub access_token: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub email: Option<String>,
}
fn config_path() -> Result<PathBuf> {
if let Some(path) = std::env::var_os("FORMANATOR_CONFIG_PATH") {
return Ok(PathBuf::from(path));
}
let home = dirs::home_dir().context("Could not determine your home directory")?;
Ok(home.join(CONFIG_FILENAME))
}
pub fn read_config() -> Result<Option<Config>> {
let path = config_path()?;
if !path.exists() {
return Ok(None);
}
let raw = fs::read_to_string(&path)
.with_context(|| format!("Failed to read config file at {}", path.display()))?;
let parsed: Config = serde_json::from_str(&raw)
.with_context(|| format!("Failed to parse config file at {}", path.display()))?;
Ok(Some(parsed))
}
pub fn get_access_token() -> Result<Option<String>> {
Ok(read_config()?.map(|c| c.access_token))
}
pub fn resolve_access_token(explicit: Option<&str>) -> Result<String> {
if let Some(token) = explicit {
return Ok(token.to_string());
}
match get_access_token()? {
Some(t) if !t.is_empty() => Ok(t),
_ => anyhow::bail!("You aren't logged in to Forma. Please run `formanator login` first."),
}
}
pub fn store_config(config: &Config) -> Result<()> {
let path = config_path()?;
let serialised = serde_json::to_string(config)?;
fs::write(&path, serialised)
.with_context(|| format!("Failed to write config file at {}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_serializes_with_camelcase_access_token_and_omits_email() {
let config = Config {
access_token: "tok".to_string(),
email: None,
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("\"accessToken\":\"tok\""), "{json}");
assert!(!json.contains("email"), "{json}");
}
#[test]
fn config_serializes_email_when_present() {
let config = Config {
access_token: "tok".to_string(),
email: Some("user@example.com".to_string()),
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("\"email\":\"user@example.com\""), "{json}");
}
#[test]
fn config_round_trips_through_json() {
let original = Config {
access_token: "tok".to_string(),
email: Some("user@example.com".to_string()),
};
let json = serde_json::to_string(&original).unwrap();
let parsed: Config = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.access_token, original.access_token);
assert_eq!(parsed.email, original.email);
}
#[test]
fn resolve_access_token_prefers_explicit_value() {
let token = resolve_access_token(Some("from-cli")).unwrap();
assert_eq!(token, "from-cli");
}
}