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;
38pub mod settings;
39
40use async_trait::async_trait;
41pub use config::{VersioningConfig, VersioningManager, VersioningStrategy};
42pub use handler::{
43	ConfigurableVersionedHandler, SimpleVersionedHandler, VersionResponseBuilder, VersionedHandler,
44	VersionedHandlerBuilder, VersionedHandlerWrapper,
45};
46pub use middleware::{ApiVersion, RequestVersionExt, VersioningMiddleware};
47use regex::Regex;
48use reinhardt_core::exception::{Error, Result};
49use reinhardt_http::Request;
50pub use reverse::{
51	ApiDocFormat, ApiDocUrlBuilder, UrlReverseManager, VersionedUrlBuilder,
52	VersioningStrategy as ReverseVersioningStrategy,
53};
54pub use settings::VersioningSettings;
55use std::collections::{HashMap, HashSet};
56use std::sync::OnceLock;
57use thiserror::Error as ThisError;
58
59/// Errors that can occur during API version determination.
60#[derive(Debug, ThisError)]
61pub enum VersioningError {
62	/// The Accept header does not contain a valid version.
63	#[error("Invalid version in Accept header")]
64	InvalidAcceptHeader,
65
66	/// The URL path does not contain a valid version segment.
67	#[error("Invalid version in URL path")]
68	InvalidURLPath,
69
70	/// The URL namespace does not contain a valid version.
71	#[error("Invalid version in URL namespace")]
72	InvalidNamespace,
73
74	/// The hostname does not contain a valid version subdomain.
75	#[error("Invalid version in hostname")]
76	InvalidHostname,
77
78	/// The query parameter does not contain a valid version.
79	#[error("Invalid version in query parameter")]
80	InvalidQueryParameter,
81
82	/// The requested version is not in the allowed versions list.
83	#[error("Version not allowed: {0}")]
84	VersionNotAllowed(String),
85}
86
87/// Base trait for API versioning strategies
88#[async_trait]
89pub trait BaseVersioning: Send + Sync {
90	/// Determine the API version from the request
91	async fn determine_version(&self, request: &Request) -> Result<String>;
92
93	/// Get the default version
94	fn default_version(&self) -> Option<&str>;
95
96	/// Get allowed versions
97	fn allowed_versions(&self) -> Option<&HashSet<String>>;
98
99	/// Check if a version is allowed
100	fn is_allowed_version(&self, version: &str) -> bool {
101		if let Some(allowed) = self.allowed_versions() {
102			if allowed.is_empty() {
103				return true;
104			}
105			return allowed.contains(version) || (self.default_version() == Some(version));
106		}
107		true
108	}
109
110	/// Get the version parameter name
111	fn version_param(&self) -> &str {
112		"version"
113	}
114}
115
116/// Accept header versioning
117///
118/// Example: `Accept: application/json; version=1.0`
119#[derive(Debug, Clone)]
120pub struct AcceptHeaderVersioning {
121	/// The fallback version when no version is specified in the Accept header.
122	pub default_version: Option<String>,
123	/// The set of allowed API versions.
124	pub allowed_versions: HashSet<String>,
125	/// The parameter name to look for in the Accept header (default: `"version"`).
126	pub version_param: String,
127}
128
129impl AcceptHeaderVersioning {
130	/// Create a new AcceptHeaderVersioning instance
131	///
132	/// # Examples
133	///
134	/// ```
135	/// use reinhardt_rest::versioning::{AcceptHeaderVersioning, BaseVersioning};
136	///
137	/// let versioning = AcceptHeaderVersioning::new();
138	/// assert_eq!(versioning.default_version.as_deref(), None);
139	/// assert_eq!(versioning.version_param.as_str(), "version");
140	/// ```
141	pub fn new() -> Self {
142		Self {
143			default_version: None,
144			allowed_versions: HashSet::new(),
145			version_param: "version".to_string(),
146		}
147	}
148	/// Set the default version to use when no version is specified
149	///
150	/// # Examples
151	///
152	/// ```
153	/// use reinhardt_rest::versioning::{AcceptHeaderVersioning, BaseVersioning};
154	///
155	/// let versioning = AcceptHeaderVersioning::new()
156	///     .with_default_version("1.0");
157	/// assert_eq!(versioning.default_version.as_deref(), Some("1.0"));
158	/// ```
159	pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
160		self.default_version = Some(version.into());
161		self
162	}
163	/// Set the allowed versions
164	///
165	/// # Examples
166	///
167	/// ```
168	/// use reinhardt_rest::versioning::{AcceptHeaderVersioning, BaseVersioning};
169	///
170	/// let versioning = AcceptHeaderVersioning::new()
171	///     .with_allowed_versions(vec!["1.0", "2.0", "3.0"]);
172	/// assert!(versioning.is_allowed_version("1.0"));
173	/// assert!(versioning.is_allowed_version("2.0"));
174	/// assert!(!versioning.is_allowed_version("4.0"));
175	/// ```
176	pub fn with_allowed_versions(mut self, versions: Vec<impl Into<String>>) -> Self {
177		self.allowed_versions = versions.into_iter().map(|v| v.into()).collect();
178		self
179	}
180	/// Set the version parameter name to look for in the Accept header
181	///
182	/// # Examples
183	///
184	/// ```
185	/// use reinhardt_rest::versioning::{AcceptHeaderVersioning, BaseVersioning};
186	///
187	/// let versioning = AcceptHeaderVersioning::new()
188	///     .with_version_param("api-version");
189	/// assert_eq!(versioning.version_param.as_str(), "api-version");
190	/// ```
191	pub fn with_version_param(mut self, param: impl Into<String>) -> Self {
192		self.version_param = param.into();
193		self
194	}
195}
196
197impl Default for AcceptHeaderVersioning {
198	fn default() -> Self {
199		Self::new()
200	}
201}
202
203#[async_trait]
204impl BaseVersioning for AcceptHeaderVersioning {
205	async fn determine_version(&self, request: &Request) -> Result<String> {
206		// Parse Accept header for version parameter
207		if let Some(accept) = request.headers.get("accept") {
208			let accept_str = accept
209				.to_str()
210				.map_err(|_| Error::Validation(VersioningError::InvalidAcceptHeader.to_string()))?;
211
212			// Parse media type parameters
213			if let Some(params_start) = accept_str.find(';') {
214				let params = &accept_str[params_start + 1..];
215				for param in params.split(';') {
216					let param = param.trim();
217					if let Some((key, value)) = param.split_once('=')
218						&& key.trim() == self.version_param
219					{
220						let version = value.trim().trim_matches('"');
221						if self.is_allowed_version(version) {
222							return Ok(version.to_owned());
223						} else {
224							// Avoid intermediate String allocation from VersionNotAllowed(String).to_string()
225							return Err(Error::Validation(format!(
226								"Version not allowed: {version}"
227							)));
228						}
229					}
230				}
231			}
232		}
233
234		// Return default version if no version in header.
235		// Use as_deref().to_owned() instead of clone().unwrap_or_else(...) to skip
236		// cloning the Option<String> wrapper. The final String allocation to satisfy
237		// the Result<String> return type is unavoidable.
238		Ok(self.default_version.as_deref().unwrap_or("1.0").to_owned())
239	}
240
241	fn default_version(&self) -> Option<&str> {
242		self.default_version.as_deref()
243	}
244
245	fn allowed_versions(&self) -> Option<&HashSet<String>> {
246		Some(&self.allowed_versions)
247	}
248
249	fn version_param(&self) -> &str {
250		&self.version_param
251	}
252}
253
254/// URL path versioning
255///
256/// Example: `/v1/users/` or `/api/v2/users/`
257#[derive(Debug, Clone)]
258pub struct URLPathVersioning {
259	/// The fallback version when no version is found in the URL path.
260	pub default_version: Option<String>,
261	/// The set of allowed API versions.
262	pub allowed_versions: HashSet<String>,
263	/// The parameter name for version (default: `"version"`).
264	pub version_param: String,
265	/// The regex pattern used to extract the version from the URL path.
266	pub path_regex: Regex,
267}
268
269impl URLPathVersioning {
270	/// Create a new URLPathVersioning instance
271	///
272	/// # Examples
273	///
274	/// ```
275	/// use reinhardt_rest::versioning::{URLPathVersioning, BaseVersioning};
276	///
277	/// let versioning = URLPathVersioning::new();
278	/// assert_eq!(versioning.default_version.as_deref(), None);
279	/// ```
280	pub fn new() -> Self {
281		Self {
282			default_version: None,
283			allowed_versions: HashSet::new(),
284			version_param: "version".to_string(),
285			path_regex: Regex::new(r"/v(\d+\.?\d*)(?:/|$)").unwrap(),
286		}
287	}
288	/// Set the default version to use when no version is found in the path
289	///
290	/// # Examples
291	///
292	/// ```
293	/// use reinhardt_rest::versioning::{URLPathVersioning, BaseVersioning};
294	///
295	/// let versioning = URLPathVersioning::new()
296	///     .with_default_version("1.0");
297	/// assert_eq!(versioning.default_version.as_deref(), Some("1.0"));
298	/// ```
299	pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
300		self.default_version = Some(version.into());
301		self
302	}
303	/// Set the allowed versions
304	///
305	/// # Examples
306	///
307	/// ```
308	/// use reinhardt_rest::versioning::{URLPathVersioning, BaseVersioning};
309	///
310	/// let versioning = URLPathVersioning::new()
311	///     .with_allowed_versions(vec!["1", "2", "3"]);
312	/// assert!(versioning.is_allowed_version("1"));
313	/// assert!(!versioning.is_allowed_version("99"));
314	/// ```
315	pub fn with_allowed_versions(mut self, versions: Vec<impl Into<String>>) -> Self {
316		self.allowed_versions = versions.into_iter().map(|v| v.into()).collect();
317		self
318	}
319	/// Set the version parameter name (for trait compatibility)
320	///
321	/// # Examples
322	///
323	/// ```
324	/// use reinhardt_rest::versioning::{URLPathVersioning, BaseVersioning};
325	///
326	/// let versioning = URLPathVersioning::new()
327	///     .with_version_param("v");
328	/// assert_eq!(versioning.version_param.as_str(), "v");
329	/// ```
330	pub fn with_version_param(mut self, param: impl Into<String>) -> Self {
331		self.version_param = param.into();
332		self
333	}
334	/// Set a custom regex pattern for extracting version from path
335	///
336	/// # Examples
337	///
338	/// ```
339	/// use reinhardt_rest::versioning::URLPathVersioning;
340	/// use regex::Regex;
341	///
342	/// let custom_regex = Regex::new(r"/api/v(\d+)").unwrap();
343	/// let versioning = URLPathVersioning::new()
344	///     .with_path_regex(custom_regex);
345	/// // The versioning will now match paths like /api/v1, /api/v2, etc.
346	/// ```
347	pub fn with_path_regex(mut self, regex: Regex) -> Self {
348		self.path_regex = regex;
349		self
350	}
351
352	/// Set a custom pattern for extracting version from path (for configuration compatibility)
353	///
354	/// This converts a pattern like "/v{version}/" into a regex pattern.
355	///
356	/// # Examples
357	///
358	/// ```
359	/// use reinhardt_rest::versioning::URLPathVersioning;
360	///
361	/// let versioning = URLPathVersioning::new()
362	///     .with_pattern("/v{version}/");
363	/// // The versioning will now match paths like /v1/, /v2/, etc.
364	/// ```
365	pub fn with_pattern(mut self, pattern: &str) -> Self {
366		// Convert pattern like "/v{version}/" to regex "/v?([^/]+)"
367		let regex_pattern = pattern.replace("{version}", "([^/]+)");
368		if let Ok(regex) = Regex::new(&regex_pattern) {
369			self.path_regex = regex;
370		}
371		self
372	}
373}
374
375impl Default for URLPathVersioning {
376	fn default() -> Self {
377		Self::new()
378	}
379}
380
381#[async_trait]
382impl BaseVersioning for URLPathVersioning {
383	async fn determine_version(&self, request: &Request) -> Result<String> {
384		let path = request.uri.path();
385
386		// Try to extract version from path using regex
387		if let Some(captures) = self.path_regex.captures(path)
388			&& let Some(version_match) = captures.get(1)
389		{
390			let version = version_match.as_str();
391			if self.is_allowed_version(version) {
392				return Ok(version.to_owned());
393			} else {
394				// Avoid intermediate String allocation from VersionNotAllowed(String).to_string()
395				return Err(Error::Validation(format!("Version not allowed: {version}")));
396			}
397		}
398
399		// Return default version if no version in path.
400		// Skip cloning the Option<String> wrapper; final String alloc is unavoidable.
401		Ok(self.default_version.as_deref().unwrap_or("1.0").to_owned())
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		// Skip cloning the Option<String> wrapper; final String alloc is unavoidable.
590		Ok(self.default_version.as_deref().unwrap_or("1.0").to_owned())
591	}
592
593	fn default_version(&self) -> Option<&str> {
594		self.default_version.as_deref()
595	}
596
597	fn allowed_versions(&self) -> Option<&HashSet<String>> {
598		Some(&self.allowed_versions)
599	}
600}
601
602/// Query parameter versioning
603///
604/// Example: `/users/?version=1.0` or `/users/?v=2.0`
605#[derive(Debug, Clone)]
606pub struct QueryParameterVersioning {
607	/// The fallback version when no version query parameter is present.
608	pub default_version: Option<String>,
609	/// The set of allowed API versions.
610	pub allowed_versions: HashSet<String>,
611	/// The query parameter name for the version (default: `"version"`).
612	pub version_param: String,
613}
614
615impl QueryParameterVersioning {
616	/// Create a new QueryParameterVersioning instance
617	///
618	/// # Examples
619	///
620	/// ```
621	/// use reinhardt_rest::versioning::QueryParameterVersioning;
622	///
623	/// let versioning = QueryParameterVersioning::new();
624	/// assert_eq!(versioning.default_version.as_deref(), None);
625	/// assert_eq!(versioning.version_param.as_str(), "version");
626	/// ```
627	pub fn new() -> Self {
628		Self {
629			default_version: None,
630			allowed_versions: HashSet::new(),
631			version_param: "version".to_string(),
632		}
633	}
634	/// Set the default version to use when no version is in query parameters
635	///
636	/// # Examples
637	///
638	/// ```
639	/// use reinhardt_rest::versioning::QueryParameterVersioning;
640	///
641	/// let versioning = QueryParameterVersioning::new()
642	///     .with_default_version("1.0");
643	/// assert_eq!(versioning.default_version.as_deref(), Some("1.0"));
644	/// ```
645	pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
646		self.default_version = Some(version.into());
647		self
648	}
649	/// Set the allowed versions
650	///
651	/// # Examples
652	///
653	/// ```
654	/// use reinhardt_rest::versioning::{QueryParameterVersioning, BaseVersioning};
655	///
656	/// let versioning = QueryParameterVersioning::new()
657	///     .with_allowed_versions(vec!["1.0", "2.0", "3.0"]);
658	/// assert!(versioning.is_allowed_version("1.0"));
659	/// assert!(!versioning.is_allowed_version("4.0"));
660	/// ```
661	pub fn with_allowed_versions(mut self, versions: Vec<impl Into<String>>) -> Self {
662		self.allowed_versions = versions.into_iter().map(|v| v.into()).collect();
663		self
664	}
665	/// Set the query parameter name to use for version detection
666	///
667	/// # Examples
668	///
669	/// ```
670	/// use reinhardt_rest::versioning::QueryParameterVersioning;
671	///
672	/// let versioning = QueryParameterVersioning::new()
673	///     .with_version_param("v");
674	/// assert_eq!(versioning.version_param.as_str(), "v");
675	/// // This will now look for ?v=1.0 instead of ?version=1.0
676	/// ```
677	pub fn with_version_param(mut self, param: impl Into<String>) -> Self {
678		self.version_param = param.into();
679		self
680	}
681}
682
683impl Default for QueryParameterVersioning {
684	fn default() -> Self {
685		Self::new()
686	}
687}
688
689#[async_trait]
690impl BaseVersioning for QueryParameterVersioning {
691	async fn determine_version(&self, request: &Request) -> Result<String> {
692		// Parse query string for version parameter
693		if let Some(query) = request.uri.query() {
694			for param in query.split('&') {
695				if let Some((key, value)) = param.split_once('=')
696					&& key == self.version_param
697				{
698					if self.is_allowed_version(value) {
699						return Ok(value.to_owned());
700					} else {
701						// Avoid intermediate String allocation from VersionNotAllowed(String).to_string()
702						return Err(Error::Validation(format!("Version not allowed: {value}")));
703					}
704				}
705			}
706		}
707
708		// Return default version if no version in query.
709		// Skip cloning the Option<String> wrapper; final String alloc is unavoidable.
710		Ok(self.default_version.as_deref().unwrap_or("1.0").to_owned())
711	}
712
713	fn default_version(&self) -> Option<&str> {
714		self.default_version.as_deref()
715	}
716
717	fn allowed_versions(&self) -> Option<&HashSet<String>> {
718		Some(&self.allowed_versions)
719	}
720
721	fn version_param(&self) -> &str {
722		&self.version_param
723	}
724}
725
726/// Namespace versioning (URL namespace-based)
727///
728/// Extracts version from URL namespace patterns (e.g., /v1/, /v2/)
729/// Now fully implemented with router namespace support
730#[derive(Debug)]
731pub struct NamespaceVersioning {
732	/// The fallback version when no version is found in the namespace.
733	pub default_version: Option<String>,
734	/// The set of allowed API versions.
735	pub allowed_versions: HashSet<String>,
736	/// Pattern for extracting version from namespace (e.g., "/v{version}/")
737	pub pattern: String,
738	/// Namespace prefix (e.g., "api")
739	pub namespace_prefix: Option<String>,
740	/// Cached compiled regex for version extraction
741	compiled_regex: OnceLock<Option<Regex>>,
742}
743
744impl Clone for NamespaceVersioning {
745	fn clone(&self) -> Self {
746		Self {
747			default_version: self.default_version.clone(),
748			allowed_versions: self.allowed_versions.clone(),
749			pattern: self.pattern.clone(),
750			namespace_prefix: self.namespace_prefix.clone(),
751			// Reset compiled_regex so it will be recompiled on first use
752			compiled_regex: OnceLock::new(),
753		}
754	}
755}
756
757impl NamespaceVersioning {
758	/// Create a new NamespaceVersioning instance
759	///
760	/// # Examples
761	///
762	/// ```
763	/// use reinhardt_rest::versioning::NamespaceVersioning;
764	///
765	/// let versioning = NamespaceVersioning::new();
766	/// assert_eq!(versioning.default_version.as_deref(), None);
767	/// assert_eq!(versioning.pattern, "/v{version}/");
768	/// ```
769	pub fn new() -> Self {
770		Self {
771			default_version: None,
772			allowed_versions: HashSet::new(),
773			pattern: "/v{version}/".to_string(),
774			namespace_prefix: None,
775			compiled_regex: OnceLock::new(),
776		}
777	}
778	/// Set the default version to use when no version is found in namespace
779	///
780	/// # Examples
781	///
782	/// ```
783	/// use reinhardt_rest::versioning::NamespaceVersioning;
784	///
785	/// let versioning = NamespaceVersioning::new()
786	///     .with_default_version("1.0");
787	/// assert_eq!(versioning.default_version.as_deref(), Some("1.0"));
788	/// ```
789	pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
790		self.default_version = Some(version.into());
791		self
792	}
793	/// Set the allowed versions
794	///
795	/// # Examples
796	///
797	/// ```
798	/// use reinhardt_rest::versioning::{NamespaceVersioning, BaseVersioning};
799	///
800	/// let versioning = NamespaceVersioning::new()
801	///     .with_allowed_versions(vec!["1", "1.0", "2", "2.0"]);
802	/// assert!(versioning.is_allowed_version("1"));
803	/// assert!(versioning.is_allowed_version("2.0"));
804	/// assert!(!versioning.is_allowed_version("99"));
805	/// ```
806	pub fn with_allowed_versions(mut self, versions: Vec<impl Into<String>>) -> Self {
807		self.allowed_versions = versions.into_iter().map(|v| v.into()).collect();
808		self
809	}
810
811	/// Set the namespace prefix (e.g., "api")
812	///
813	/// This prefix is used when constructing full namespace patterns for version detection.
814	///
815	/// # Examples
816	///
817	/// ```
818	/// use reinhardt_rest::versioning::NamespaceVersioning;
819	///
820	/// let versioning = NamespaceVersioning::new()
821	///     .with_namespace_prefix("api");
822	/// assert_eq!(versioning.namespace_prefix, Some("api".to_string()));
823	/// ```
824	pub fn with_namespace_prefix(mut self, prefix: &str) -> Self {
825		self.namespace_prefix = Some(prefix.to_string());
826		self
827	}
828
829	/// Set a custom pattern for extracting version from namespace
830	///
831	/// This converts a pattern like "/v{version}/" into a regex pattern for matching
832	/// namespaces like /v1/, /v2/, etc.
833	///
834	/// # Examples
835	///
836	/// ```
837	/// use reinhardt_rest::versioning::NamespaceVersioning;
838	///
839	/// let versioning = NamespaceVersioning::new()
840	///     .with_pattern("/api/v{version}/");
841	/// assert_eq!(versioning.pattern, "/api/v{version}/");
842	/// ```
843	pub fn with_pattern(mut self, pattern: &str) -> Self {
844		self.pattern = pattern.to_string();
845		// Reset cached regex since the pattern changed
846		self.compiled_regex = OnceLock::new();
847		self
848	}
849}
850
851impl Default for NamespaceVersioning {
852	fn default() -> Self {
853		Self::new()
854	}
855}
856
857#[async_trait]
858impl BaseVersioning for NamespaceVersioning {
859	async fn determine_version(&self, request: &Request) -> Result<String> {
860		let path = request.uri.path();
861
862		// Use the configured pattern to extract version
863		if let Some(version) = self.extract_version_from_path(path)
864			&& self.is_allowed_version(&version)
865		{
866			return Ok(version);
867		}
868
869		// Fallback to default version.
870		// Skip cloning the Option<String> wrapper; final String alloc is unavoidable.
871		Ok(self.default_version.as_deref().unwrap_or("1.0").to_owned())
872	}
873
874	fn default_version(&self) -> Option<&str> {
875		self.default_version.as_deref()
876	}
877
878	fn allowed_versions(&self) -> Option<&HashSet<String>> {
879		Some(&self.allowed_versions)
880	}
881}
882
883impl NamespaceVersioning {
884	/// Get or compile the regex for version extraction from the configured pattern
885	fn get_compiled_regex(&self) -> Option<&Regex> {
886		self.compiled_regex
887			.get_or_init(|| {
888				let regex_pattern = self
889					.pattern
890					.replace("{version}", r"([^/]+)")
891					.replace("/", r"\/");
892				let full_pattern = format!("^{}", regex_pattern);
893				regex::Regex::new(&full_pattern).ok()
894			})
895			.as_ref()
896	}
897
898	/// Extract version from a path using the configured pattern
899	fn extract_version_from_path(&self, path: &str) -> Option<String> {
900		if let Some(regex) = self.get_compiled_regex()
901			&& let Some(captures) = regex.captures(path)
902			&& let Some(version_match) = captures.get(1)
903		{
904			return Some(version_match.as_str().to_string());
905		}
906		None
907	}
908
909	/// Check if a version is allowed
910	fn is_allowed_version(&self, version: &str) -> bool {
911		self.allowed_versions.is_empty() || self.allowed_versions.contains(version)
912	}
913
914	/// Extract a version from a router-aware path, applying the
915	/// configured pattern.
916	///
917	/// Unlike `extract_version_from_path` (private helper), this method is
918	/// router-aware: it returns `Some(version)` only if `path` matches
919	/// (starts with) at least one `path_prefix` registered on the
920	/// router AND the configured pattern successfully extracts a
921	/// version from that prefix. Otherwise it returns `None`. This
922	/// prevents reporting a version for paths that no route on
923	/// `router` actually serves.
924	///
925	/// The trait bound on [`reinhardt_router::VersionedRouter`] is what
926	/// finally lets this method live in `reinhardt-rest` without
927	/// pulling in `reinhardt-urls` (issue #4321).
928	///
929	/// # Examples
930	///
931	/// ```
932	/// use reinhardt_rest::versioning::NamespaceVersioning;
933	/// use reinhardt_router::{RouteVersionInfo, VersionedRouter};
934	///
935	/// struct FakeRouter;
936	/// impl VersionedRouter for FakeRouter {
937	///     fn route_version_infos(&self) -> Vec<RouteVersionInfo> {
938	///         vec![RouteVersionInfo::new(Some("v1".into()), "/v1/")]
939	///     }
940	/// }
941	///
942	/// let versioning = NamespaceVersioning::new()
943	///     .with_pattern("/v{version}/")
944	///     .with_allowed_versions(vec!["1", "2"]);
945	///
946	/// let router = FakeRouter;
947	/// // "/v1/users/" matches the "/v1/" prefix registered on the router.
948	/// let version = versioning.extract_version_from_router(&router, "/v1/users/");
949	/// assert_eq!(version, Some("1".to_string()));
950	///
951	/// // "/v9/users/" does NOT match any registered prefix → None.
952	/// let unknown = versioning.extract_version_from_router(&router, "/v9/users/");
953	/// assert_eq!(unknown, None);
954	/// ```
955	pub fn extract_version_from_router<R: reinhardt_router::VersionedRouter + ?Sized>(
956		&self,
957		router: &R,
958		path: &str,
959	) -> Option<String> {
960		// Find the first registered route whose `path_prefix` matches
961		// the incoming `path`, then extract the version from that
962		// prefix. If no route matches, the path is not served by this
963		// router and we return None.
964		router
965			.route_version_infos()
966			.into_iter()
967			.find(|info| path.starts_with(&info.path_prefix))
968			.and_then(|info| self.extract_version_from_path(&info.path_prefix))
969	}
970
971	/// Enumerate the versions currently registered on `router`.
972	///
973	/// The router exposes its routes through
974	/// [`reinhardt_router::VersionedRouter`]; this method then applies
975	/// the configured pattern to each route's `path_prefix` and filters
976	/// by `allowed_versions` (when configured).
977	///
978	/// # Ordering
979	///
980	/// The returned `Vec<String>` is sorted **ascending in
981	/// lexicographic (string) order** and deduplicated. Lexicographic
982	/// order coincides with numeric order for single-digit versions
983	/// (e.g. `"1" < "2"`) but diverges for multi-digit versions
984	/// (e.g. `"10"` sorts before `"2"`). Callers that need a different
985	/// ordering must re-sort the result themselves.
986	///
987	/// # Examples
988	///
989	/// ```
990	/// use reinhardt_rest::versioning::NamespaceVersioning;
991	/// use reinhardt_router::{RouteVersionInfo, VersionedRouter};
992	///
993	/// struct FakeRouter;
994	/// impl VersionedRouter for FakeRouter {
995	///     fn route_version_infos(&self) -> Vec<RouteVersionInfo> {
996	///         vec![
997	///             RouteVersionInfo::new(Some("v1".into()), "/v1/users/"),
998	///             RouteVersionInfo::new(Some("v2".into()), "/v2/users/"),
999	///         ]
1000	///     }
1001	/// }
1002	///
1003	/// let versioning = NamespaceVersioning::new().with_pattern("/v{version}/");
1004	/// let router = FakeRouter;
1005	///
1006	/// let versions = versioning.get_available_versions_from_router(&router);
1007	/// assert!(versions.contains(&"1".to_string()));
1008	/// assert!(versions.contains(&"2".to_string()));
1009	/// ```
1010	pub fn get_available_versions_from_router<R: reinhardt_router::VersionedRouter + ?Sized>(
1011		&self,
1012		router: &R,
1013	) -> Vec<String> {
1014		let mut versions: Vec<String> = router
1015			.route_version_infos()
1016			.into_iter()
1017			.filter_map(|info| self.extract_version_from_path(&info.path_prefix))
1018			.filter(|version| self.is_allowed_version(version))
1019			.collect();
1020		versions.sort();
1021		versions.dedup();
1022		versions
1023	}
1024}
1025
1026#[cfg(test)]
1027pub mod test_utils {
1028	use bytes::Bytes;
1029	use hyper::header::HeaderName;
1030	use hyper::{HeaderMap, Method, Uri, Version};
1031	use reinhardt_http::Request;
1032
1033	pub fn create_test_request(uri: &str, headers: Vec<(String, String)>) -> Request {
1034		let uri = uri.parse::<Uri>().unwrap();
1035		let mut header_map = HeaderMap::new();
1036		for (key, value) in headers {
1037			let header_name: HeaderName = key.parse().unwrap();
1038			header_map.insert(header_name, value.parse().unwrap());
1039		}
1040
1041		Request::builder()
1042			.method(Method::GET)
1043			.uri(uri)
1044			.version(Version::HTTP_11)
1045			.headers(header_map)
1046			.body(Bytes::new())
1047			.build()
1048			.unwrap()
1049	}
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054	use super::*;
1055	use test_utils::create_test_request;
1056
1057	#[tokio::test]
1058	async fn test_accept_header_versioning() {
1059		let versioning = AcceptHeaderVersioning::new()
1060			.with_default_version("1.0")
1061			.with_allowed_versions(vec!["1.0", "2.0"]);
1062
1063		// Test with version in Accept header
1064		let request = create_test_request(
1065			"/users/",
1066			vec![(
1067				"accept".to_string(),
1068				"application/json; version=2.0".to_string(),
1069			)],
1070		);
1071		let version = versioning.determine_version(&request).await.unwrap();
1072		assert_eq!(version, "2.0");
1073
1074		// Test without version (should return default)
1075		let request = create_test_request(
1076			"/users/",
1077			vec![("accept".to_string(), "application/json".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_url_path_versioning() {
1085		let versioning = URLPathVersioning::new()
1086			.with_default_version("1.0")
1087			.with_allowed_versions(vec!["1.0", "2.0", "2"]);
1088
1089		// Test with version in path
1090		let request = create_test_request("/v2/users/", vec![]);
1091		let version = versioning.determine_version(&request).await.unwrap();
1092		assert_eq!(version, "2");
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_hostname_versioning() {
1102		let versioning = HostNameVersioning::new()
1103			.with_default_version("1.0")
1104			.with_allowed_versions(vec!["v1", "v2"]);
1105
1106		// Test with version in hostname
1107		let request = create_test_request(
1108			"/users/",
1109			vec![("host".to_string(), "v2.api.example.com".to_string())],
1110		);
1111		let version = versioning.determine_version(&request).await.unwrap();
1112		assert_eq!(version, "v2");
1113
1114		// Test without version (should return default)
1115		let request = create_test_request(
1116			"/users/",
1117			vec![("host".to_string(), "api.example.com".to_string())],
1118		);
1119		let version = versioning.determine_version(&request).await.unwrap();
1120		assert_eq!(version, "1.0");
1121	}
1122
1123	#[tokio::test]
1124	async fn test_query_parameter_versioning() {
1125		let versioning = QueryParameterVersioning::new()
1126			.with_default_version("1.0")
1127			.with_allowed_versions(vec!["1.0", "2.0"]);
1128
1129		// Test with version in query parameter
1130		let request = create_test_request("/users/?version=2.0", vec![]);
1131		let version = versioning.determine_version(&request).await.unwrap();
1132		assert_eq!(version, "2.0");
1133
1134		// Test without version (should return default)
1135		let request = create_test_request("/users/", vec![]);
1136		let version = versioning.determine_version(&request).await.unwrap();
1137		assert_eq!(version, "1.0");
1138	}
1139
1140	#[tokio::test]
1141	async fn test_namespace_versioning() {
1142		let versioning = NamespaceVersioning::new()
1143			.with_default_version("1.0")
1144			.with_allowed_versions(vec!["1", "1.0", "2", "2.0", "3.0"]);
1145
1146		// Test with version in namespace (v1 format)
1147		let request = create_test_request("/v1/users/", vec![]);
1148		let version = versioning.determine_version(&request).await.unwrap();
1149		assert_eq!(version, "1");
1150
1151		// Test with version in namespace (v2.0 format)
1152		let request = create_test_request("/v2.0/users/", vec![]);
1153		let version = versioning.determine_version(&request).await.unwrap();
1154		assert_eq!(version, "2.0");
1155
1156		// Test without version (should return default)
1157		let request = create_test_request("/users/", vec![]);
1158		let version = versioning.determine_version(&request).await.unwrap();
1159		assert_eq!(version, "1.0");
1160
1161		// Test with non-version namespace
1162		let request = create_test_request("/api/users/", vec![]);
1163		let version = versioning.determine_version(&request).await.unwrap();
1164		assert_eq!(version, "1.0");
1165	}
1166
1167	#[tokio::test]
1168	async fn test_namespace_versioning_with_custom_pattern() {
1169		let versioning = NamespaceVersioning::new()
1170			.with_default_version("1.0")
1171			.with_pattern("/api/v{version}/")
1172			.with_allowed_versions(vec!["1", "2"]);
1173
1174		// Test with custom pattern
1175		let request = create_test_request("/api/v1/users/", vec![]);
1176		let version = versioning.determine_version(&request).await.unwrap();
1177		assert_eq!(version, "1");
1178
1179		// Test with different version
1180		let request = create_test_request("/api/v2/users/", vec![]);
1181		let version = versioning.determine_version(&request).await.unwrap();
1182		assert_eq!(version, "2");
1183
1184		// Test with old pattern (should not match)
1185		let request = create_test_request("/v1/users/", vec![]);
1186		let version = versioning.determine_version(&request).await.unwrap();
1187		assert_eq!(version, "1.0"); // Falls back to default
1188	}
1189
1190	#[tokio::test]
1191	async fn test_hostname_versioning_with_host_format_dots_not_corrupted() {
1192		// Arrange - format with dots that would be corrupted by the old implementation
1193		let versioning = HostNameVersioning::new()
1194			.with_host_format("{version}.api.v2.example.com")
1195			.with_allowed_versions(vec!["v1", "v3"]);
1196
1197		// Act
1198		let request = create_test_request(
1199			"/users/",
1200			vec![("host".to_string(), "v1.api.v2.example.com".to_string())],
1201		);
1202		let version = versioning.determine_version(&request).await.unwrap();
1203
1204		// Assert
1205		assert_eq!(version, "v1");
1206
1207		// Act - different version
1208		let request = create_test_request(
1209			"/users/",
1210			vec![("host".to_string(), "v3.api.v2.example.com".to_string())],
1211		);
1212		let version = versioning.determine_version(&request).await.unwrap();
1213
1214		// Assert
1215		assert_eq!(version, "v3");
1216	}
1217
1218	// Note: Router integration test removed to avoid circular dependency with reinhardt-urls.
1219	// Router integration tests should be placed in /tests/integration crate.
1220}