Skip to main content

reinhardt_conf/settings/
validation.rs

1//! Configuration validation framework
2//!
3//! Provides validation rules and checks for settings to ensure security
4//! and correctness before application startup.
5
6use super::profile::Profile;
7use serde_json::Value;
8use std::collections::HashMap;
9
10// Import base SettingsValidator trait from reinhardt-core
11use reinhardt_core::validators::SettingsValidator as BaseSettingsValidator;
12
13/// Validation result
14pub type ValidationResult = Result<(), ValidationError>;
15
16/// Validation error
17#[non_exhaustive]
18#[derive(Debug, thiserror::Error)]
19pub enum ValidationError {
20	/// A security-related validation failed (e.g., weak secret key).
21	#[error("Security error: {0}")]
22	Security(String),
23
24	/// A settings value is invalid for the given key.
25	#[error("Invalid value for '{key}': {message}")]
26	InvalidValue {
27		/// The settings key with the invalid value.
28		key: String,
29		/// Description of why the value is invalid.
30		message: String,
31	},
32
33	/// A required settings field is missing.
34	#[error("Missing required field: {0}")]
35	MissingRequired(String),
36
37	/// A constraint on the settings value was violated.
38	#[error("Constraint violation: {0}")]
39	Constraint(String),
40
41	/// Multiple validation errors occurred.
42	#[error("Multiple validation errors: {0:?}")]
43	Multiple(Vec<ValidationError>),
44}
45
46impl From<ValidationError> for reinhardt_core::validators::ValidationError {
47	fn from(error: ValidationError) -> Self {
48		reinhardt_core::validators::ValidationError::Custom(error.to_string())
49	}
50}
51
52/// Trait for validation rules
53pub trait Validator: Send + Sync {
54	/// Validate a specific key-value pair
55	fn validate(&self, key: &str, value: &Value) -> ValidationResult;
56
57	/// Get validator description
58	fn description(&self) -> String;
59}
60
61/// Trait for settings validators that can validate entire settings
62pub trait SettingsValidator: Send + Sync {
63	/// Validate the entire settings map
64	fn validate_settings(&self, settings: &HashMap<String, Value>) -> ValidationResult;
65
66	/// Get validator description
67	fn description(&self) -> String;
68}
69
70/// Required field validator
71pub struct RequiredValidator {
72	fields: Vec<String>,
73}
74
75impl RequiredValidator {
76	/// Create a new required field validator
77	///
78	/// # Examples
79	///
80	/// ```
81	/// use reinhardt_conf::settings::validation::RequiredValidator;
82	///
83	/// let validator = RequiredValidator::new(vec![
84	///     "secret_key".to_string(),
85	///     "database_url".to_string(),
86	/// ]);
87	/// // Validator will check that these fields exist in settings
88	/// ```
89	pub fn new(fields: Vec<String>) -> Self {
90		Self { fields }
91	}
92}
93
94impl SettingsValidator for RequiredValidator {
95	fn validate_settings(&self, settings: &HashMap<String, Value>) -> ValidationResult {
96		let mut errors = Vec::new();
97
98		for field in &self.fields {
99			if !settings.contains_key(field) {
100				errors.push(ValidationError::MissingRequired(field.clone()));
101			}
102		}
103
104		if errors.is_empty() {
105			Ok(())
106		} else {
107			Err(ValidationError::Multiple(errors))
108		}
109	}
110
111	fn description(&self) -> String {
112		format!("Required fields: {:?}", self.fields)
113	}
114}
115
116impl BaseSettingsValidator for RequiredValidator {
117	fn validate_setting(
118		&self,
119		_key: &str,
120		_value: &Value,
121	) -> reinhardt_core::validators::ValidationResult<()> {
122		// This validator checks presence, not individual values
123		// Always pass for individual settings
124		Ok(())
125	}
126
127	fn description(&self) -> String {
128		format!("Required fields: {:?}", self.fields)
129	}
130}
131
132/// Security validator for production environments
133pub struct SecurityValidator {
134	profile: Profile,
135}
136
137impl SecurityValidator {
138	/// Create a new security validator for the given profile
139	///
140	/// # Examples
141	///
142	/// ```
143	/// use reinhardt_conf::settings::validation::SecurityValidator;
144	/// use reinhardt_conf::settings::profile::Profile;
145	///
146	/// let validator = SecurityValidator::new(Profile::Production);
147	/// // Validator will enforce production security requirements
148	/// ```
149	pub fn new(profile: Profile) -> Self {
150		Self { profile }
151	}
152}
153
154impl SettingsValidator for SecurityValidator {
155	fn validate_settings(&self, settings: &HashMap<String, Value>) -> ValidationResult {
156		if !self.profile.is_production() {
157			return Ok(());
158		}
159
160		let mut errors = Vec::new();
161
162		// Check DEBUG is false in production
163		if let Some(debug) = settings.get("debug")
164			&& debug.as_bool() == Some(true)
165		{
166			errors.push(ValidationError::Security(
167				"DEBUG must be false in production".to_string(),
168			));
169		}
170
171		// Check SECRET_KEY is not default value
172		if let Some(secret_key) = settings.get("secret_key")
173			&& let Some(key_str) = secret_key.as_str()
174			&& (key_str.contains("insecure") || key_str == "change-this" || key_str.len() < 32)
175		{
176			errors.push(ValidationError::Security(
177				"SECRET_KEY must be a strong random value in production".to_string(),
178			));
179		}
180
181		// Check ALLOWED_HOSTS is set
182		if let Some(allowed_hosts) = settings.get("allowed_hosts") {
183			if let Some(hosts) = allowed_hosts.as_array()
184				&& (hosts.is_empty() || hosts.iter().any(|h| h.as_str() == Some("*")))
185			{
186				errors.push(ValidationError::Security(
187					"ALLOWED_HOSTS must be properly configured in production (no wildcards)"
188						.to_string(),
189				));
190			}
191		} else {
192			errors.push(ValidationError::Security(
193				"ALLOWED_HOSTS must be set in production".to_string(),
194			));
195		}
196
197		// Check HTTPS settings
198		if let Some(secure_ssl) = settings.get("secure_ssl_redirect") {
199			if secure_ssl.as_bool() != Some(true) {
200				errors.push(ValidationError::Security(
201					"SECURE_SSL_REDIRECT should be true in production".to_string(),
202				));
203			}
204		} else {
205			errors.push(ValidationError::Security(
206				"SECURE_SSL_REDIRECT must be set in production".to_string(),
207			));
208		}
209
210		if errors.is_empty() {
211			Ok(())
212		} else {
213			Err(ValidationError::Multiple(errors))
214		}
215	}
216
217	fn description(&self) -> String {
218		format!("Security validation for {} environment", self.profile)
219	}
220}
221
222impl BaseSettingsValidator for SecurityValidator {
223	fn validate_setting(
224		&self,
225		key: &str,
226		value: &Value,
227	) -> reinhardt_core::validators::ValidationResult<()> {
228		if !self.profile.is_production() {
229			return Ok(());
230		}
231
232		match key {
233			"debug" => {
234				if value.as_bool() == Some(true) {
235					return Err(reinhardt_core::validators::ValidationError::Custom(
236						"DEBUG must be false in production".to_string(),
237					));
238				}
239			}
240			"secret_key" => {
241				if let Some(key_str) = value.as_str()
242					&& (key_str.contains("insecure")
243						|| key_str == "change-this"
244						|| key_str.len() < 32)
245				{
246					return Err(reinhardt_core::validators::ValidationError::Custom(
247						"SECRET_KEY must be a strong random value in production".to_string(),
248					));
249				}
250			}
251			"allowed_hosts" => {
252				if let Some(hosts) = value.as_array() {
253					if hosts.is_empty() || hosts.iter().any(|h| h.as_str() == Some("*")) {
254						return Err(reinhardt_core::validators::ValidationError::Custom(
255                            "ALLOWED_HOSTS must be properly configured in production (no wildcards)".to_string(),
256                        ));
257					}
258				} else {
259					return Err(reinhardt_core::validators::ValidationError::Custom(
260						"ALLOWED_HOSTS must be an array".to_string(),
261					));
262				}
263			}
264			"secure_ssl_redirect" if value.as_bool() != Some(true) => {
265				return Err(reinhardt_core::validators::ValidationError::Custom(
266					"SECURE_SSL_REDIRECT should be true in production".to_string(),
267				));
268			}
269			_ => {}
270		}
271
272		Ok(())
273	}
274
275	fn description(&self) -> String {
276		format!("Security validation for {} environment", self.profile)
277	}
278}
279
280/// Range validator for numeric values
281pub struct RangeValidator {
282	min: Option<f64>,
283	max: Option<f64>,
284}
285
286impl RangeValidator {
287	/// Create a range validator with optional min and max
288	///
289	/// # Examples
290	///
291	/// ```
292	/// use reinhardt_conf::settings::validation::RangeValidator;
293	///
294	/// let validator = RangeValidator::new(Some(0.0), Some(100.0));
295	/// // Validator will check values are between 0 and 100
296	/// ```
297	pub fn new(min: Option<f64>, max: Option<f64>) -> Self {
298		Self { min, max }
299	}
300	/// Create a validator with only a minimum value
301	///
302	/// # Examples
303	///
304	/// ```
305	/// use reinhardt_conf::settings::validation::RangeValidator;
306	///
307	/// let validator = RangeValidator::min(0.0);
308	/// // Values must be >= 0
309	/// ```
310	pub fn min(min: f64) -> Self {
311		Self {
312			min: Some(min),
313			max: None,
314		}
315	}
316	/// Create a validator with only a maximum value
317	///
318	/// # Examples
319	///
320	/// ```
321	/// use reinhardt_conf::settings::validation::RangeValidator;
322	///
323	/// let validator = RangeValidator::max(100.0);
324	/// // Values must be <= 100
325	/// ```
326	pub fn max(max: f64) -> Self {
327		Self {
328			min: None,
329			max: Some(max),
330		}
331	}
332	/// Create a validator for a range between min and max
333	///
334	/// # Examples
335	///
336	/// ```
337	/// use reinhardt_conf::settings::validation::RangeValidator;
338	///
339	/// let validator = RangeValidator::between(1.0, 10.0);
340	/// // Values must be between 1 and 10 (inclusive)
341	/// ```
342	pub fn between(min: f64, max: f64) -> Self {
343		Self {
344			min: Some(min),
345			max: Some(max),
346		}
347	}
348}
349
350impl Validator for RangeValidator {
351	fn validate(&self, key: &str, value: &Value) -> ValidationResult {
352		if let Some(num) = value.as_f64() {
353			if let Some(min) = self.min
354				&& num < min
355			{
356				return Err(ValidationError::InvalidValue {
357					key: key.to_string(),
358					message: format!("Value {} is less than minimum {}", num, min),
359				});
360			}
361
362			if let Some(max) = self.max
363				&& num > max
364			{
365				return Err(ValidationError::InvalidValue {
366					key: key.to_string(),
367					message: format!("Value {} is greater than maximum {}", num, max),
368				});
369			}
370
371			Ok(())
372		} else {
373			Err(ValidationError::InvalidValue {
374				key: key.to_string(),
375				message: "Expected numeric value".to_string(),
376			})
377		}
378	}
379
380	fn description(&self) -> String {
381		match (self.min, self.max) {
382			(Some(min), Some(max)) => format!("Range: {} to {}", min, max),
383			(Some(min), None) => format!("Minimum: {}", min),
384			(None, Some(max)) => format!("Maximum: {}", max),
385			(None, None) => "Range validator".to_string(),
386		}
387	}
388}
389
390impl BaseSettingsValidator for RangeValidator {
391	fn validate_setting(
392		&self,
393		key: &str,
394		value: &Value,
395	) -> reinhardt_core::validators::ValidationResult<()> {
396		if let Some(num) = value.as_f64() {
397			if let Some(min) = self.min
398				&& num < min
399			{
400				return Err(reinhardt_core::validators::ValidationError::Custom(
401					format!("Value {} for '{}' is less than minimum {}", num, key, min),
402				));
403			}
404
405			if let Some(max) = self.max
406				&& num > max
407			{
408				return Err(reinhardt_core::validators::ValidationError::Custom(
409					format!(
410						"Value {} for '{}' is greater than maximum {}",
411						num, key, max
412					),
413				));
414			}
415
416			Ok(())
417		} else {
418			Err(reinhardt_core::validators::ValidationError::Custom(
419				format!("Expected numeric value for '{}'", key),
420			))
421		}
422	}
423
424	fn description(&self) -> String {
425		match (self.min, self.max) {
426			(Some(min), Some(max)) => format!("Range: {} to {}", min, max),
427			(Some(min), None) => format!("Minimum: {}", min),
428			(None, Some(max)) => format!("Maximum: {}", max),
429			(None, None) => "Range validator".to_string(),
430		}
431	}
432}
433
434/// String pattern validator
435pub struct PatternValidator {
436	pattern: regex::Regex,
437}
438
439impl PatternValidator {
440	/// Create a pattern validator with a regex pattern
441	///
442	/// # Examples
443	///
444	/// ```
445	/// use reinhardt_conf::settings::validation::PatternValidator;
446	///
447	/// let validator = PatternValidator::new(r"^\d{3}-\d{3}-\d{4}$").unwrap();
448	/// // Validates phone number format
449	/// ```
450	pub fn new(pattern: &str) -> Result<Self, regex::Error> {
451		Ok(Self {
452			pattern: regex::Regex::new(pattern)?,
453		})
454	}
455}
456
457impl Validator for PatternValidator {
458	fn validate(&self, key: &str, value: &Value) -> ValidationResult {
459		if let Some(s) = value.as_str() {
460			if self.pattern.is_match(s) {
461				Ok(())
462			} else {
463				Err(ValidationError::InvalidValue {
464					key: key.to_string(),
465					message: format!("Value does not match pattern: {}", self.pattern.as_str()),
466				})
467			}
468		} else {
469			Err(ValidationError::InvalidValue {
470				key: key.to_string(),
471				message: "Expected string value".to_string(),
472			})
473		}
474	}
475
476	fn description(&self) -> String {
477		format!("Pattern: {}", self.pattern.as_str())
478	}
479}
480
481impl BaseSettingsValidator for PatternValidator {
482	fn validate_setting(
483		&self,
484		key: &str,
485		value: &Value,
486	) -> reinhardt_core::validators::ValidationResult<()> {
487		if let Some(s) = value.as_str() {
488			if self.pattern.is_match(s) {
489				Ok(())
490			} else {
491				Err(reinhardt_core::validators::ValidationError::Custom(
492					format!(
493						"Value for '{}' does not match pattern: {}",
494						key,
495						self.pattern.as_str()
496					),
497				))
498			}
499		} else {
500			Err(reinhardt_core::validators::ValidationError::Custom(
501				format!("Expected string value for '{}'", key),
502			))
503		}
504	}
505
506	fn description(&self) -> String {
507		format!("Pattern: {}", self.pattern.as_str())
508	}
509}
510
511/// Choice validator (enum-like)
512pub struct ChoiceValidator {
513	choices: Vec<String>,
514}
515
516impl ChoiceValidator {
517	/// Create a choice validator with allowed values
518	///
519	/// # Examples
520	///
521	/// ```
522	/// use reinhardt_conf::settings::validation::ChoiceValidator;
523	///
524	/// let validator = ChoiceValidator::new(vec![
525	///     "development".to_string(),
526	///     "staging".to_string(),
527	///     "production".to_string(),
528	/// ]);
529	/// // Value must be one of the allowed choices
530	/// ```
531	pub fn new(choices: Vec<String>) -> Self {
532		Self { choices }
533	}
534}
535
536impl Validator for ChoiceValidator {
537	fn validate(&self, key: &str, value: &Value) -> ValidationResult {
538		if let Some(s) = value.as_str() {
539			if self.choices.contains(&s.to_string()) {
540				Ok(())
541			} else {
542				Err(ValidationError::InvalidValue {
543					key: key.to_string(),
544					message: format!(
545						"Value '{}' is not in allowed choices: {:?}",
546						s, self.choices
547					),
548				})
549			}
550		} else {
551			Err(ValidationError::InvalidValue {
552				key: key.to_string(),
553				message: "Expected string value".to_string(),
554			})
555		}
556	}
557
558	fn description(&self) -> String {
559		format!("Choices: {:?}", self.choices)
560	}
561}
562
563impl BaseSettingsValidator for ChoiceValidator {
564	fn validate_setting(
565		&self,
566		key: &str,
567		value: &Value,
568	) -> reinhardt_core::validators::ValidationResult<()> {
569		if let Some(s) = value.as_str() {
570			if self.choices.contains(&s.to_string()) {
571				Ok(())
572			} else {
573				Err(reinhardt_core::validators::ValidationError::Custom(
574					format!(
575						"Value '{}' for '{}' is not in allowed choices: {:?}",
576						s, key, self.choices
577					),
578				))
579			}
580		} else {
581			Err(reinhardt_core::validators::ValidationError::Custom(
582				format!("Expected string value for '{}'", key),
583			))
584		}
585	}
586
587	fn description(&self) -> String {
588		format!("Choices: {:?}", self.choices)
589	}
590}
591
592#[cfg(test)]
593mod tests {
594	use super::*;
595	use rstest::rstest;
596
597	#[test]
598	fn test_settings_validation_required() {
599		let validator = RequiredValidator::new(vec!["key1".to_string(), "key2".to_string()]);
600
601		let mut settings = HashMap::new();
602		settings.insert("key1".to_string(), Value::String("value".to_string()));
603
604		assert!(validator.validate_settings(&settings).is_err());
605
606		settings.insert("key2".to_string(), Value::String("value".to_string()));
607		assert!(validator.validate_settings(&settings).is_ok());
608	}
609
610	#[test]
611	fn test_security_validator_production() {
612		let validator = SecurityValidator::new(Profile::Production);
613
614		let mut settings = HashMap::new();
615		settings.insert("debug".to_string(), Value::Bool(true));
616		settings.insert(
617			"secret_key".to_string(),
618			Value::String("insecure".to_string()),
619		);
620
621		let result = validator.validate_settings(&settings);
622		assert!(result.is_err());
623	}
624
625	#[test]
626	fn test_security_validator_development() {
627		let validator = SecurityValidator::new(Profile::Development);
628
629		let mut settings = HashMap::new();
630		settings.insert("debug".to_string(), Value::Bool(true));
631		settings.insert(
632			"secret_key".to_string(),
633			Value::String("insecure".to_string()),
634		);
635
636		// Should pass in development
637		assert!(validator.validate_settings(&settings).is_ok());
638	}
639
640	#[test]
641	fn test_settings_range_validator() {
642		let validator = RangeValidator::between(0.0, 100.0);
643
644		assert!(validator.validate("key", &Value::Number(50.into())).is_ok());
645		assert!(
646			validator
647				.validate("key", &Value::Number((-10).into()))
648				.is_err()
649		);
650		assert!(
651			validator
652				.validate("key", &Value::Number(150.into()))
653				.is_err()
654		);
655	}
656
657	#[rstest]
658	fn test_security_validator_missing_ssl_redirect_in_production() {
659		// Arrange
660		let validator = SecurityValidator::new(Profile::Production);
661		let mut settings = HashMap::new();
662		settings.insert("debug".to_string(), Value::Bool(false));
663		settings.insert(
664			"secret_key".to_string(),
665			Value::String("a-very-long-secure-random-key-that-is-at-least-32-chars".to_string()),
666		);
667		settings.insert(
668			"allowed_hosts".to_string(),
669			Value::Array(vec![Value::String("example.com".to_string())]),
670		);
671		// Note: secure_ssl_redirect is intentionally omitted
672
673		// Act
674		let result = validator.validate_settings(&settings);
675
676		// Assert
677		let err = result.unwrap_err();
678		let error_msg = err.to_string();
679		assert!(
680			error_msg.contains("SECURE_SSL_REDIRECT must be set in production"),
681			"Expected error about missing SECURE_SSL_REDIRECT, got: {error_msg}"
682		);
683	}
684
685	#[rstest]
686	fn test_security_validator_ssl_redirect_false_in_production() {
687		// Arrange
688		let validator = SecurityValidator::new(Profile::Production);
689		let mut settings = HashMap::new();
690		settings.insert("debug".to_string(), Value::Bool(false));
691		settings.insert(
692			"secret_key".to_string(),
693			Value::String("a-very-long-secure-random-key-that-is-at-least-32-chars".to_string()),
694		);
695		settings.insert(
696			"allowed_hosts".to_string(),
697			Value::Array(vec![Value::String("example.com".to_string())]),
698		);
699		settings.insert("secure_ssl_redirect".to_string(), Value::Bool(false));
700
701		// Act
702		let result = validator.validate_settings(&settings);
703
704		// Assert
705		let err = result.unwrap_err();
706		let error_msg = err.to_string();
707		assert!(
708			error_msg.contains("SECURE_SSL_REDIRECT should be true in production"),
709			"Expected error about SECURE_SSL_REDIRECT being false, got: {error_msg}"
710		);
711	}
712
713	#[rstest]
714	fn test_security_validator_ssl_redirect_true_in_production() {
715		// Arrange
716		let validator = SecurityValidator::new(Profile::Production);
717		let mut settings = HashMap::new();
718		settings.insert("debug".to_string(), Value::Bool(false));
719		settings.insert(
720			"secret_key".to_string(),
721			Value::String("a-very-long-secure-random-key-that-is-at-least-32-chars".to_string()),
722		);
723		settings.insert(
724			"allowed_hosts".to_string(),
725			Value::Array(vec![Value::String("example.com".to_string())]),
726		);
727		settings.insert("secure_ssl_redirect".to_string(), Value::Bool(true));
728
729		// Act
730		let result = validator.validate_settings(&settings);
731
732		// Assert
733		assert!(
734			result.is_ok(),
735			"Expected validation to pass with valid production settings, got: {result:?}"
736		);
737	}
738
739	#[test]
740	fn test_settings_validation_choice() {
741		let validator =
742			ChoiceValidator::new(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
743
744		assert!(
745			validator
746				.validate("key", &Value::String("a".to_string()))
747				.is_ok()
748		);
749		assert!(
750			validator
751				.validate("key", &Value::String("d".to_string()))
752				.is_err()
753		);
754	}
755}