Skip to main content

perfgate_config/
lib.rs

1//! Configuration loading and merging logic for perfgate.
2//!
3//! Loads TOML configuration files, merges environment variables and CLI overrides,
4//! and resolves baseline server settings for perfgate workflows.
5//!
6//! Part of the [perfgate](https://github.com/EffortlessMetrics/perfgate) workspace.
7//!
8//! # Example
9//!
10//! ```no_run
11//! use perfgate_config::load_config_file;
12//! use std::path::Path;
13//!
14//! let config = load_config_file(Path::new("perfgate.toml")).unwrap();
15//! println!("Benches: {}", config.benches.len());
16//! ```
17
18use anyhow::Context;
19use perfgate_client::{BaselineClient, ClientConfig, FallbackClient, FallbackStorage};
20use perfgate_types::{BaselineServerConfig, ConfigFile};
21use std::fs;
22use std::path::Path;
23
24/// Resolved server configuration with all sources merged.
25#[derive(Debug, Clone, Default)]
26pub struct ResolvedServerConfig {
27    pub url: Option<String>,
28    pub api_key: Option<String>,
29    pub project: Option<String>,
30    pub fallback_to_local: bool,
31}
32
33impl ResolvedServerConfig {
34    /// Returns true if server is configured (has a URL).
35    pub fn is_configured(&self) -> bool {
36        self.url.as_ref().is_some_and(|u| !u.is_empty())
37    }
38
39    /// Creates a BaselineClient from this configuration.
40    pub fn create_client(&self) -> anyhow::Result<Option<BaselineClient>> {
41        if !self.is_configured() {
42            return Ok(None);
43        }
44
45        let url = self.url.as_ref().unwrap();
46        let mut config = ClientConfig::new(url);
47
48        if let Some(api_key) = &self.api_key {
49            config = config.with_api_key(api_key);
50        }
51
52        let client = BaselineClient::new(config)
53            .with_context(|| format!("Failed to create baseline client for {}", url))?;
54
55        Ok(Some(client))
56    }
57
58    /// Creates a FallbackClient if fallback is enabled and server is configured.
59    pub fn create_fallback_client(
60        &self,
61        fallback_dir: Option<&Path>,
62    ) -> anyhow::Result<Option<FallbackClient>> {
63        let client = match self.create_client()? {
64            Some(c) => c,
65            None => return Ok(None),
66        };
67
68        let fallback = if self.fallback_to_local {
69            fallback_dir.map(|dir| FallbackStorage::local(dir.to_path_buf()))
70        } else {
71            None
72        };
73
74        Ok(Some(FallbackClient::new(client, fallback)))
75    }
76
77    /// Returns a baseline client for server operations, or an error if not configured.
78    pub fn require_fallback_client(
79        &self,
80        fallback_dir: Option<&Path>,
81        error_msg: &str,
82    ) -> anyhow::Result<FallbackClient> {
83        self.create_fallback_client(fallback_dir)?
84            .ok_or_else(|| anyhow::anyhow!(error_msg.to_string()))
85    }
86
87    /// Resolve a project for server operations.
88    pub fn resolve_project(&self, project: Option<String>) -> anyhow::Result<String> {
89        project.or_else(|| self.project.clone()).ok_or_else(|| {
90            anyhow::anyhow!(
91                "--project is required (or set --project flag, PERFGATE_PROJECT, or [baseline_server].project in perfgate.toml)"
92            )
93        })
94    }
95}
96
97/// Loads the perfgate.toml or perfgate.json config file.
98pub fn load_config_file(path: &Path) -> anyhow::Result<ConfigFile> {
99    if !path.exists() {
100        return Ok(ConfigFile::default());
101    }
102
103    let content = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
104
105    if path
106        .extension()
107        .and_then(|ext| ext.to_str())
108        .is_some_and(|ext| ext == "json")
109    {
110        serde_json::from_str::<ConfigFile>(&content)
111            .with_context(|| format!("parse {}", path.display()))
112    } else {
113        toml::from_str::<ConfigFile>(&content).with_context(|| format!("parse {}", path.display()))
114    }
115}
116
117/// Resolves server configuration from multiple sources.
118pub fn resolve_server_config(
119    flag_url: Option<String>,
120    flag_key: Option<String>,
121    flag_project: Option<String>,
122    file_config: &BaselineServerConfig,
123) -> ResolvedServerConfig {
124    ResolvedServerConfig {
125        url: flag_url.or_else(|| file_config.resolved_url()),
126        api_key: flag_key.or_else(|| file_config.resolved_api_key()),
127        project: flag_project.or_else(|| file_config.resolved_project()),
128        fallback_to_local: file_config.fallback_to_local,
129    }
130}