reinhardt_rest/versioning/
config.rs1use super::{
7 AcceptHeaderVersioning, HostNameVersioning, NamespaceVersioning, QueryParameterVersioning,
8 URLPathVersioning,
9};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13
14#[non_exhaustive]
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct VersioningConfig {
18 pub default_version: String,
20
21 pub allowed_versions: Vec<String>,
23
24 pub strategy: VersioningStrategy,
26
27 pub strict_mode: bool,
29
30 pub version_param: Option<String>,
32
33 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#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(tag = "type", content = "config")]
53pub enum VersioningStrategy {
54 AcceptHeader,
56
57 URLPath {
59 pattern: Option<String>,
61 },
62
63 QueryParameter {
65 param_name: Option<String>,
67 },
68
69 HostName {
71 patterns: Option<HashMap<String, String>>,
73 },
74
75 Namespace {
77 pattern: Option<String>,
79 },
80}
81
82impl VersioningConfig {
83 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
90 self.default_version = version.into();
91 self
92 }
93
94 pub fn with_allowed_versions(mut self, versions: Vec<String>) -> Self {
96 self.allowed_versions = versions;
97 self
98 }
99
100 pub fn with_strategy(mut self, strategy: VersioningStrategy) -> Self {
102 self.strategy = strategy;
103 self
104 }
105
106 pub fn with_strict_mode(mut self, strict: bool) -> Self {
108 self.strict_mode = strict;
109 self
110 }
111
112 pub fn with_version_param(mut self, param: impl Into<String>) -> Self {
114 self.version_param = Some(param.into());
115 self
116 }
117
118 pub fn with_hostname_patterns(mut self, patterns: HashMap<String, String>) -> Self {
120 self.hostname_patterns = Some(patterns);
121 self
122 }
123
124 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
222pub 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 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 pub fn config(&self) -> &VersioningConfig {
249 &self.config
250 }
251
252 pub fn versioning(&self) -> Arc<dyn super::BaseVersioning + Send + Sync> {
254 self.versioning.clone()
255 }
256
257 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 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 #[test]
358 fn test_from_settings_default_values() {
359 let settings = VersioningSettings::default();
361
362 let config = VersioningConfig::from(settings);
364
365 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 let settings = VersioningSettings {
378 default_version: "2.0".to_string(),
379 ..VersioningSettings::default()
380 };
381
382 let config = VersioningConfig::from(settings);
384
385 assert_eq!(config.default_version, "2.0");
387 }
388
389 #[test]
390 fn test_from_settings_allowed_versions() {
391 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 let config = VersioningConfig::from(settings);
399
400 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 let settings = VersioningSettings {
411 strategy: VersioningStrategy::URLPath { pattern: None },
412 ..VersioningSettings::default()
413 };
414
415 let config = VersioningConfig::from(settings);
417
418 assert!(matches!(
420 config.strategy,
421 VersioningStrategy::URLPath { .. }
422 ));
423 }
424
425 #[test]
426 fn test_from_settings_strategy_query_parameter() {
427 let settings = VersioningSettings {
429 strategy: VersioningStrategy::QueryParameter { param_name: None },
430 ..VersioningSettings::default()
431 };
432
433 let config = VersioningConfig::from(settings);
435
436 assert!(matches!(
438 config.strategy,
439 VersioningStrategy::QueryParameter { .. }
440 ));
441 }
442
443 #[test]
444 fn test_from_settings_strategy_hostname() {
445 let settings = VersioningSettings {
447 strategy: VersioningStrategy::HostName { patterns: None },
448 ..VersioningSettings::default()
449 };
450
451 let config = VersioningConfig::from(settings);
453
454 assert!(matches!(
456 config.strategy,
457 VersioningStrategy::HostName { .. }
458 ));
459 }
460
461 #[test]
462 fn test_from_settings_strategy_namespace() {
463 let settings = VersioningSettings {
465 strategy: VersioningStrategy::Namespace { pattern: None },
466 ..VersioningSettings::default()
467 };
468
469 let config = VersioningConfig::from(settings);
471
472 assert!(matches!(
474 config.strategy,
475 VersioningStrategy::Namespace { .. }
476 ));
477 }
478
479 #[test]
480 fn test_from_settings_strict_mode_false() {
481 let settings = VersioningSettings {
483 strict_mode: false,
484 ..VersioningSettings::default()
485 };
486
487 let config = VersioningConfig::from(settings);
489
490 assert!(!config.strict_mode);
492 }
493
494 #[test]
495 fn test_from_settings_strict_mode_true_explicit() {
496 let settings = VersioningSettings {
498 strict_mode: true,
499 ..VersioningSettings::default()
500 };
501
502 let config = VersioningConfig::from(settings);
504
505 assert!(config.strict_mode);
507 }
508
509 #[test]
510 fn test_from_settings_combined() {
511 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 let config = VersioningConfig::from(settings);
522
523 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 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 let config = VersioningConfig::from(settings);
548
549 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}