rngo-cli 0.22.0

Data simulation CLI using api.rngo.dev
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)
}