use anyhow::{Context, Result, anyhow};
use clap::ValueEnum;
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub key: Option<String>,
pub api_key: Option<String>,
#[serde(default = "default_api_url")]
pub api_url: String,
#[serde(default = "default_docs_url")]
pub docs_url: String,
#[serde(default = "default_seed")]
pub seed: u64,
pub start: Option<String>,
pub end: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UserConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docs_url: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, ValueEnum)]
#[serde(rename_all = "camelCase")]
pub enum AiAgent {
Claude,
Codex,
Copilot,
}
fn default_api_url() -> String {
"https://api.rngo.dev".into()
}
fn default_docs_url() -> String {
"https://rngo.dev/docs".into()
}
fn default_seed() -> u64 {
1
}
pub fn get_config() -> Result<Config> {
let user_config = config::Config::builder()
.add_source(config::File::from(user_config_file_path()?).required(false))
.build()?;
let project_config = config::Config::builder()
.add_source(config::File::from(project_config_file_path()?).required(false))
.build()?;
let config = config::Config::builder()
.add_source(user_config)
.add_source(project_config)
.add_source(config::Environment::with_prefix("RNGO").convert_case(config::Case::Camel))
.build()?;
config
.try_deserialize::<Config>()
.with_context(|| "Failed to deserialize config")
}
pub fn set_user_config<F>(f: F) -> Result<()>
where
F: FnOnce(&mut UserConfig),
{
let user_config_builder = config::Config::builder()
.add_source(config::File::from(user_config_file_path()?).required(false))
.build()?;
let mut config = user_config_builder
.try_deserialize::<UserConfig>()
.unwrap_or_default();
f(&mut config);
let mut file_base_path = user_config_file_path()?;
file_base_path.set_extension("yml");
if let Some(parent) = file_base_path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = File::create(file_base_path).with_context(|| "Failed to open config file")?;
let yaml = serde_yaml::to_string(&config).with_context(|| "Failed to serialize config")?;
file.write_all(yaml.as_bytes())
.with_context(|| "Failed to write config")
}
fn user_config_file_path() -> Result<PathBuf> {
let mut config_path = ProjectDirs::from("dev", "rngo", "cli")
.map(|project_dirs| project_dirs.config_dir().to_path_buf())
.ok_or_else(|| anyhow!("Could not determine home directory"))?;
config_path.push("config");
Ok(config_path)
}
fn project_config_file_path() -> Result<PathBuf> {
let path: PathBuf = [".", ".rngo", "config.yml"].iter().collect();
Ok(path)
}