1use anyhow::Context;
19use perfgate_client::{BaselineClient, ClientConfig, FallbackClient, FallbackStorage};
20use perfgate_types::{BaselineServerConfig, ConfigFile};
21use std::fs;
22use std::path::Path;
23
24#[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 pub fn is_configured(&self) -> bool {
36 self.url.as_ref().is_some_and(|u| !u.is_empty())
37 }
38
39 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 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 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 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
97pub 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
117pub 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}