use super::{
AcceptHeaderVersioning, HostNameVersioning, NamespaceVersioning, QueryParameterVersioning,
URLPathVersioning,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersioningConfig {
pub default_version: String,
pub allowed_versions: Vec<String>,
pub strategy: VersioningStrategy,
pub strict_mode: bool,
pub version_param: Option<String>,
pub hostname_patterns: Option<HashMap<String, String>>,
}
impl Default for VersioningConfig {
fn default() -> Self {
Self {
default_version: "1.0".to_string(),
allowed_versions: vec![],
strategy: VersioningStrategy::AcceptHeader,
strict_mode: true,
version_param: None,
hostname_patterns: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "config")]
pub enum VersioningStrategy {
AcceptHeader,
URLPath {
pattern: Option<String>,
},
QueryParameter {
param_name: Option<String>,
},
HostName {
patterns: Option<HashMap<String, String>>,
},
Namespace {
pattern: Option<String>,
},
}
impl VersioningConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
self.default_version = version.into();
self
}
pub fn with_allowed_versions(mut self, versions: Vec<String>) -> Self {
self.allowed_versions = versions;
self
}
pub fn with_strategy(mut self, strategy: VersioningStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn with_strict_mode(mut self, strict: bool) -> Self {
self.strict_mode = strict;
self
}
pub fn with_version_param(mut self, param: impl Into<String>) -> Self {
self.version_param = Some(param.into());
self
}
pub fn with_hostname_patterns(mut self, patterns: HashMap<String, String>) -> Self {
self.hostname_patterns = Some(patterns);
self
}
pub fn create_versioning(&self) -> Box<dyn super::BaseVersioning + Send + Sync> {
match &self.strategy {
VersioningStrategy::AcceptHeader => {
let mut versioning =
AcceptHeaderVersioning::new().with_default_version(&self.default_version);
if !self.allowed_versions.is_empty() {
versioning = versioning.with_allowed_versions(self.allowed_versions.clone());
}
Box::new(versioning)
}
VersioningStrategy::URLPath { pattern } => {
let mut versioning =
URLPathVersioning::new().with_default_version(&self.default_version);
if let Some(p) = pattern {
versioning = versioning.with_pattern(p);
}
if !self.allowed_versions.is_empty() {
versioning = versioning.with_allowed_versions(self.allowed_versions.clone());
}
Box::new(versioning)
}
VersioningStrategy::QueryParameter { param_name } => {
let mut versioning =
QueryParameterVersioning::new().with_default_version(&self.default_version);
if let Some(name) = param_name {
versioning = versioning.with_version_param(name);
} else if let Some(name) = &self.version_param {
versioning = versioning.with_version_param(name);
}
if !self.allowed_versions.is_empty() {
versioning = versioning.with_allowed_versions(self.allowed_versions.clone());
}
Box::new(versioning)
}
VersioningStrategy::HostName { patterns } => {
let mut versioning =
HostNameVersioning::new().with_default_version(&self.default_version);
if let Some(p) = patterns {
for (version, hostname) in p {
versioning = versioning.with_hostname_pattern(version, hostname);
}
} else if let Some(p) = &self.hostname_patterns {
for (version, hostname) in p {
versioning = versioning.with_hostname_pattern(version, hostname);
}
}
if !self.allowed_versions.is_empty() {
versioning = versioning.with_allowed_versions(self.allowed_versions.clone());
}
Box::new(versioning)
}
VersioningStrategy::Namespace { pattern } => {
let mut versioning =
NamespaceVersioning::new().with_default_version(&self.default_version);
if let Some(p) = pattern {
versioning = versioning.with_pattern(p);
}
if !self.allowed_versions.is_empty() {
versioning = versioning.with_allowed_versions(self.allowed_versions.clone());
}
Box::new(versioning)
}
}
}
}
impl From<super::settings::VersioningSettings> for VersioningConfig {
fn from(settings: super::settings::VersioningSettings) -> Self {
Self {
default_version: settings.default_version,
allowed_versions: settings.allowed_versions,
strategy: settings.strategy,
strict_mode: settings.strict_mode,
version_param: settings.version_param,
hostname_patterns: settings.hostname_patterns,
}
}
}
pub struct VersioningManager {
config: VersioningConfig,
versioning: Arc<dyn super::BaseVersioning + Send + Sync>,
}
impl std::fmt::Debug for VersioningManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VersioningManager")
.field("config", &self.config)
.field("versioning", &"<dyn BaseVersioning>")
.finish()
}
}
impl VersioningManager {
pub fn new(config: VersioningConfig) -> Self {
let versioning = config.create_versioning();
Self {
config,
versioning: Arc::from(versioning),
}
}
pub fn config(&self) -> &VersioningConfig {
&self.config
}
pub fn versioning(&self) -> Arc<dyn super::BaseVersioning + Send + Sync> {
self.versioning.clone()
}
pub fn update_config(&mut self, config: VersioningConfig) {
self.config = config;
self.versioning = Arc::from(self.config.create_versioning());
}
}
impl Default for VersioningManager {
fn default() -> Self {
Self::new(VersioningConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::versioning::settings::VersioningSettings;
use std::collections::HashMap;
#[test]
fn test_versioning_config_default() {
let config = VersioningConfig::default();
assert_eq!(config.default_version, "1.0");
assert!(config.allowed_versions.is_empty());
assert!(matches!(config.strategy, VersioningStrategy::AcceptHeader));
assert!(config.strict_mode);
}
#[test]
fn test_versioning_config_builder() {
let config = VersioningConfig::new()
.with_default_version("2.0")
.with_allowed_versions(vec!["2.0".to_string(), "3.0".to_string()])
.with_strategy(VersioningStrategy::URLPath { pattern: None })
.with_strict_mode(false);
assert_eq!(config.default_version, "2.0");
assert_eq!(config.allowed_versions, vec!["2.0", "3.0"]);
assert!(matches!(
config.strategy,
VersioningStrategy::URLPath { .. }
));
assert!(!config.strict_mode);
}
#[test]
fn test_versioning_strategy_serialization() {
let strategy = VersioningStrategy::QueryParameter {
param_name: Some("v".to_string()),
};
let json = serde_json::to_string(&strategy).unwrap();
let deserialized: VersioningStrategy = serde_json::from_str(&json).unwrap();
match deserialized {
VersioningStrategy::QueryParameter { param_name } => {
assert_eq!(param_name, Some("v".to_string()));
}
_ => panic!("Expected QueryParameter strategy"),
}
}
#[test]
fn test_versioning_manager_creation() {
let config = VersioningConfig::new()
.with_default_version("1.0")
.with_strategy(VersioningStrategy::AcceptHeader);
let manager = VersioningManager::new(config);
assert_eq!(manager.config().default_version, "1.0");
}
#[tokio::test]
async fn test_hostname_patterns() {
let mut patterns = HashMap::new();
patterns.insert("v1".to_string(), "v1.api.example.com".to_string());
patterns.insert("v2".to_string(), "v2.api.example.com".to_string());
let config = VersioningConfig::new().with_strategy(VersioningStrategy::HostName {
patterns: Some(patterns.clone()),
});
let versioning = config.create_versioning();
assert!(
versioning
.determine_version(&crate::versioning::test_utils::create_test_request(
"/",
vec![]
))
.await
.is_ok()
);
}
#[test]
fn test_from_settings_default_values() {
let settings = VersioningSettings::default();
let config = VersioningConfig::from(settings);
assert_eq!(config.default_version, "1.0");
assert!(config.allowed_versions.is_empty());
assert!(matches!(config.strategy, VersioningStrategy::AcceptHeader));
assert!(config.strict_mode);
assert!(config.version_param.is_none());
assert!(config.hostname_patterns.is_none());
}
#[test]
fn test_from_settings_custom_default_version() {
let settings = VersioningSettings {
default_version: "2.0".to_string(),
..VersioningSettings::default()
};
let config = VersioningConfig::from(settings);
assert_eq!(config.default_version, "2.0");
}
#[test]
fn test_from_settings_allowed_versions() {
let settings = VersioningSettings {
allowed_versions: vec!["1.0".to_string(), "2.0".to_string(), "3.0".to_string()],
..VersioningSettings::default()
};
let config = VersioningConfig::from(settings);
assert_eq!(config.allowed_versions.len(), 3);
assert!(config.allowed_versions.contains(&"1.0".to_string()));
assert!(config.allowed_versions.contains(&"2.0".to_string()));
assert!(config.allowed_versions.contains(&"3.0".to_string()));
}
#[test]
fn test_from_settings_strategy_url_path() {
let settings = VersioningSettings {
strategy: VersioningStrategy::URLPath { pattern: None },
..VersioningSettings::default()
};
let config = VersioningConfig::from(settings);
assert!(matches!(
config.strategy,
VersioningStrategy::URLPath { .. }
));
}
#[test]
fn test_from_settings_strategy_query_parameter() {
let settings = VersioningSettings {
strategy: VersioningStrategy::QueryParameter { param_name: None },
..VersioningSettings::default()
};
let config = VersioningConfig::from(settings);
assert!(matches!(
config.strategy,
VersioningStrategy::QueryParameter { .. }
));
}
#[test]
fn test_from_settings_strategy_hostname() {
let settings = VersioningSettings {
strategy: VersioningStrategy::HostName { patterns: None },
..VersioningSettings::default()
};
let config = VersioningConfig::from(settings);
assert!(matches!(
config.strategy,
VersioningStrategy::HostName { .. }
));
}
#[test]
fn test_from_settings_strategy_namespace() {
let settings = VersioningSettings {
strategy: VersioningStrategy::Namespace { pattern: None },
..VersioningSettings::default()
};
let config = VersioningConfig::from(settings);
assert!(matches!(
config.strategy,
VersioningStrategy::Namespace { .. }
));
}
#[test]
fn test_from_settings_strict_mode_false() {
let settings = VersioningSettings {
strict_mode: false,
..VersioningSettings::default()
};
let config = VersioningConfig::from(settings);
assert!(!config.strict_mode);
}
#[test]
fn test_from_settings_strict_mode_true_explicit() {
let settings = VersioningSettings {
strict_mode: true,
..VersioningSettings::default()
};
let config = VersioningConfig::from(settings);
assert!(config.strict_mode);
}
#[test]
fn test_from_settings_combined() {
let settings = VersioningSettings {
default_version: "3.0".to_string(),
allowed_versions: vec!["2.0".to_string(), "3.0".to_string(), "4.0".to_string()],
strategy: VersioningStrategy::URLPath { pattern: None },
strict_mode: false,
..VersioningSettings::default()
};
let config = VersioningConfig::from(settings);
assert_eq!(config.default_version, "3.0");
assert_eq!(config.allowed_versions.len(), 3);
assert!(matches!(
config.strategy,
VersioningStrategy::URLPath { .. }
));
assert!(!config.strict_mode);
}
#[test]
fn test_from_settings_preserves_optional_fields() {
let mut hostname_patterns = HashMap::new();
hostname_patterns.insert("v1".to_string(), "v1.example.com".to_string());
let settings = VersioningSettings {
version_param: Some("v".to_string()),
hostname_patterns: Some(hostname_patterns.clone()),
..VersioningSettings::default()
};
let config = VersioningConfig::from(settings);
assert_eq!(config.version_param.as_deref(), Some("v"));
assert_eq!(config.hostname_patterns.as_ref().map(|p| p.len()), Some(1));
}
}