xbp 10.8.4

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
//! configuration management module
//!
//! handles ssh configuration and yaml config file management
//! provides loading and saving of configuration files
//! supports home directory based config storage

use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::PathBuf;

#[derive(Debug, Clone)]
pub struct GlobalXbpPaths {
    pub root_dir: PathBuf,
    pub config_file: PathBuf,
    pub ssh_dir: PathBuf,
    pub cache_dir: PathBuf,
    pub logs_dir: PathBuf,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SshConfig {
    pub password: Option<String>,
    pub username: Option<String>,
    pub host: Option<String>,
    pub project_dir: Option<String>,
}

impl SshConfig {
    pub fn new() -> Self {
        SshConfig {
            password: None,
            username: None,
            host: None,
            project_dir: None,
        }
    }

    pub fn load() -> Result<Self, String> {
        let config_path = get_config_path();
        let legacy_path = legacy_config_path();
        let path_to_read = if config_path.exists() {
            config_path
        } else if legacy_path.exists() {
            legacy_path
        } else {
            return Ok(SshConfig::new());
        };

        let content = fs::read_to_string(&path_to_read)
            .map_err(|e| format!("Failed to read config file: {}", e))?;
        serde_yaml::from_str(&content).map_err(|e| format!("Failed to parse config file: {}", e))
    }

    pub fn save(&self) -> Result<(), String> {
        let config_path = get_config_path();
        let config_dir = config_path.parent().ok_or("Invalid config path")?;
        fs::create_dir_all(config_dir)
            .map_err(|e| format!("Failed to create config directory: {}", e))?;

        let content = serde_yaml::to_string(self)
            .map_err(|e| format!("Failed to serialize config: {}", e))?;
        fs::write(&config_path, content).map_err(|e| format!("Failed to write config file: {}", e))
    }
}

pub fn ensure_global_xbp_paths() -> Result<GlobalXbpPaths, String> {
    let root_dir = dirs::config_dir()
        .or_else(|| dirs::home_dir().map(|home| home.join(".config")))
        .unwrap_or_else(|| PathBuf::from("."))
        .join("xbp");

    let paths = GlobalXbpPaths {
        config_file: root_dir.join("config.yaml"),
        ssh_dir: root_dir.join("ssh"),
        cache_dir: root_dir.join("cache"),
        logs_dir: root_dir.join("logs"),
        root_dir,
    };

    for dir in [
        &paths.root_dir,
        &paths.ssh_dir,
        &paths.cache_dir,
        &paths.logs_dir,
    ] {
        fs::create_dir_all(dir)
            .map_err(|e| format!("Failed to create XBP directory {}: {}", dir.display(), e))?;
    }

    if !paths.config_file.exists() {
        fs::write(
            &paths.config_file,
            "password: null\nusername: null\nhost: null\nproject_dir: null\n",
        )
        .map_err(|e| {
            format!(
                "Failed to initialize config file {}: {}",
                paths.config_file.display(),
                e
            )
        })?;
    }

    Ok(paths)
}

pub fn global_xbp_paths() -> Result<GlobalXbpPaths, String> {
    ensure_global_xbp_paths()
}

pub fn get_config_path() -> PathBuf {
    ensure_global_xbp_paths()
        .map(|paths| paths.config_file)
        .unwrap_or_else(|_| legacy_config_path())
}

fn legacy_config_path() -> PathBuf {
    dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join(".xbp")
        .join("config.yaml")
}

pub fn describe_global_xbp_paths() -> Result<Vec<(String, PathBuf)>, String> {
    let paths = global_xbp_paths()?;
    Ok(vec![
        ("root".to_string(), paths.root_dir),
        ("config".to_string(), paths.config_file),
        ("ssh".to_string(), paths.ssh_dir),
        ("cache".to_string(), paths.cache_dir),
        ("logs".to_string(), paths.logs_dir),
    ])
}

const DEFAULT_API_XBP_URL: &str = "https://api.xbp.app";

/// Simple API configuration for the XBP version endpoints.
#[derive(Debug, Clone)]
pub struct ApiConfig {
    base_url: String,
}

impl ApiConfig {
    /// Load the API configuration from API_XBP_URL, falling back to the default.
    pub fn load() -> Self {
        let raw_url = env::var("API_XBP_URL").unwrap_or_else(|_| DEFAULT_API_XBP_URL.to_string());
        let base_url = Self::normalize_base_url(&raw_url);
        ApiConfig { base_url }
    }

    /// Return the normalized base URL that downstream callers should use.
    pub fn base_url(&self) -> &str {
        &self.base_url
    }

    /// Build the version query endpoint.
    pub fn version_endpoint(&self, project_name: &str) -> String {
        format!("{}/version?project_name={}", self.base_url, project_name)
    }

    /// Build the endpoint that increments a version.
    pub fn increment_endpoint(&self) -> String {
        format!("{}/version/increment", self.base_url)
    }

    fn normalize_base_url(raw: &str) -> String {
        let trimmed = raw.trim();
        if trimmed.is_empty() {
            return DEFAULT_API_XBP_URL.to_string();
        }

        let trimmed = trimmed.trim_end_matches('/');
        if trimmed.is_empty() {
            return DEFAULT_API_XBP_URL.to_string();
        }

        trimmed.to_string()
    }
}