use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use url::Url;
pub fn get_config_dir() -> Result<PathBuf> {
let config_dir = dirs::config_dir()
.context("Could not determine config directory")?
.join("micropub");
fs::create_dir_all(&config_dir).context("Failed to create config directory")?;
Ok(config_dir)
}
pub fn get_data_dir() -> Result<PathBuf> {
let data_dir = dirs::data_dir()
.context("Could not determine data directory")?
.join("micropub");
fs::create_dir_all(&data_dir).context("Failed to create data directory")?;
Ok(data_dir)
}
pub fn get_drafts_dir() -> Result<PathBuf> {
let drafts_dir = get_data_dir()?.join("drafts");
fs::create_dir_all(&drafts_dir)?;
Ok(drafts_dir)
}
pub fn get_archive_dir() -> Result<PathBuf> {
let archive_dir = get_data_dir()?.join("archive");
fs::create_dir_all(&archive_dir)?;
Ok(archive_dir)
}
pub fn get_tokens_dir() -> Result<PathBuf> {
let tokens_dir = get_data_dir()?.join("tokens");
fs::create_dir_all(&tokens_dir)?;
Ok(tokens_dir)
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub default_profile: String,
pub editor: Option<String>,
pub client_id: Option<String>,
pub profiles: HashMap<String, Profile>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Profile {
pub domain: String,
pub micropub_endpoint: Option<String>,
pub media_endpoint: Option<String>,
pub token_endpoint: Option<String>,
pub authorization_endpoint: Option<String>,
}
impl Config {
pub fn validate(&self) -> Result<()> {
if let Some(ref client_id) = self.client_id {
Url::parse(client_id)
.context("client_id must be a valid URL (e.g., 'https://github.com/user/repo')")?;
}
Ok(())
}
pub fn load() -> Result<Self> {
let config_path = get_config_dir()?.join("config.toml");
if config_path.exists() {
let contents =
fs::read_to_string(&config_path).context("Failed to read config file")?;
let config: Config =
toml::from_str(&contents).context("Failed to parse config file")?;
config.validate()?;
Ok(config)
} else {
Ok(Config {
default_profile: String::new(),
editor: None,
client_id: None,
profiles: HashMap::new(),
})
}
}
pub fn save(&self) -> Result<()> {
let config_path = get_config_dir()?.join("config.toml");
let contents = toml::to_string_pretty(self).context("Failed to serialize config")?;
fs::write(&config_path, contents).context("Failed to write config file")?;
Ok(())
}
pub fn get_profile(&self, name: &str) -> Option<&Profile> {
self.profiles.get(name)
}
pub fn upsert_profile(&mut self, name: String, profile: Profile) {
self.profiles.insert(name, profile);
}
}
pub fn load_token(profile_name: &str) -> Result<String> {
let tokens_dir = get_tokens_dir()?;
let token_path = tokens_dir.join(format!("{}.token", profile_name));
let token = fs::read_to_string(&token_path)
.context("Token not found. Run 'micropub auth <domain>' to authenticate")?
.trim()
.to_string();
if token.is_empty() {
anyhow::bail!("Token file is empty. Re-authenticate with: micropub auth <domain>");
}
Ok(token)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_serialization() {
let mut config = Config {
default_profile: "test".to_string(),
editor: Some("vim".to_string()),
client_id: None,
profiles: HashMap::new(),
};
config.upsert_profile(
"test".to_string(),
Profile {
domain: "example.com".to_string(),
micropub_endpoint: Some("https://example.com/micropub".to_string()),
media_endpoint: None,
token_endpoint: None,
authorization_endpoint: None,
},
);
let toml = toml::to_string(&config).unwrap();
assert!(toml.contains("example.com"));
}
#[test]
fn test_validate_valid_client_id() {
let config = Config {
default_profile: "test".to_string(),
editor: None,
client_id: Some("https://github.com/user/repo".to_string()),
profiles: HashMap::new(),
};
assert!(config.validate().is_ok());
}
#[test]
fn test_validate_invalid_client_id() {
let config = Config {
default_profile: "test".to_string(),
editor: None,
client_id: Some("not-a-url".to_string()),
profiles: HashMap::new(),
};
let result = config.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("client_id must be a valid URL"));
}
#[test]
fn test_validate_no_client_id() {
let config = Config {
default_profile: "test".to_string(),
editor: None,
client_id: None,
profiles: HashMap::new(),
};
assert!(config.validate().is_ok());
}
}