aperture_cli/config/
url_resolver.rs1use crate::cache::models::CachedSpec;
2use crate::config::models::{ApiConfig, GlobalConfig};
3
4pub struct BaseUrlResolver<'a> {
6 spec: &'a CachedSpec,
8 global_config: Option<&'a GlobalConfig>,
10 environment_override: Option<String>,
12}
13
14impl<'a> BaseUrlResolver<'a> {
15 #[must_use]
17 pub const fn new(spec: &'a CachedSpec) -> Self {
18 Self {
19 spec,
20 global_config: None,
21 environment_override: None,
22 }
23 }
24
25 #[must_use]
27 #[allow(clippy::missing_const_for_fn)]
28 pub fn with_global_config(mut self, config: &'a GlobalConfig) -> Self {
29 self.global_config = Some(config);
30 self
31 }
32
33 #[must_use]
35 pub fn with_environment(mut self, env: Option<String>) -> Self {
36 self.environment_override = env;
37 self
38 }
39
40 #[must_use]
47 pub fn resolve(&self, explicit_url: Option<&str>) -> String {
48 if let Some(url) = explicit_url {
50 return url.to_string();
51 }
52
53 if let Some(config) = self.global_config {
55 if let Some(api_config) = config.api_configs.get(&self.spec.name) {
56 let env_to_check = self.environment_override.as_ref().map_or_else(
58 || std::env::var("APERTURE_ENV").unwrap_or_default(),
59 std::clone::Clone::clone,
60 );
61
62 if !env_to_check.is_empty() {
63 if let Some(env_url) = api_config.environment_urls.get(&env_to_check) {
64 return env_url.clone();
65 }
66 }
67
68 if let Some(override_url) = &api_config.base_url_override {
70 return override_url.clone();
71 }
72 }
73 }
74
75 if let Ok(url) = std::env::var("APERTURE_BASE_URL") {
77 return url;
78 }
79
80 if let Some(base_url) = &self.spec.base_url {
82 return base_url.clone();
83 }
84
85 "https://api.example.com".to_string()
87 }
88
89 #[must_use]
91 pub fn get_api_config(&self) -> Option<&ApiConfig> {
92 self.global_config
93 .and_then(|config| config.api_configs.get(&self.spec.name))
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::cache::models::CachedSpec;
101 use std::collections::HashMap;
102 use std::sync::Mutex;
103
104 static ENV_TEST_MUTEX: Mutex<()> = Mutex::new(());
106
107 fn create_test_spec(name: &str, base_url: Option<&str>) -> CachedSpec {
108 CachedSpec {
109 cache_format_version: crate::cache::models::CACHE_FORMAT_VERSION,
110 name: name.to_string(),
111 version: "1.0.0".to_string(),
112 commands: vec![],
113 base_url: base_url.map(|s| s.to_string()),
114 servers: base_url.map(|s| vec![s.to_string()]).unwrap_or_default(),
115 security_schemes: HashMap::new(),
116 skipped_endpoints: vec![],
117 }
118 }
119
120 fn test_with_env_isolation<F>(test_fn: F)
122 where
123 F: FnOnce() + std::panic::UnwindSafe,
124 {
125 let _guard = ENV_TEST_MUTEX.lock().unwrap();
127
128 let original_value = std::env::var("APERTURE_BASE_URL").ok();
130
131 std::env::remove_var("APERTURE_BASE_URL");
133
134 let result = std::panic::catch_unwind(test_fn);
136
137 if let Some(original) = original_value {
139 std::env::set_var("APERTURE_BASE_URL", original);
140 } else {
141 std::env::remove_var("APERTURE_BASE_URL");
142 }
143
144 drop(_guard);
146
147 if let Err(panic_info) = result {
149 std::panic::resume_unwind(panic_info);
150 }
151 }
152
153 #[test]
154 fn test_priority_1_explicit_url() {
155 test_with_env_isolation(|| {
156 let spec = create_test_spec("test-api", Some("https://spec.example.com"));
157 let resolver = BaseUrlResolver::new(&spec);
158
159 assert_eq!(
160 resolver.resolve(Some("https://explicit.example.com")),
161 "https://explicit.example.com"
162 );
163 });
164 }
165
166 #[test]
167 fn test_priority_2_api_config_override() {
168 test_with_env_isolation(|| {
169 let spec = create_test_spec("test-api", Some("https://spec.example.com"));
170
171 let mut api_configs = HashMap::new();
172 api_configs.insert(
173 "test-api".to_string(),
174 ApiConfig {
175 base_url_override: Some("https://config.example.com".to_string()),
176 environment_urls: HashMap::new(),
177 strict_mode: false,
178 },
179 );
180
181 let global_config = GlobalConfig {
182 api_configs,
183 ..Default::default()
184 };
185
186 let resolver = BaseUrlResolver::new(&spec).with_global_config(&global_config);
187
188 assert_eq!(resolver.resolve(None), "https://config.example.com");
189 });
190 }
191
192 #[test]
193 fn test_priority_2_environment_specific() {
194 test_with_env_isolation(|| {
195 let spec = create_test_spec("test-api", Some("https://spec.example.com"));
196
197 let mut environment_urls = HashMap::new();
198 environment_urls.insert(
199 "staging".to_string(),
200 "https://staging.example.com".to_string(),
201 );
202 environment_urls.insert("prod".to_string(), "https://prod.example.com".to_string());
203
204 let mut api_configs = HashMap::new();
205 api_configs.insert(
206 "test-api".to_string(),
207 ApiConfig {
208 base_url_override: Some("https://config.example.com".to_string()),
209 environment_urls,
210 strict_mode: false,
211 },
212 );
213
214 let global_config = GlobalConfig {
215 api_configs,
216 ..Default::default()
217 };
218
219 let resolver = BaseUrlResolver::new(&spec)
220 .with_global_config(&global_config)
221 .with_environment(Some("staging".to_string()));
222
223 assert_eq!(resolver.resolve(None), "https://staging.example.com");
224 });
225 }
226
227 #[test]
228 fn test_priority_config_override_beats_env_var() {
229 test_with_env_isolation(|| {
231 let spec = create_test_spec("test-api", Some("https://spec.example.com"));
232
233 std::env::set_var("APERTURE_BASE_URL", "https://env.example.com");
235
236 let mut api_configs = HashMap::new();
237 api_configs.insert(
238 "test-api".to_string(),
239 ApiConfig {
240 base_url_override: Some("https://config.example.com".to_string()),
241 environment_urls: HashMap::new(),
242 strict_mode: false,
243 },
244 );
245
246 let global_config = GlobalConfig {
247 api_configs,
248 ..Default::default()
249 };
250
251 let resolver = BaseUrlResolver::new(&spec).with_global_config(&global_config);
252
253 assert_eq!(resolver.resolve(None), "https://config.example.com");
255 });
256 }
257
258 #[test]
259 fn test_priority_3_env_var() {
260 test_with_env_isolation(|| {
262 let spec = create_test_spec("test-api", Some("https://spec.example.com"));
263
264 std::env::set_var("APERTURE_BASE_URL", "https://env.example.com");
266
267 let resolver = BaseUrlResolver::new(&spec);
268
269 assert_eq!(resolver.resolve(None), "https://env.example.com");
270 });
271 }
272
273 #[test]
274 fn test_priority_4_spec_default() {
275 test_with_env_isolation(|| {
276 let spec = create_test_spec("test-api", Some("https://spec.example.com"));
277 let resolver = BaseUrlResolver::new(&spec);
278
279 assert_eq!(resolver.resolve(None), "https://spec.example.com");
280 });
281 }
282
283 #[test]
284 fn test_priority_5_fallback() {
285 test_with_env_isolation(|| {
286 let spec = create_test_spec("test-api", None);
287 let resolver = BaseUrlResolver::new(&spec);
288
289 assert_eq!(resolver.resolve(None), "https://api.example.com");
290 });
291 }
292}