Skip to main content

reinhardt_rest/versioning/
config.rs

1//! Global configuration system for API versioning
2//!
3//! Provides centralized configuration management for versioning strategies,
4//! allowing applications to configure versioning behavior through settings.
5
6use super::{
7	AcceptHeaderVersioning, HostNameVersioning, NamespaceVersioning, QueryParameterVersioning,
8	URLPathVersioning,
9};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13
14/// Global versioning configuration
15#[non_exhaustive]
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct VersioningConfig {
18	/// Default version to use when no version is specified
19	pub default_version: String,
20
21	/// Allowed versions (empty means any version is allowed)
22	pub allowed_versions: Vec<String>,
23
24	/// Versioning strategy configuration
25	pub strategy: VersioningStrategy,
26
27	/// Whether to raise errors for invalid versions
28	pub strict_mode: bool,
29
30	/// Custom version parameter name for query parameter versioning
31	pub version_param: Option<String>,
32
33	/// Custom hostname patterns for hostname versioning
34	pub hostname_patterns: Option<HashMap<String, String>>,
35}
36
37impl Default for VersioningConfig {
38	fn default() -> Self {
39		Self {
40			default_version: "1.0".to_string(),
41			allowed_versions: vec![],
42			strategy: VersioningStrategy::AcceptHeader,
43			strict_mode: true,
44			version_param: None,
45			hostname_patterns: None,
46		}
47	}
48}
49
50/// Versioning strategy configuration
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(tag = "type", content = "config")]
53pub enum VersioningStrategy {
54	/// Accept header versioning
55	AcceptHeader,
56
57	/// URL path versioning
58	URLPath {
59		/// URL pattern for version extraction (e.g., "/v{version}/")
60		pattern: Option<String>,
61	},
62
63	/// Query parameter versioning
64	QueryParameter {
65		/// Parameter name (default: "version")
66		param_name: Option<String>,
67	},
68
69	/// Hostname versioning
70	HostName {
71		/// Hostname patterns mapping versions to hostnames
72		patterns: Option<HashMap<String, String>>,
73	},
74
75	/// Namespace versioning
76	Namespace {
77		/// Namespace pattern (e.g., "/v{version}/")
78		pattern: Option<String>,
79	},
80}
81
82impl VersioningConfig {
83	/// Create a new versioning configuration
84	pub fn new() -> Self {
85		Self::default()
86	}
87
88	/// Set the default version
89	pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
90		self.default_version = version.into();
91		self
92	}
93
94	/// Set allowed versions
95	pub fn with_allowed_versions(mut self, versions: Vec<String>) -> Self {
96		self.allowed_versions = versions;
97		self
98	}
99
100	/// Set versioning strategy
101	pub fn with_strategy(mut self, strategy: VersioningStrategy) -> Self {
102		self.strategy = strategy;
103		self
104	}
105
106	/// Enable or disable strict mode
107	pub fn with_strict_mode(mut self, strict: bool) -> Self {
108		self.strict_mode = strict;
109		self
110	}
111
112	/// Set custom version parameter name
113	pub fn with_version_param(mut self, param: impl Into<String>) -> Self {
114		self.version_param = Some(param.into());
115		self
116	}
117
118	/// Set hostname patterns
119	pub fn with_hostname_patterns(mut self, patterns: HashMap<String, String>) -> Self {
120		self.hostname_patterns = Some(patterns);
121		self
122	}
123
124	/// Create versioning instance from configuration
125	pub fn create_versioning(&self) -> Box<dyn super::BaseVersioning + Send + Sync> {
126		match &self.strategy {
127			VersioningStrategy::AcceptHeader => {
128				let mut versioning =
129					AcceptHeaderVersioning::new().with_default_version(&self.default_version);
130
131				if !self.allowed_versions.is_empty() {
132					versioning = versioning.with_allowed_versions(self.allowed_versions.clone());
133				}
134
135				Box::new(versioning)
136			}
137
138			VersioningStrategy::URLPath { pattern } => {
139				let mut versioning =
140					URLPathVersioning::new().with_default_version(&self.default_version);
141
142				if let Some(p) = pattern {
143					versioning = versioning.with_pattern(p);
144				}
145
146				if !self.allowed_versions.is_empty() {
147					versioning = versioning.with_allowed_versions(self.allowed_versions.clone());
148				}
149
150				Box::new(versioning)
151			}
152
153			VersioningStrategy::QueryParameter { param_name } => {
154				let mut versioning =
155					QueryParameterVersioning::new().with_default_version(&self.default_version);
156
157				if let Some(name) = param_name {
158					versioning = versioning.with_version_param(name);
159				} else if let Some(name) = &self.version_param {
160					versioning = versioning.with_version_param(name);
161				}
162
163				if !self.allowed_versions.is_empty() {
164					versioning = versioning.with_allowed_versions(self.allowed_versions.clone());
165				}
166
167				Box::new(versioning)
168			}
169
170			VersioningStrategy::HostName { patterns } => {
171				let mut versioning =
172					HostNameVersioning::new().with_default_version(&self.default_version);
173
174				if let Some(p) = patterns {
175					for (version, hostname) in p {
176						versioning = versioning.with_hostname_pattern(version, hostname);
177					}
178				} else if let Some(p) = &self.hostname_patterns {
179					for (version, hostname) in p {
180						versioning = versioning.with_hostname_pattern(version, hostname);
181					}
182				}
183
184				if !self.allowed_versions.is_empty() {
185					versioning = versioning.with_allowed_versions(self.allowed_versions.clone());
186				}
187
188				Box::new(versioning)
189			}
190
191			VersioningStrategy::Namespace { pattern } => {
192				let mut versioning =
193					NamespaceVersioning::new().with_default_version(&self.default_version);
194
195				if let Some(p) = pattern {
196					versioning = versioning.with_pattern(p);
197				}
198
199				if !self.allowed_versions.is_empty() {
200					versioning = versioning.with_allowed_versions(self.allowed_versions.clone());
201				}
202
203				Box::new(versioning)
204			}
205		}
206	}
207}
208
209impl From<super::settings::VersioningSettings> for VersioningConfig {
210	fn from(settings: super::settings::VersioningSettings) -> Self {
211		Self {
212			default_version: settings.default_version,
213			allowed_versions: settings.allowed_versions,
214			strategy: settings.strategy,
215			strict_mode: settings.strict_mode,
216			version_param: settings.version_param,
217			hostname_patterns: settings.hostname_patterns,
218		}
219	}
220}
221
222/// Global versioning manager
223pub struct VersioningManager {
224	config: VersioningConfig,
225	versioning: Arc<dyn super::BaseVersioning + Send + Sync>,
226}
227
228impl std::fmt::Debug for VersioningManager {
229	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230		f.debug_struct("VersioningManager")
231			.field("config", &self.config)
232			.field("versioning", &"<dyn BaseVersioning>")
233			.finish()
234	}
235}
236
237impl VersioningManager {
238	/// Create a new versioning manager with configuration
239	pub fn new(config: VersioningConfig) -> Self {
240		let versioning = config.create_versioning();
241		Self {
242			config,
243			versioning: Arc::from(versioning),
244		}
245	}
246
247	/// Get the current configuration
248	pub fn config(&self) -> &VersioningConfig {
249		&self.config
250	}
251
252	/// Get the versioning instance
253	pub fn versioning(&self) -> Arc<dyn super::BaseVersioning + Send + Sync> {
254		self.versioning.clone()
255	}
256
257	/// Update configuration and recreate versioning instance
258	pub fn update_config(&mut self, config: VersioningConfig) {
259		self.config = config;
260		self.versioning = Arc::from(self.config.create_versioning());
261	}
262}
263
264impl Default for VersioningManager {
265	fn default() -> Self {
266		Self::new(VersioningConfig::default())
267	}
268}
269
270#[cfg(test)]
271mod tests {
272	use super::*;
273	use crate::versioning::settings::VersioningSettings;
274	use std::collections::HashMap;
275
276	#[test]
277	fn test_versioning_config_default() {
278		let config = VersioningConfig::default();
279		assert_eq!(config.default_version, "1.0");
280		assert!(config.allowed_versions.is_empty());
281		assert!(matches!(config.strategy, VersioningStrategy::AcceptHeader));
282		assert!(config.strict_mode);
283	}
284
285	#[test]
286	fn test_versioning_config_builder() {
287		let config = VersioningConfig::new()
288			.with_default_version("2.0")
289			.with_allowed_versions(vec!["2.0".to_string(), "3.0".to_string()])
290			.with_strategy(VersioningStrategy::URLPath { pattern: None })
291			.with_strict_mode(false);
292
293		assert_eq!(config.default_version, "2.0");
294		assert_eq!(config.allowed_versions, vec!["2.0", "3.0"]);
295		assert!(matches!(
296			config.strategy,
297			VersioningStrategy::URLPath { .. }
298		));
299		assert!(!config.strict_mode);
300	}
301
302	#[test]
303	fn test_versioning_strategy_serialization() {
304		let strategy = VersioningStrategy::QueryParameter {
305			param_name: Some("v".to_string()),
306		};
307
308		let json = serde_json::to_string(&strategy).unwrap();
309		let deserialized: VersioningStrategy = serde_json::from_str(&json).unwrap();
310
311		match deserialized {
312			VersioningStrategy::QueryParameter { param_name } => {
313				assert_eq!(param_name, Some("v".to_string()));
314			}
315			_ => panic!("Expected QueryParameter strategy"),
316		}
317	}
318
319	#[test]
320	fn test_versioning_manager_creation() {
321		let config = VersioningConfig::new()
322			.with_default_version("1.0")
323			.with_strategy(VersioningStrategy::AcceptHeader);
324
325		let manager = VersioningManager::new(config);
326		assert_eq!(manager.config().default_version, "1.0");
327	}
328
329	#[tokio::test]
330	async fn test_hostname_patterns() {
331		let mut patterns = HashMap::new();
332		patterns.insert("v1".to_string(), "v1.api.example.com".to_string());
333		patterns.insert("v2".to_string(), "v2.api.example.com".to_string());
334
335		let config = VersioningConfig::new().with_strategy(VersioningStrategy::HostName {
336			patterns: Some(patterns.clone()),
337		});
338
339		let versioning = config.create_versioning();
340		// The versioning instance should be created successfully
341		assert!(
342			versioning
343				.determine_version(&crate::versioning::test_utils::create_test_request(
344					"/",
345					vec![]
346				))
347				.await
348				.is_ok()
349		);
350	}
351
352	// --------------------------------------------------------------------
353	// Settings-based construction tests (replaces the prior `from_env`
354	// tests, which exercised env-var parsing that no longer exists).
355	// --------------------------------------------------------------------
356
357	#[test]
358	fn test_from_settings_default_values() {
359		// Arrange
360		let settings = VersioningSettings::default();
361
362		// Act
363		let config = VersioningConfig::from(settings);
364
365		// Assert
366		assert_eq!(config.default_version, "1.0");
367		assert!(config.allowed_versions.is_empty());
368		assert!(matches!(config.strategy, VersioningStrategy::AcceptHeader));
369		assert!(config.strict_mode);
370		assert!(config.version_param.is_none());
371		assert!(config.hostname_patterns.is_none());
372	}
373
374	#[test]
375	fn test_from_settings_custom_default_version() {
376		// Arrange
377		let settings = VersioningSettings {
378			default_version: "2.0".to_string(),
379			..VersioningSettings::default()
380		};
381
382		// Act
383		let config = VersioningConfig::from(settings);
384
385		// Assert
386		assert_eq!(config.default_version, "2.0");
387	}
388
389	#[test]
390	fn test_from_settings_allowed_versions() {
391		// Arrange
392		let settings = VersioningSettings {
393			allowed_versions: vec!["1.0".to_string(), "2.0".to_string(), "3.0".to_string()],
394			..VersioningSettings::default()
395		};
396
397		// Act
398		let config = VersioningConfig::from(settings);
399
400		// Assert
401		assert_eq!(config.allowed_versions.len(), 3);
402		assert!(config.allowed_versions.contains(&"1.0".to_string()));
403		assert!(config.allowed_versions.contains(&"2.0".to_string()));
404		assert!(config.allowed_versions.contains(&"3.0".to_string()));
405	}
406
407	#[test]
408	fn test_from_settings_strategy_url_path() {
409		// Arrange
410		let settings = VersioningSettings {
411			strategy: VersioningStrategy::URLPath { pattern: None },
412			..VersioningSettings::default()
413		};
414
415		// Act
416		let config = VersioningConfig::from(settings);
417
418		// Assert
419		assert!(matches!(
420			config.strategy,
421			VersioningStrategy::URLPath { .. }
422		));
423	}
424
425	#[test]
426	fn test_from_settings_strategy_query_parameter() {
427		// Arrange
428		let settings = VersioningSettings {
429			strategy: VersioningStrategy::QueryParameter { param_name: None },
430			..VersioningSettings::default()
431		};
432
433		// Act
434		let config = VersioningConfig::from(settings);
435
436		// Assert
437		assert!(matches!(
438			config.strategy,
439			VersioningStrategy::QueryParameter { .. }
440		));
441	}
442
443	#[test]
444	fn test_from_settings_strategy_hostname() {
445		// Arrange
446		let settings = VersioningSettings {
447			strategy: VersioningStrategy::HostName { patterns: None },
448			..VersioningSettings::default()
449		};
450
451		// Act
452		let config = VersioningConfig::from(settings);
453
454		// Assert
455		assert!(matches!(
456			config.strategy,
457			VersioningStrategy::HostName { .. }
458		));
459	}
460
461	#[test]
462	fn test_from_settings_strategy_namespace() {
463		// Arrange
464		let settings = VersioningSettings {
465			strategy: VersioningStrategy::Namespace { pattern: None },
466			..VersioningSettings::default()
467		};
468
469		// Act
470		let config = VersioningConfig::from(settings);
471
472		// Assert
473		assert!(matches!(
474			config.strategy,
475			VersioningStrategy::Namespace { .. }
476		));
477	}
478
479	#[test]
480	fn test_from_settings_strict_mode_false() {
481		// Arrange
482		let settings = VersioningSettings {
483			strict_mode: false,
484			..VersioningSettings::default()
485		};
486
487		// Act
488		let config = VersioningConfig::from(settings);
489
490		// Assert
491		assert!(!config.strict_mode);
492	}
493
494	#[test]
495	fn test_from_settings_strict_mode_true_explicit() {
496		// Arrange
497		let settings = VersioningSettings {
498			strict_mode: true,
499			..VersioningSettings::default()
500		};
501
502		// Act
503		let config = VersioningConfig::from(settings);
504
505		// Assert
506		assert!(config.strict_mode);
507	}
508
509	#[test]
510	fn test_from_settings_combined() {
511		// Arrange
512		let settings = VersioningSettings {
513			default_version: "3.0".to_string(),
514			allowed_versions: vec!["2.0".to_string(), "3.0".to_string(), "4.0".to_string()],
515			strategy: VersioningStrategy::URLPath { pattern: None },
516			strict_mode: false,
517			..VersioningSettings::default()
518		};
519
520		// Act
521		let config = VersioningConfig::from(settings);
522
523		// Assert
524		assert_eq!(config.default_version, "3.0");
525		assert_eq!(config.allowed_versions.len(), 3);
526		assert!(matches!(
527			config.strategy,
528			VersioningStrategy::URLPath { .. }
529		));
530		assert!(!config.strict_mode);
531	}
532
533	#[test]
534	fn test_from_settings_preserves_optional_fields() {
535		// Arrange — exercise the path that previously was not covered by
536		// the env-based tests (which always left these as None).
537		let mut hostname_patterns = HashMap::new();
538		hostname_patterns.insert("v1".to_string(), "v1.example.com".to_string());
539
540		let settings = VersioningSettings {
541			version_param: Some("v".to_string()),
542			hostname_patterns: Some(hostname_patterns.clone()),
543			..VersioningSettings::default()
544		};
545
546		// Act
547		let config = VersioningConfig::from(settings);
548
549		// Assert
550		assert_eq!(config.version_param.as_deref(), Some("v"));
551		assert_eq!(config.hostname_patterns.as_ref().map(|p| p.len()), Some(1));
552	}
553}