cargo-rustapi 0.1.550

The official CLI tool for the RustAPI framework. Scaffold new projects, run development servers, and manage database migrations.
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct UserInfo {
    pub login: String,
    pub tier: String,
    #[serde(default)]
    pub avatar_url: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CloudConfig {
    #[serde(default)]
    pub token: Option<String>,
    #[serde(default)]
    pub refresh_token: Option<String>,
    #[serde(default)]
    pub user: Option<UserInfo>,
    #[serde(default)]
    pub last_login: Option<String>,
    #[serde(default)]
    pub cloud_url: Option<String>,
}

fn config_dir() -> PathBuf {
    let home = std::env::var("USERPROFILE")
        .or_else(|_| std::env::var("HOME"))
        .unwrap_or_else(|_| ".".into());

    PathBuf::from(home).join(".rustapi")
}

fn config_path() -> PathBuf {
    if let Ok(path) = std::env::var("RUSTAPI_CONFIG_PATH") {
        return PathBuf::from(path);
    }
    config_dir().join("config.json")
}

pub fn load_config() -> Result<CloudConfig> {
    let path = config_path();
    if !path.exists() {
        return Err(anyhow::anyhow!("Not logged in. Run `rustapi login` first."));
    }

    let json = std::fs::read_to_string(&path)
        .with_context(|| format!("Failed to read config at {}", path.display()))?;

    serde_json::from_str(&json).context("Failed to parse config file")
}

pub fn save_config(config: &CloudConfig) -> Result<()> {
    let path = config_path();
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("Failed to create {}", parent.display()))?;
    }

    let json = serde_json::to_string_pretty(config).context("Failed to serialize config")?;
    std::fs::write(&path, json)
        .with_context(|| format!("Failed to write config to {}", path.display()))?;

    Ok(())
}

pub fn clear_config() -> Result<()> {
    let path = config_path();
    if path.exists() {
        std::fs::remove_file(&path)
            .with_context(|| format!("Failed to remove {}", path.display()))?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{Mutex, OnceLock};

    static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();

    fn env_lock() -> std::sync::MutexGuard<'static, ()> {
        ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
    }

    #[test]
    fn save_config_creates_parent_dir_for_rustapi_config_path_override() {
        let _guard = env_lock();
        let dir = tempfile::tempdir().expect("tempdir");
        let config_file = dir.path().join("nested").join("cloud-config.json");
        std::env::set_var("RUSTAPI_CONFIG_PATH", config_file.to_str().unwrap());

        let cfg = CloudConfig {
            token: Some("test-token".into()),
            refresh_token: None,
            user: Some(UserInfo {
                login: "test".into(),
                tier: "hobby".into(),
                avatar_url: None,
            }),
            last_login: None,
            cloud_url: Some("http://127.0.0.1:8080".into()),
        };

        save_config(&cfg).expect("save under override path");
        assert!(config_file.exists());

        std::env::remove_var("RUSTAPI_CONFIG_PATH");
    }
}