Skip to main content

reinhardt_rest/
versioning.rs

1//! # Reinhardt Versioning
2//!
3//! API versioning strategies for Reinhardt framework.
4//!
5//! ## Features
6//!
7//! - **AcceptHeaderVersioning**: Version from Accept header (e.g., `Accept: application/json; version=1.0`)
8//! - **URLPathVersioning**: Version from URL path (e.g., `/v1/users/`)
9//! - **NamespaceVersioning**: Version from URL namespace
10//! - **HostNameVersioning**: Version from subdomain (e.g., `v1.api.example.com`)
11//! - **QueryParameterVersioning**: Version from query parameter (e.g., `?version=1.0`)
12//! - **VersioningMiddleware**: Automatic version detection middleware
13//!
14//! ## Example
15//!
16//! ```rust
17//! use reinhardt_rest::versioning::{BaseVersioning, AcceptHeaderVersioning, QueryParameterVersioning};
18//! use reinhardt_rest::versioning::{VersioningMiddleware, RequestVersionExt};
19//!
20//! // Accept header versioning
21//! let accept_versioning = AcceptHeaderVersioning::new()
22//!     .with_default_version("1.0")
23//!     .with_allowed_versions(vec!["1.0", "2.0"]);
24//!
25//! // Query parameter versioning
26//! let query_versioning = QueryParameterVersioning::new()
27//!     .with_version_param("v")
28//!     .with_default_version("1.0");
29//!
30//! // Middleware for automatic version detection
31//! let middleware = VersioningMiddleware::new(accept_versioning);
32//! ```
33
34pub mod config;
35pub mod handler;
36pub mod middleware;
37pub mod reverse;
38
39use async_trait::async_trait;
40pub use config::{VersioningConfig, VersioningManager, VersioningStrategy};
41pub use handler::{
42	ConfigurableVersionedHandler, SimpleVersionedHandler, VersionResponseBuilder, VersionedHandler,
43	VersionedHandlerBuilder, VersionedHandlerWrapper,
44};
45pub use middleware::{ApiVersion, RequestVersionExt, VersioningMiddleware};
46use regex::Regex;
47use reinhardt_core::exception::{Error, Result};
48use reinhardt_http::Request;
49pub use reverse::{
50	ApiDocFormat, ApiDocUrlBuilder, UrlReverseManager, VersionedUrlBuilder,
51	VersioningStrategy as ReverseVersioningStrategy,
52};
53use std::collections::{HashMap, HashSet};
54use std::sync::OnceLock;
55use thiserror::Error as ThisError;
56
57/// Errors that can occur during API version determination.
58#[derive(Debug, ThisError)]
59pub enum VersioningError {
60	/// The Accept header does not contain a valid version.
61	#[error("Invalid version in Accept header")]
62	InvalidAcceptHeader,
63
64	/// The URL path does not contain a valid version segment.
65	#[error("Invalid version in URL path")]
66	InvalidURLPath,
67
68	/// The URL namespace does not contain a valid version.
69	#[error("Invalid version in URL namespace")]
70	InvalidNamespace,
71
72	/// The hostname does not contain a valid version subdomain.
73	#[error("Invalid version in hostname")]
74	InvalidHostname,
75
76	/// The query parameter does not contain a valid version.
77	#[error("Invalid version in query parameter")]
78	InvalidQueryParameter,
79
80	/// The requested version is not in the allowed versions list.
81	#[error("Version not allowed: {0}")]
82	VersionNotAllowed(String),
83}
84
85/// Base trait for API versioning strategies
86#[async_trait]
87pub trait BaseVersioning: Send + Sync {
88	/// Determine the API version from the request
89	async fn determine_version(&self, request: &Request) -> Result<String>;
90
91	/// Get the default version
92	fn default_version(&self) -> Option<&str>;
93
94	/// Get allowed versions
95	fn allowed_versions(&self) -> Option<&HashSet<String>>;
96
97	/// Check if a version is allowed
98	fn is_allowed_version(&self, version: &str) -> bool {
99		if let Some(allowed) = self.allowed_versions() {
100			if allowed.is_empty() {
101				return true;
102			}
103			return allowed.contains(version) || (self.default_version() == Some(version));
104		}
105		true
106	}
107
108	/// Get the version parameter name
109	fn version_param(&self) -> &str {
110		"version"
111	}
112}
113
114/// Accept header versioning
115///
116/// Example: `Accept: application/json; version=1.0`
117#[derive(Debug, Clone)]
118pub struct AcceptHeaderVersioning {
119	/// The fallback version when no version is specified in the Accept header.
120	pub default_version: Option<String>,
121	/// The set of allowed API versions.
122	pub allowed_versions: HashSet<String>,
123	/// The parameter name to look for in the Accept header (default: `"version"`).
124	pub version_param: String,
125}
126
127impl AcceptHeaderVersioning {
128	/// Create a new AcceptHeaderVersioning instance
129	///
130	/// # Examples
131	///
132	/// ```
133	/// use reinhardt_rest::versioning::{AcceptHeaderVersioning, BaseVersioning};
134	///
135	/// let versioning = AcceptHeaderVersioning::new();
136	/// assert_eq!(versioning.default_version.as_deref(), None);
137	/// assert_eq!(versioning.version_param.as_str(), "version");
138	/// ```
139	pub fn new() -> Self {
140		Self {
141			default_version: None,
142			allowed_versions: HashSet::new(),
143			version_param: "version".to_string(),
144		}
145	}
146	/// Set the default version to use when no version is specified
147	///
148	/// # Examples
149	///
150	/// ```
151	/// use reinhardt_rest::versioning::{AcceptHeaderVersioning, BaseVersioning};
152	///
153	/// let versioning = AcceptHeaderVersioning::new()
154	///     .with_default_version("1.0");
155	/// assert_eq!(versioning.default_version.as_deref(), Some("1.0"));
156	/// ```
157	pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
158		self.default_version = Some(version.into());
159		self
160	}
161	/// Set the allowed versions
162	///
163	/// # Examples
164	///
165	/// ```
166	/// use reinhardt_rest::versioning::{AcceptHeaderVersioning, BaseVersioning};
167	///
168	/// let versioning = AcceptHeaderVersioning::new()
169	///     .with_allowed_versions(vec!["1.0", "2.0", "3.0"]);
170	/// assert!(versioning.is_allowed_version("1.0"));
171	/// assert!(versioning.is_allowed_version("2.0"));
172	/// assert!(!versioning.is_allowed_version("4.0"));
173	/// ```
174	pub fn with_allowed_versions(mut self, versions: Vec<impl Into<String>>) -> Self {
175		self.allowed_versions = versions.into_iter().map(|v| v.into()).collect();
176		self
177	}
178	/// Set the version parameter name to look for in the Accept header
179	///
180	/// # Examples
181	///
182	/// ```
183	/// use reinhardt_rest::versioning::{AcceptHeaderVersioning, BaseVersioning};
184	///
185	/// let versioning = AcceptHeaderVersioning::new()
186	///     .with_version_param("api-version");
187	/// assert_eq!(versioning.version_param.as_str(), "api-version");
188	/// ```
189	pub fn with_version_param(mut self, param: impl Into<String>) -> Self {
190		self.version_param = param.into();
191		self
192	}
193}
194
195impl Default for AcceptHeaderVersioning {
196	fn default() -> Self {
197		Self::new()
198	}
199}
200
201#[async_trait]
202impl BaseVersioning for AcceptHeaderVersioning {
203	async fn determine_version(&self, request: &Request) -> Result<String> {
204		// Parse Accept header for version parameter
205		if let Some(accept) = request.headers.get("accept") {
206			let accept_str = accept
207				.to_str()
208				.map_err(|_| Error::Validation(VersioningError::InvalidAcceptHeader.to_string()))?;
209
210			// Parse media type parameters
211			if let Some(params_start) = accept_str.find(';') {
212				let params = &accept_str[params_start + 1..];
213				for param in params.split(';') {
214					let param = param.trim();
215					if let Some((key, value)) = param.split_once('=')
216						&& key.trim() == self.version_param
217					{
218						let version = value.trim().trim_matches('"');
219						if self.is_allowed_version(version) {
220							return Ok(version.to_string());
221						} else {
222							return Err(Error::Validation(
223								VersioningError::VersionNotAllowed(version.to_string()).to_string(),
224							));
225						}
226					}
227				}
228			}
229		}
230
231		// Return default version if no version in header
232		Ok(self
233			.default_version
234			.clone()
235			.unwrap_or_else(|| "1.0".to_string()))
236	}
237
238	fn default_version(&self) -> Option<&str> {
239		self.default_version.as_deref()
240	}
241
242	fn allowed_versions(&self) -> Option<&HashSet<String>> {
243		Some(&self.allowed_versions)
244	}
245
246	fn version_param(&self) -> &str {
247		&self.version_param
248	}
249}
250
251/// URL path versioning
252///
253/// Example: `/v1/users/` or `/api/v2/users/`
254#[derive(Debug, Clone)]
255pub struct URLPathVersioning {
256	/// The fallback version when no version is found in the URL path.
257	pub default_version: Option<String>,
258	/// The set of allowed API versions.
259	pub allowed_versions: HashSet<String>,
260	/// The parameter name for version (default: `"version"`).
261	pub version_param: String,
262	/// The regex pattern used to extract the version from the URL path.
263	pub path_regex: Regex,
264}
265
266impl URLPathVersioning {
267	/// Create a new URLPathVersioning instance
268	///
269	/// # Examples
270	///
271	/// ```
272	/// use reinhardt_rest::versioning::{URLPathVersioning, BaseVersioning};
273	///
274	/// let versioning = URLPathVersioning::new();
275	/// assert_eq!(versioning.default_version.as_deref(), None);
276	/// ```
277	pub fn new() -> Self {
278		Self {
279			default_version: None,
280			allowed_versions: HashSet::new(),
281			version_param: "version".to_string(),
282			path_regex: Regex::new(r"/v(\d+\.?\d*)(?:/|$)").unwrap(),
283		}
284	}
285	/// Set the default version to use when no version is found in the path
286	///
287	/// # Examples
288	///
289	/// ```
290	/// use reinhardt_rest::versioning::{URLPathVersioning, BaseVersioning};
291	///
292	/// let versioning = URLPathVersioning::new()
293	///     .with_default_version("1.0");
294	/// assert_eq!(versioning.default_version.as_deref(), Some("1.0"));
295	/// ```
296	pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
297		self.default_version = Some(version.into());
298		self
299	}
300	/// Set the allowed versions
301	///
302	/// # Examples
303	///
304	/// ```
305	/// use reinhardt_rest::versioning::{URLPathVersioning, BaseVersioning};
306	///
307	/// let versioning = URLPathVersioning::new()
308	///     .with_allowed_versions(vec!["1", "2", "3"]);
309	/// assert!(versioning.is_allowed_version("1"));
310	/// assert!(!versioning.is_allowed_version("99"));
311	/// ```
312	pub fn with_allowed_versions(mut self, versions: Vec<impl Into<String>>) -> Self {
313		self.allowed_versions = versions.into_iter().map(|v| v.into()).collect();
314		self
315	}
316	/// Set the version parameter name (for trait compatibility)
317	///
318	/// # Examples
319	///
320	/// ```
321	/// use reinhardt_rest::versioning::{URLPathVersioning, BaseVersioning};
322	///
323	/// let versioning = URLPathVersioning::new()
324	///     .with_version_param("v");
325	/// assert_eq!(versioning.version_param.as_str(), "v");
326	/// ```
327	pub fn with_version_param(mut self, param: impl Into<String>) -> Self {
328		self.version_param = param.into();
329		self
330	}
331	/// Set a custom regex pattern for extracting version from path
332	///
333	/// # Examples
334	///
335	/// ```
336	/// use reinhardt_rest::versioning::URLPathVersioning;
337	/// use regex::Regex;
338	///
339	/// let custom_regex = Regex::new(r"/api/v(\d+)").unwrap();
340	/// let versioning = URLPathVersioning::new()
341	///     .with_path_regex(custom_regex);
342	/// // The versioning will now match paths like /api/v1, /api/v2, etc.
343	/// ```
344	pub fn with_path_regex(mut self, regex: Regex) -> Self {
345		self.path_regex = regex;
346		self
347	}
348
349	/// Set a custom pattern for extracting version from path (for configuration compatibility)
350	///
351	/// This converts a pattern like "/v{version}/" into a regex pattern.
352	///
353	/// # Examples
354	///
355	/// ```
356	/// use reinhardt_rest::versioning::URLPathVersioning;
357	///
358	/// let versioning = URLPathVersioning::new()
359	///     .with_pattern("/v{version}/");
360	/// // The versioning will now match paths like /v1/, /v2/, etc.
361	/// ```
362	pub fn with_pattern(mut self, pattern: &str) -> Self {
363		// Convert pattern like "/v{version}/" to regex "/v?([^/]+)"
364		let regex_pattern = pattern.replace("{version}", "([^/]+)");
365		if let Ok(regex) = Regex::new(&regex_pattern) {
366			self.path_regex = regex;
367		}
368		self
369	}
370}
371
372impl Default for URLPathVersioning {
373	fn default() -> Self {
374		Self::new()
375	}
376}
377
378#[async_trait]
379impl BaseVersioning for URLPathVersioning {
380	async fn determine_version(&self, request: &Request) -> Result<String> {
381		let path = request.uri.path();
382
383		// Try to extract version from path using regex
384		if let Some(captures) = self.path_regex.captures(path)
385			&& let Some(version_match) = captures.get(1)
386		{
387			let version = version_match.as_str();
388			if self.is_allowed_version(version) {
389				return Ok(version.to_string());
390			} else {
391				return Err(Error::Validation(
392					VersioningError::VersionNotAllowed(version.to_string()).to_string(),
393				));
394			}
395		}
396
397		// Return default version if no version in path
398		Ok(self
399			.default_version
400			.clone()
401			.unwrap_or_else(|| "1.0".to_string()))
402	}
403
404	fn default_version(&self) -> Option<&str> {
405		self.default_version.as_deref()
406	}
407
408	fn allowed_versions(&self) -> Option<&HashSet<String>> {
409		Some(&self.allowed_versions)
410	}
411
412	fn version_param(&self) -> &str {
413		&self.version_param
414	}
415}
416
417/// Hostname versioning
418///
419/// Example: `v1.api.example.com` or `api-v2.example.com`
420#[derive(Debug, Clone)]
421pub struct HostNameVersioning {
422	/// The fallback version when no version is found in the hostname.
423	pub default_version: Option<String>,
424	/// The set of allowed API versions.
425	pub allowed_versions: HashSet<String>,
426	/// The regex pattern used to extract the version from the hostname.
427	pub hostname_regex: Regex,
428	/// Maps specific hostnames to their API versions.
429	/// Takes precedence over regex extraction.
430	pub hostname_to_version: HashMap<String, String>,
431}
432
433impl HostNameVersioning {
434	/// Create a new HostNameVersioning instance
435	///
436	/// # Examples
437	///
438	/// ```
439	/// use reinhardt_rest::versioning::HostNameVersioning;
440	///
441	/// let versioning = HostNameVersioning::new();
442	/// assert_eq!(versioning.default_version.as_deref(), None);
443	/// ```
444	pub fn new() -> Self {
445		Self {
446			default_version: None,
447			allowed_versions: HashSet::new(),
448			hostname_regex: Regex::new(r"^([a-zA-Z0-9]+)\.").unwrap(),
449			hostname_to_version: HashMap::new(),
450		}
451	}
452	/// Set the default version to use when no version is found in hostname
453	///
454	/// # Examples
455	///
456	/// ```
457	/// use reinhardt_rest::versioning::HostNameVersioning;
458	///
459	/// let versioning = HostNameVersioning::new()
460	///     .with_default_version("1.0");
461	/// assert_eq!(versioning.default_version.as_deref(), Some("1.0"));
462	/// ```
463	pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
464		self.default_version = Some(version.into());
465		self
466	}
467	/// Set the allowed versions
468	///
469	/// # Examples
470	///
471	/// ```
472	/// use reinhardt_rest::versioning::{HostNameVersioning, BaseVersioning};
473	///
474	/// let versioning = HostNameVersioning::new()
475	///     .with_allowed_versions(vec!["v1", "v2", "v3"]);
476	/// assert!(versioning.is_allowed_version("v1"));
477	/// assert!(!versioning.is_allowed_version("v99"));
478	/// ```
479	pub fn with_allowed_versions(mut self, versions: Vec<impl Into<String>>) -> Self {
480		self.allowed_versions = versions.into_iter().map(|v| v.into()).collect();
481		self
482	}
483	/// Set a custom regex pattern for extracting version from hostname
484	///
485	/// # Examples
486	///
487	/// ```
488	/// use reinhardt_rest::versioning::HostNameVersioning;
489	/// use regex::Regex;
490	///
491	/// let custom_regex = Regex::new(r"^v(\d+)-api\.").unwrap();
492	/// let versioning = HostNameVersioning::new()
493	///     .with_hostname_regex(custom_regex);
494	/// // The versioning will now match hostnames like v1-api.example.com
495	/// ```
496	pub fn with_hostname_regex(mut self, regex: Regex) -> Self {
497		self.hostname_regex = regex;
498		self
499	}
500
501	/// Set a host format pattern (for configuration compatibility)
502	///
503	/// This converts a host format like "{version}.api.example.com" into a regex pattern.
504	///
505	/// # Examples
506	///
507	/// ```
508	/// use reinhardt_rest::versioning::HostNameVersioning;
509	///
510	/// let versioning = HostNameVersioning::new()
511	///     .with_host_format("{version}.api.example.com");
512	/// // The versioning will match hostnames like v1.api.example.com
513	/// ```
514	pub fn with_host_format(mut self, format: &str) -> Self {
515		// Convert format like "{version}.api.example.com" to regex "^([^.]+)\.api\.example\.com"
516		// Escape dots first, then replace placeholder to prevent regex corruption
517		const PLACEHOLDER: &str = "__REINHARDT_VERSION_PLACEHOLDER__";
518		let pattern = format.replace("{version}", PLACEHOLDER);
519		let pattern = pattern.replace(".", "\\.");
520		let pattern = pattern.replace(PLACEHOLDER, "([^.]+)");
521		let pattern = format!("^{}", pattern);
522		if let Ok(regex) = Regex::new(&pattern) {
523			self.hostname_regex = regex;
524		}
525		self
526	}
527
528	/// Set hostname patterns for version mapping (for configuration compatibility)
529	///
530	/// This allows mapping specific hostnames to their API versions.
531	/// The hostname mapping takes precedence over regex extraction when determining version.
532	///
533	/// # Examples
534	///
535	/// ```
536	/// use reinhardt_rest::versioning::HostNameVersioning;
537	///
538	/// let versioning = HostNameVersioning::new()
539	///     .with_hostname_pattern("v1", "v1.api.example.com")
540	///     .with_hostname_pattern("v2", "v2.api.example.com");
541	/// // Request to v1.api.example.com will resolve to version "v1"
542	/// // Request to v2.api.example.com will resolve to version "v2"
543	/// ```
544	pub fn with_hostname_pattern(mut self, version: &str, hostname: &str) -> Self {
545		self.allowed_versions.insert(version.to_string());
546		self.hostname_to_version
547			.insert(hostname.to_string(), version.to_string());
548		self
549	}
550}
551
552impl Default for HostNameVersioning {
553	fn default() -> Self {
554		Self::new()
555	}
556}
557
558#[async_trait]
559impl BaseVersioning for HostNameVersioning {
560	async fn determine_version(&self, request: &Request) -> Result<String> {
561		// Extract hostname from request
562		if let Some(host) = request.headers.get("host") {
563			let host_str = host
564				.to_str()
565				.map_err(|_| Error::Validation(VersioningError::InvalidHostname.to_string()))?;
566
567			// Remove port if present
568			let hostname = host_str.split(':').next().unwrap_or(host_str);
569
570			// Priority 1: Check explicit hostname→version mapping
571			if let Some(version) = self.hostname_to_version.get(hostname)
572				&& self.is_allowed_version(version)
573			{
574				return Ok(version.clone());
575			}
576
577			// Priority 2: Try to extract version from hostname using regex
578			if let Some(captures) = self.hostname_regex.captures(hostname)
579				&& let Some(version_match) = captures.get(1)
580			{
581				let version = version_match.as_str();
582				if self.is_allowed_version(version) {
583					return Ok(version.to_string());
584				}
585			}
586		}
587
588		// Return default version if no version in hostname
589		Ok(self
590			.default_version
591			.clone()
592			.unwrap_or_else(|| "1.0".to_string()))
593	}
594
595	fn default_version(&self) -> Option<&str> {
596		self.default_version.as_deref()
597	}
598
599	fn allowed_versions(&self) -> Option<&HashSet<String>> {
600		Some(&self.allowed_versions)
601	}
602}
603
604/// Query parameter versioning
605///
606/// Example: `/users/?version=1.0` or `/users/?v=2.0`
607#[derive(Debug, Clone)]
608pub struct QueryParameterVersioning {
609	/// The fallback version when no version query parameter is present.
610	pub default_version: Option<String>,
611	/// The set of allowed API versions.
612	pub allowed_versions: HashSet<String>,
613	/// The query parameter name for the version (default: `"version"`).
614	pub version_param: String,
615}
616
617impl QueryParameterVersioning {
618	/// Create a new QueryParameterVersioning instance
619	///
620	/// # Examples
621	///
622	/// ```
623	/// use reinhardt_rest::versioning::QueryParameterVersioning;
624	///
625	/// let versioning = QueryParameterVersioning::new();
626	/// assert_eq!(versioning.default_version.as_deref(), None);
627	/// assert_eq!(versioning.version_param.as_str(), "version");
628	/// ```
629	pub fn new() -> Self {
630		Self {
631			default_version: None,
632			allowed_versions: HashSet::new(),
633			version_param: "version".to_string(),
634		}
635	}
636	/// Set the default version to use when no version is in query parameters
637	///
638	/// # Examples
639	///
640	/// ```
641	/// use reinhardt_rest::versioning::QueryParameterVersioning;
642	///
643	/// let versioning = QueryParameterVersioning::new()
644	///     .with_default_version("1.0");
645	/// assert_eq!(versioning.default_version.as_deref(), Some("1.0"));
646	/// ```
647	pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
648		self.default_version = Some(version.into());
649		self
650	}
651	/// Set the allowed versions
652	///
653	/// # Examples
654	///
655	/// ```
656	/// use reinhardt_rest::versioning::{QueryParameterVersioning, BaseVersioning};
657	///
658	/// let versioning = QueryParameterVersioning::new()
659	///     .with_allowed_versions(vec!["1.0", "2.0", "3.0"]);
660	/// assert!(versioning.is_allowed_version("1.0"));
661	/// assert!(!versioning.is_allowed_version("4.0"));
662	/// ```
663	pub fn with_allowed_versions(mut self, versions: Vec<impl Into<String>>) -> Self {
664		self.allowed_versions = versions.into_iter().map(|v| v.into()).collect();
665		self
666	}
667	/// Set the query parameter name to use for version detection
668	///
669	/// # Examples
670	///
671	/// ```
672	/// use reinhardt_rest::versioning::QueryParameterVersioning;
673	///
674	/// let versioning = QueryParameterVersioning::new()
675	///     .with_version_param("v");
676	/// assert_eq!(versioning.version_param.as_str(), "v");
677	/// // This will now look for ?v=1.0 instead of ?version=1.0
678	/// ```
679	pub fn with_version_param(mut self, param: impl Into<String>) -> Self {
680		self.version_param = param.into();
681		self
682	}
683}
684
685impl Default for QueryParameterVersioning {
686	fn default() -> Self {
687		Self::new()
688	}
689}
690
691#[async_trait]
692impl BaseVersioning for QueryParameterVersioning {
693	async fn determine_version(&self, request: &Request) -> Result<String> {
694		// Parse query string for version parameter
695		if let Some(query) = request.uri.query() {
696			for param in query.split('&') {
697				if let Some((key, value)) = param.split_once('=')
698					&& key == self.version_param
699				{
700					if self.is_allowed_version(value) {
701						return Ok(value.to_string());
702					} else {
703						return Err(Error::Validation(
704							VersioningError::VersionNotAllowed(value.to_string()).to_string(),
705						));
706					}
707				}
708			}
709		}
710
711		// Return default version if no version in query
712		Ok(self
713			.default_version
714			.clone()
715			.unwrap_or_else(|| "1.0".to_string()))
716	}
717
718	fn default_version(&self) -> Option<&str> {
719		self.default_version.as_deref()
720	}
721
722	fn allowed_versions(&self) -> Option<&HashSet<String>> {
723		Some(&self.allowed_versions)
724	}
725
726	fn version_param(&self) -> &str {
727		&self.version_param
728	}
729}
730
731/// Namespace versioning (URL namespace-based)
732///
733/// Extracts version from URL namespace patterns (e.g., /v1/, /v2/)
734/// Now fully implemented with router namespace support
735#[derive(Debug)]
736pub struct NamespaceVersioning {
737	/// The fallback version when no version is found in the namespace.
738	pub default_version: Option<String>,
739	/// The set of allowed API versions.
740	pub allowed_versions: HashSet<String>,
741	/// Pattern for extracting version from namespace (e.g., "/v{version}/")
742	pub pattern: String,
743	/// Namespace prefix (e.g., "api")
744	pub namespace_prefix: Option<String>,
745	/// Cached compiled regex for version extraction
746	compiled_regex: OnceLock<Option<Regex>>,
747}
748
749impl Clone for NamespaceVersioning {
750	fn clone(&self) -> Self {
751		Self {
752			default_version: self.default_version.clone(),
753			allowed_versions: self.allowed_versions.clone(),
754			pattern: self.pattern.clone(),
755			namespace_prefix: self.namespace_prefix.clone(),
756			// Reset compiled_regex so it will be recompiled on first use
757			compiled_regex: OnceLock::new(),
758		}
759	}
760}
761
762impl NamespaceVersioning {
763	/// Create a new NamespaceVersioning instance
764	///
765	/// # Examples
766	///
767	/// ```
768	/// use reinhardt_rest::versioning::NamespaceVersioning;
769	///
770	/// let versioning = NamespaceVersioning::new();
771	/// assert_eq!(versioning.default_version.as_deref(), None);
772	/// assert_eq!(versioning.pattern, "/v{version}/");
773	/// ```
774	pub fn new() -> Self {
775		Self {
776			default_version: None,
777			allowed_versions: HashSet::new(),
778			pattern: "/v{version}/".to_string(),
779			namespace_prefix: None,
780			compiled_regex: OnceLock::new(),
781		}
782	}
783	/// Set the default version to use when no version is found in namespace
784	///
785	/// # Examples
786	///
787	/// ```
788	/// use reinhardt_rest::versioning::NamespaceVersioning;
789	///
790	/// let versioning = NamespaceVersioning::new()
791	///     .with_default_version("1.0");
792	/// assert_eq!(versioning.default_version.as_deref(), Some("1.0"));
793	/// ```
794	pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
795		self.default_version = Some(version.into());
796		self
797	}
798	/// Set the allowed versions
799	///
800	/// # Examples
801	///
802	/// ```
803	/// use reinhardt_rest::versioning::{NamespaceVersioning, BaseVersioning};
804	///
805	/// let versioning = NamespaceVersioning::new()
806	///     .with_allowed_versions(vec!["1", "1.0", "2", "2.0"]);
807	/// assert!(versioning.is_allowed_version("1"));
808	/// assert!(versioning.is_allowed_version("2.0"));
809	/// assert!(!versioning.is_allowed_version("99"));
810	/// ```
811	pub fn with_allowed_versions(mut self, versions: Vec<impl Into<String>>) -> Self {
812		self.allowed_versions = versions.into_iter().map(|v| v.into()).collect();
813		self
814	}
815
816	/// Set the namespace prefix (e.g., "api")
817	///
818	/// This prefix is used when constructing full namespace patterns for version detection.
819	///
820	/// # Examples
821	///
822	/// ```
823	/// use reinhardt_rest::versioning::NamespaceVersioning;
824	///
825	/// let versioning = NamespaceVersioning::new()
826	///     .with_namespace_prefix("api");
827	/// assert_eq!(versioning.namespace_prefix, Some("api".to_string()));
828	/// ```
829	pub fn with_namespace_prefix(mut self, prefix: &str) -> Self {
830		self.namespace_prefix = Some(prefix.to_string());
831		self
832	}
833
834	/// Set a custom pattern for extracting version from namespace
835	///
836	/// This converts a pattern like "/v{version}/" into a regex pattern for matching
837	/// namespaces like /v1/, /v2/, etc.
838	///
839	/// # Examples
840	///
841	/// ```
842	/// use reinhardt_rest::versioning::NamespaceVersioning;
843	///
844	/// let versioning = NamespaceVersioning::new()
845	///     .with_pattern("/api/v{version}/");
846	/// assert_eq!(versioning.pattern, "/api/v{version}/");
847	/// ```
848	pub fn with_pattern(mut self, pattern: &str) -> Self {
849		self.pattern = pattern.to_string();
850		// Reset cached regex since the pattern changed
851		self.compiled_regex = OnceLock::new();
852		self
853	}
854}
855
856impl Default for NamespaceVersioning {
857	fn default() -> Self {
858		Self::new()
859	}
860}
861
862#[async_trait]
863impl BaseVersioning for NamespaceVersioning {
864	async fn determine_version(&self, request: &Request) -> Result<String> {
865		let path = request.uri.path();
866
867		// Use the configured pattern to extract version
868		if let Some(version) = self.extract_version_from_path(path)
869			&& self.is_allowed_version(&version)
870		{
871			return Ok(version);
872		}
873
874		// Fallback to default version
875		Ok(self
876			.default_version
877			.clone()
878			.unwrap_or_else(|| "1.0".to_string()))
879	}
880
881	fn default_version(&self) -> Option<&str> {
882		self.default_version.as_deref()
883	}
884
885	fn allowed_versions(&self) -> Option<&HashSet<String>> {
886		Some(&self.allowed_versions)
887	}
888}
889
890impl NamespaceVersioning {
891	/// Get or compile the regex for version extraction from the configured pattern
892	fn get_compiled_regex(&self) -> Option<&Regex> {
893		self.compiled_regex
894			.get_or_init(|| {
895				let regex_pattern = self
896					.pattern
897					.replace("{version}", r"([^/]+)")
898					.replace("/", r"\/");
899				let full_pattern = format!("^{}", regex_pattern);
900				regex::Regex::new(&full_pattern).ok()
901			})
902			.as_ref()
903	}
904
905	/// Extract version from a path using the configured pattern
906	fn extract_version_from_path(&self, path: &str) -> Option<String> {
907		if let Some(regex) = self.get_compiled_regex()
908			&& let Some(captures) = regex.captures(path)
909			&& let Some(version_match) = captures.get(1)
910		{
911			return Some(version_match.as_str().to_string());
912		}
913		None
914	}
915
916	/// Check if a version is allowed
917	fn is_allowed_version(&self, version: &str) -> bool {
918		self.allowed_versions.is_empty() || self.allowed_versions.contains(version)
919	}
920
921	/// Extract version from a router's namespace pattern
922	/// This method integrates with reinhardt-routers for namespace-based versioning
923	///
924	/// # Examples
925	///
926	/// ```ignore
927	/// use reinhardt_rest::versioning::NamespaceVersioning;
928	/// use reinhardt_urls::routers::DefaultRouter;
929	///
930	/// let versioning = NamespaceVersioning::new()
931	///     .with_pattern("/v{version}/")
932	///     .with_allowed_versions(vec!["1", "2"]);
933	///
934	/// let router = DefaultRouter::new();
935	/// let version = versioning.extract_version_from_router(&router, "/v1/users/");
936	/// assert_eq!(version, Some("1".to_string()));
937	/// ```
938	// Router integration disabled due to circular dependency (reinhardt-urls ↔ reinhardt-rest)
939	// Use extract_version_from_path() directly instead
940	#[allow(dead_code)]
941	fn extract_version_from_router_stub(&self, _router: &(), path: &str) -> Option<String> {
942		self.extract_version_from_path(path)
943	}
944
945	/// Get available versions from a router's registered routes
946	/// This discovers all versions that are currently registered in the router
947	///
948	/// # Examples
949	///
950	/// ```ignore
951	/// // This example is disabled because router integration is disabled
952	/// // due to circular dependency (reinhardt-urls ↔ reinhardt-rest)
953	/// use reinhardt_rest::versioning::NamespaceVersioning;
954	/// use reinhardt_urls::routers::{DefaultRouter, Router, path};
955	/// use reinhardt_http::Handler;
956	/// use std::sync::Arc;
957	///
958	/// # use async_trait::async_trait;
959	/// # use reinhardt_http::{Request, Response, Result};
960	/// # struct DummyHandler;
961	/// # #[async_trait]
962	/// # impl Handler for DummyHandler {
963	/// #     async fn handle(&self, _req: Request) -> Result<Response> {
964	/// #         Ok(Response::ok())
965	/// #     }
966	/// # }
967	/// let versioning = NamespaceVersioning::new()
968	///     .with_pattern("/v{version}/");
969	///
970	/// let mut router = DefaultRouter::new();
971	/// let handler = Arc::new(DummyHandler);
972	/// router.add_route(path("/v1/users/", handler.clone()).with_namespace("v1"));
973	/// router.add_route(path("/v2/users/", handler).with_namespace("v2"));
974	///
975	/// let versions = versioning.get_available_versions_from_router(&router);
976	/// assert!(versions.contains(&"1".to_string()));
977	/// assert!(versions.contains(&"2".to_string()));
978	/// ```
979	// Router integration disabled due to circular dependency (reinhardt-urls ↔ reinhardt-rest)
980	#[allow(dead_code)]
981	fn get_available_versions_from_router_stub(&self, _router: &()) -> Vec<String> {
982		Vec::new()
983	}
984}
985
986#[cfg(test)]
987pub mod test_utils {
988	use bytes::Bytes;
989	use hyper::header::HeaderName;
990	use hyper::{HeaderMap, Method, Uri, Version};
991	use reinhardt_http::Request;
992
993	pub fn create_test_request(uri: &str, headers: Vec<(String, String)>) -> Request {
994		let uri = uri.parse::<Uri>().unwrap();
995		let mut header_map = HeaderMap::new();
996		for (key, value) in headers {
997			let header_name: HeaderName = key.parse().unwrap();
998			header_map.insert(header_name, value.parse().unwrap());
999		}
1000
1001		Request::builder()
1002			.method(Method::GET)
1003			.uri(uri)
1004			.version(Version::HTTP_11)
1005			.headers(header_map)
1006			.body(Bytes::new())
1007			.build()
1008			.unwrap()
1009	}
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014	use super::*;
1015	use test_utils::create_test_request;
1016
1017	#[tokio::test]
1018	async fn test_accept_header_versioning() {
1019		let versioning = AcceptHeaderVersioning::new()
1020			.with_default_version("1.0")
1021			.with_allowed_versions(vec!["1.0", "2.0"]);
1022
1023		// Test with version in Accept header
1024		let request = create_test_request(
1025			"/users/",
1026			vec![(
1027				"accept".to_string(),
1028				"application/json; version=2.0".to_string(),
1029			)],
1030		);
1031		let version = versioning.determine_version(&request).await.unwrap();
1032		assert_eq!(version, "2.0");
1033
1034		// Test without version (should return default)
1035		let request = create_test_request(
1036			"/users/",
1037			vec![("accept".to_string(), "application/json".to_string())],
1038		);
1039		let version = versioning.determine_version(&request).await.unwrap();
1040		assert_eq!(version, "1.0");
1041	}
1042
1043	#[tokio::test]
1044	async fn test_url_path_versioning() {
1045		let versioning = URLPathVersioning::new()
1046			.with_default_version("1.0")
1047			.with_allowed_versions(vec!["1.0", "2.0", "2"]);
1048
1049		// Test with version in path
1050		let request = create_test_request("/v2/users/", vec![]);
1051		let version = versioning.determine_version(&request).await.unwrap();
1052		assert_eq!(version, "2");
1053
1054		// Test without version (should return default)
1055		let request = create_test_request("/users/", vec![]);
1056		let version = versioning.determine_version(&request).await.unwrap();
1057		assert_eq!(version, "1.0");
1058	}
1059
1060	#[tokio::test]
1061	async fn test_hostname_versioning() {
1062		let versioning = HostNameVersioning::new()
1063			.with_default_version("1.0")
1064			.with_allowed_versions(vec!["v1", "v2"]);
1065
1066		// Test with version in hostname
1067		let request = create_test_request(
1068			"/users/",
1069			vec![("host".to_string(), "v2.api.example.com".to_string())],
1070		);
1071		let version = versioning.determine_version(&request).await.unwrap();
1072		assert_eq!(version, "v2");
1073
1074		// Test without version (should return default)
1075		let request = create_test_request(
1076			"/users/",
1077			vec![("host".to_string(), "api.example.com".to_string())],
1078		);
1079		let version = versioning.determine_version(&request).await.unwrap();
1080		assert_eq!(version, "1.0");
1081	}
1082
1083	#[tokio::test]
1084	async fn test_query_parameter_versioning() {
1085		let versioning = QueryParameterVersioning::new()
1086			.with_default_version("1.0")
1087			.with_allowed_versions(vec!["1.0", "2.0"]);
1088
1089		// Test with version in query parameter
1090		let request = create_test_request("/users/?version=2.0", vec![]);
1091		let version = versioning.determine_version(&request).await.unwrap();
1092		assert_eq!(version, "2.0");
1093
1094		// Test without version (should return default)
1095		let request = create_test_request("/users/", vec![]);
1096		let version = versioning.determine_version(&request).await.unwrap();
1097		assert_eq!(version, "1.0");
1098	}
1099
1100	#[tokio::test]
1101	async fn test_namespace_versioning() {
1102		let versioning = NamespaceVersioning::new()
1103			.with_default_version("1.0")
1104			.with_allowed_versions(vec!["1", "1.0", "2", "2.0", "3.0"]);
1105
1106		// Test with version in namespace (v1 format)
1107		let request = create_test_request("/v1/users/", vec![]);
1108		let version = versioning.determine_version(&request).await.unwrap();
1109		assert_eq!(version, "1");
1110
1111		// Test with version in namespace (v2.0 format)
1112		let request = create_test_request("/v2.0/users/", vec![]);
1113		let version = versioning.determine_version(&request).await.unwrap();
1114		assert_eq!(version, "2.0");
1115
1116		// Test without version (should return default)
1117		let request = create_test_request("/users/", vec![]);
1118		let version = versioning.determine_version(&request).await.unwrap();
1119		assert_eq!(version, "1.0");
1120
1121		// Test with non-version namespace
1122		let request = create_test_request("/api/users/", vec![]);
1123		let version = versioning.determine_version(&request).await.unwrap();
1124		assert_eq!(version, "1.0");
1125	}
1126
1127	#[tokio::test]
1128	async fn test_namespace_versioning_with_custom_pattern() {
1129		let versioning = NamespaceVersioning::new()
1130			.with_default_version("1.0")
1131			.with_pattern("/api/v{version}/")
1132			.with_allowed_versions(vec!["1", "2"]);
1133
1134		// Test with custom pattern
1135		let request = create_test_request("/api/v1/users/", vec![]);
1136		let version = versioning.determine_version(&request).await.unwrap();
1137		assert_eq!(version, "1");
1138
1139		// Test with different version
1140		let request = create_test_request("/api/v2/users/", vec![]);
1141		let version = versioning.determine_version(&request).await.unwrap();
1142		assert_eq!(version, "2");
1143
1144		// Test with old pattern (should not match)
1145		let request = create_test_request("/v1/users/", vec![]);
1146		let version = versioning.determine_version(&request).await.unwrap();
1147		assert_eq!(version, "1.0"); // Falls back to default
1148	}
1149
1150	#[tokio::test]
1151	async fn test_hostname_versioning_with_host_format_dots_not_corrupted() {
1152		// Arrange - format with dots that would be corrupted by the old implementation
1153		let versioning = HostNameVersioning::new()
1154			.with_host_format("{version}.api.v2.example.com")
1155			.with_allowed_versions(vec!["v1", "v3"]);
1156
1157		// Act
1158		let request = create_test_request(
1159			"/users/",
1160			vec![("host".to_string(), "v1.api.v2.example.com".to_string())],
1161		);
1162		let version = versioning.determine_version(&request).await.unwrap();
1163
1164		// Assert
1165		assert_eq!(version, "v1");
1166
1167		// Act - different version
1168		let request = create_test_request(
1169			"/users/",
1170			vec![("host".to_string(), "v3.api.v2.example.com".to_string())],
1171		);
1172		let version = versioning.determine_version(&request).await.unwrap();
1173
1174		// Assert
1175		assert_eq!(version, "v3");
1176	}
1177
1178	// Note: Router integration test removed to avoid circular dependency with reinhardt-urls.
1179	// Router integration tests should be placed in /tests/integration crate.
1180}