Skip to main content

reinhardt_rest/versioning/
settings.rs

1//! Versioning settings fragment
2//!
3//! Provides composable API versioning configuration as a
4//! [`SettingsFragment`](reinhardt_conf::settings::fragment::SettingsFragment).
5//!
6//! This fragment is read from the `[rest_versioning]` section of the project's
7//! TOML settings. Convert it into a [`VersioningConfig`](super::VersioningConfig)
8//! via [`VersioningConfig::from`](super::VersioningConfig::from) to obtain the
9//! runtime configuration consumed by
10//! [`VersioningManager`](super::VersioningManager).
11//!
12//! # Section naming
13//!
14//! The section name uses an underscore (`rest_versioning`) rather than a dotted
15//! path (`rest.versioning`) because the `#[settings]` macro generates a method
16//! identifier from the section string, and Rust identifiers cannot contain dots.
17//! A future refactor may introduce a parent `RestSettings` fragment that hosts
18//! versioning as a nested `[rest.versioning]` sub-section; until then the
19//! flat `[rest_versioning]` form is the canonical location.
20
21use super::config::VersioningStrategy;
22use reinhardt_core::macros::settings;
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25
26fn default_default_version() -> String {
27	"1.0".to_string()
28}
29
30fn default_strict_mode() -> bool {
31	true
32}
33
34fn default_strategy() -> VersioningStrategy {
35	VersioningStrategy::AcceptHeader
36}
37
38/// Versioning configuration fragment.
39///
40/// Maps to the `[rest_versioning]` TOML section. Controls the default
41/// API version, the set of allowed versions, the versioning strategy,
42/// strict-mode enforcement, and strategy-specific overrides (query
43/// parameter name, hostname patterns).
44///
45/// # Example
46///
47/// ```toml
48/// [rest_versioning]
49/// default_version = "1.0"
50/// allowed_versions = ["1.0", "2.0"]
51/// strict_mode = true
52///
53/// [rest_versioning.strategy]
54/// type = "AcceptHeader"
55/// ```
56#[settings(fragment = true, section = "rest_versioning")]
57#[non_exhaustive]
58#[derive(Clone, Debug, Serialize, Deserialize)]
59pub struct VersioningSettings {
60	/// Default version to use when no version is specified.
61	#[serde(default = "default_default_version")]
62	pub default_version: String,
63
64	/// Allowed versions (empty means any version is allowed).
65	#[serde(default)]
66	pub allowed_versions: Vec<String>,
67
68	/// Versioning strategy configuration.
69	#[serde(default = "default_strategy")]
70	pub strategy: VersioningStrategy,
71
72	/// Whether to raise errors for invalid versions.
73	#[serde(default = "default_strict_mode")]
74	pub strict_mode: bool,
75
76	/// Custom version parameter name for query parameter versioning.
77	#[serde(default)]
78	pub version_param: Option<String>,
79
80	/// Custom hostname patterns for hostname versioning.
81	#[serde(default)]
82	pub hostname_patterns: Option<HashMap<String, String>>,
83}
84
85impl Default for VersioningSettings {
86	fn default() -> Self {
87		Self {
88			default_version: default_default_version(),
89			allowed_versions: vec![],
90			strategy: default_strategy(),
91			strict_mode: default_strict_mode(),
92			version_param: None,
93			hostname_patterns: None,
94		}
95	}
96}
97
98#[cfg(test)]
99mod tests {
100	use super::*;
101	use reinhardt_conf::settings::fragment::SettingsFragment;
102	use rstest::rstest;
103
104	#[rstest]
105	fn test_versioning_section_name() {
106		// Arrange / Act
107		let section = VersioningSettings::section();
108
109		// Assert
110		assert_eq!(section, "rest_versioning");
111	}
112
113	#[rstest]
114	fn test_versioning_default_values() {
115		// Arrange / Act
116		let settings = VersioningSettings::default();
117
118		// Assert
119		assert_eq!(settings.default_version, "1.0");
120		assert!(settings.allowed_versions.is_empty());
121		assert!(matches!(
122			settings.strategy,
123			VersioningStrategy::AcceptHeader
124		));
125		assert!(settings.strict_mode);
126		assert!(settings.version_param.is_none());
127		assert!(settings.hostname_patterns.is_none());
128	}
129
130	#[rstest]
131	fn test_versioning_deserialize_from_json() {
132		// Arrange — emulate a `[rest_versioning]` TOML section after table
133		// flattening (matches what reinhardt-conf produces from TOML).
134		let json = r#"{
135			"default_version": "2.0",
136			"allowed_versions": ["1.0", "2.0", "3.0"],
137			"strict_mode": false,
138			"strategy": { "type": "URLPath", "config": { "pattern": "/v{version}/" } }
139		}"#;
140
141		// Act
142		let settings: VersioningSettings = serde_json::from_str(json).unwrap();
143
144		// Assert
145		assert_eq!(settings.default_version, "2.0");
146		assert_eq!(settings.allowed_versions, vec!["1.0", "2.0", "3.0"]);
147		assert!(!settings.strict_mode);
148		assert!(matches!(
149			settings.strategy,
150			VersioningStrategy::URLPath { .. }
151		));
152	}
153}