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" => {
265				if value.as_bool() != Some(true) {
266					return Err(reinhardt_core::validators::ValidationError::Custom(
267						"SECURE_SSL_REDIRECT should be true in production".to_string(),
268					));
269				}
270			}
271			_ => {}
272		}
273
274		Ok(())
275	}
276
277	fn description(&self) -> String {
278		format!("Security validation for {} environment", self.profile)
279	}
280}
281
282/// Range validator for numeric values
283pub struct RangeValidator {
284	min: Option<f64>,
285	max: Option<f64>,
286}
287
288impl RangeValidator {
289	/// Create a range validator with optional min and max
290	///
291	/// # Examples
292	///
293	/// ```
294	/// use reinhardt_conf::settings::validation::RangeValidator;
295	///
296	/// let validator = RangeValidator::new(Some(0.0), Some(100.0));
297	/// // Validator will check values are between 0 and 100
298	/// ```
299	pub fn new(min: Option<f64>, max: Option<f64>) -> Self {
300		Self { min, max }
301	}
302	/// Create a validator with only a minimum value
303	///
304	/// # Examples
305	///
306	/// ```
307	/// use reinhardt_conf::settings::validation::RangeValidator;
308	///
309	/// let validator = RangeValidator::min(0.0);
310	/// // Values must be >= 0
311	/// ```
312	pub fn min(min: f64) -> Self {
313		Self {
314			min: Some(min),
315			max: None,
316		}
317	}
318	/// Create a validator with only a maximum value
319	///
320	/// # Examples
321	///
322	/// ```
323	/// use reinhardt_conf::settings::validation::RangeValidator;
324	///
325	/// let validator = RangeValidator::max(100.0);
326	/// // Values must be <= 100
327	/// ```
328	pub fn max(max: f64) -> Self {
329		Self {
330			min: None,
331			max: Some(max),
332		}
333	}
334	/// Create a validator for a range between min and max
335	///
336	/// # Examples
337	///
338	/// ```
339	/// use reinhardt_conf::settings::validation::RangeValidator;
340	///
341	/// let validator = RangeValidator::between(1.0, 10.0);
342	/// // Values must be between 1 and 10 (inclusive)
343	/// ```
344	pub fn between(min: f64, max: f64) -> Self {
345		Self {
346			min: Some(min),
347			max: Some(max),
348		}
349	}
350}
351
352impl Validator for RangeValidator {
353	fn validate(&self, key: &str, value: &Value) -> ValidationResult {
354		if let Some(num) = value.as_f64() {
355			if let Some(min) = self.min
356				&& num < min
357			{
358				return Err(ValidationError::InvalidValue {
359					key: key.to_string(),
360					message: format!("Value {} is less than minimum {}", num, min),
361				});
362			}
363
364			if let Some(max) = self.max
365				&& num > max
366			{
367				return Err(ValidationError::InvalidValue {
368					key: key.to_string(),
369					message: format!("Value {} is greater than maximum {}", num, max),
370				});
371			}
372
373			Ok(())
374		} else {
375			Err(ValidationError::InvalidValue {
376				key: key.to_string(),
377				message: "Expected numeric value".to_string(),
378			})
379		}
380	}
381
382	fn description(&self) -> String {
383		match (self.min, self.max) {
384			(Some(min), Some(max)) => format!("Range: {} to {}", min, max),
385			(Some(min), None) => format!("Minimum: {}", min),
386			(None, Some(max)) => format!("Maximum: {}", max),
387			(None, None) => "Range validator".to_string(),
388		}
389	}
390}
391
392impl BaseSettingsValidator for RangeValidator {
393	fn validate_setting(
394		&self,
395		key: &str,
396		value: &Value,
397	) -> reinhardt_core::validators::ValidationResult<()> {
398		if let Some(num) = value.as_f64() {
399			if let Some(min) = self.min
400				&& num < min
401			{
402				return Err(reinhardt_core::validators::ValidationError::Custom(
403					format!("Value {} for '{}' is less than minimum {}", num, key, min),
404				));
405			}
406
407			if let Some(max) = self.max
408				&& num > max
409			{
410				return Err(reinhardt_core::validators::ValidationError::Custom(
411					format!(
412						"Value {} for '{}' is greater than maximum {}",
413						num, key, max
414					),
415				));
416			}
417
418			Ok(())
419		} else {
420			Err(reinhardt_core::validators::ValidationError::Custom(
421				format!("Expected numeric value for '{}'", key),
422			))
423		}
424	}
425
426	fn description(&self) -> String {
427		match (self.min, self.max) {
428			(Some(min), Some(max)) => format!("Range: {} to {}", min, max),
429			(Some(min), None) => format!("Minimum: {}", min),
430			(None, Some(max)) => format!("Maximum: {}", max),
431			(None, None) => "Range validator".to_string(),
432		}
433	}
434}
435
436/// String pattern validator
437pub struct PatternValidator {
438	pattern: regex::Regex,
439}
440
441impl PatternValidator {
442	/// Create a pattern validator with a regex pattern
443	///
444	/// # Examples
445	///
446	/// ```
447	/// use reinhardt_conf::settings::validation::PatternValidator;
448	///
449	/// let validator = PatternValidator::new(r"^\d{3}-\d{3}-\d{4}$").unwrap();
450	/// // Validates phone number format
451	/// ```
452	pub fn new(pattern: &str) -> Result<Self, regex::Error> {
453		Ok(Self {
454			pattern: regex::Regex::new(pattern)?,
455		})
456	}
457}
458
459impl Validator for PatternValidator {
460	fn validate(&self, key: &str, value: &Value) -> ValidationResult {
461		if let Some(s) = value.as_str() {
462			if self.pattern.is_match(s) {
463				Ok(())
464			} else {
465				Err(ValidationError::InvalidValue {
466					key: key.to_string(),
467					message: format!("Value does not match pattern: {}", self.pattern.as_str()),
468				})
469			}
470		} else {
471			Err(ValidationError::InvalidValue {
472				key: key.to_string(),
473				message: "Expected string value".to_string(),
474			})
475		}
476	}
477
478	fn description(&self) -> String {
479		format!("Pattern: {}", self.pattern.as_str())
480	}
481}
482
483impl BaseSettingsValidator for PatternValidator {
484	fn validate_setting(
485		&self,
486		key: &str,
487		value: &Value,
488	) -> reinhardt_core::validators::ValidationResult<()> {
489		if let Some(s) = value.as_str() {
490			if self.pattern.is_match(s) {
491				Ok(())
492			} else {
493				Err(reinhardt_core::validators::ValidationError::Custom(
494					format!(
495						"Value for '{}' does not match pattern: {}",
496						key,
497						self.pattern.as_str()
498					),
499				))
500			}
501		} else {
502			Err(reinhardt_core::validators::ValidationError::Custom(
503				format!("Expected string value for '{}'", key),
504			))
505		}
506	}
507
508	fn description(&self) -> String {
509		format!("Pattern: {}", self.pattern.as_str())
510	}
511}
512
513/// Choice validator (enum-like)
514pub struct ChoiceValidator {
515	choices: Vec<String>,
516}
517
518impl ChoiceValidator {
519	/// Create a choice validator with allowed values
520	///
521	/// # Examples
522	///
523	/// ```
524	/// use reinhardt_conf::settings::validation::ChoiceValidator;
525	///
526	/// let validator = ChoiceValidator::new(vec![
527	///     "development".to_string(),
528	///     "staging".to_string(),
529	///     "production".to_string(),
530	/// ]);
531	/// // Value must be one of the allowed choices
532	/// ```
533	pub fn new(choices: Vec<String>) -> Self {
534		Self { choices }
535	}
536}
537
538impl Validator for ChoiceValidator {
539	fn validate(&self, key: &str, value: &Value) -> ValidationResult {
540		if let Some(s) = value.as_str() {
541			if self.choices.contains(&s.to_string()) {
542				Ok(())
543			} else {
544				Err(ValidationError::InvalidValue {
545					key: key.to_string(),
546					message: format!(
547						"Value '{}' is not in allowed choices: {:?}",
548						s, self.choices
549					),
550				})
551			}
552		} else {
553			Err(ValidationError::InvalidValue {
554				key: key.to_string(),
555				message: "Expected string value".to_string(),
556			})
557		}
558	}
559
560	fn description(&self) -> String {
561		format!("Choices: {:?}", self.choices)
562	}
563}
564
565impl BaseSettingsValidator for ChoiceValidator {
566	fn validate_setting(
567		&self,
568		key: &str,
569		value: &Value,
570	) -> reinhardt_core::validators::ValidationResult<()> {
571		if let Some(s) = value.as_str() {
572			if self.choices.contains(&s.to_string()) {
573				Ok(())
574			} else {
575				Err(reinhardt_core::validators::ValidationError::Custom(
576					format!(
577						"Value '{}' for '{}' is not in allowed choices: {:?}",
578						s, key, self.choices
579					),
580				))
581			}
582		} else {
583			Err(reinhardt_core::validators::ValidationError::Custom(
584				format!("Expected string value for '{}'", key),
585			))
586		}
587	}
588
589	fn description(&self) -> String {
590		format!("Choices: {:?}", self.choices)
591	}
592}
593
594#[cfg(test)]
595mod tests {
596	use super::*;
597	use rstest::rstest;
598
599	#[test]
600	fn test_settings_validation_required() {
601		let validator = RequiredValidator::new(vec!["key1".to_string(), "key2".to_string()]);
602
603		let mut settings = HashMap::new();
604		settings.insert("key1".to_string(), Value::String("value".to_string()));
605
606		assert!(validator.validate_settings(&settings).is_err());
607
608		settings.insert("key2".to_string(), Value::String("value".to_string()));
609		assert!(validator.validate_settings(&settings).is_ok());
610	}
611
612	#[test]
613	fn test_security_validator_production() {
614		let validator = SecurityValidator::new(Profile::Production);
615
616		let mut settings = HashMap::new();
617		settings.insert("debug".to_string(), Value::Bool(true));
618		settings.insert(
619			"secret_key".to_string(),
620			Value::String("insecure".to_string()),
621		);
622
623		let result = validator.validate_settings(&settings);
624		assert!(result.is_err());
625	}
626
627	#[test]
628	fn test_security_validator_development() {
629		let validator = SecurityValidator::new(Profile::Development);
630
631		let mut settings = HashMap::new();
632		settings.insert("debug".to_string(), Value::Bool(true));
633		settings.insert(
634			"secret_key".to_string(),
635			Value::String("insecure".to_string()),
636		);
637
638		// Should pass in development
639		assert!(validator.validate_settings(&settings).is_ok());
640	}
641
642	#[test]
643	fn test_settings_range_validator() {
644		let validator = RangeValidator::between(0.0, 100.0);
645
646		assert!(validator.validate("key", &Value::Number(50.into())).is_ok());
647		assert!(
648			validator
649				.validate("key", &Value::Number((-10).into()))
650				.is_err()
651		);
652		assert!(
653			validator
654				.validate("key", &Value::Number(150.into()))
655				.is_err()
656		);
657	}
658
659	#[rstest]
660	fn test_security_validator_missing_ssl_redirect_in_production() {
661		// Arrange
662		let validator = SecurityValidator::new(Profile::Production);
663		let mut settings = HashMap::new();
664		settings.insert("debug".to_string(), Value::Bool(false));
665		settings.insert(
666			"secret_key".to_string(),
667			Value::String("a-very-long-secure-random-key-that-is-at-least-32-chars".to_string()),
668		);
669		settings.insert(
670			"allowed_hosts".to_string(),
671			Value::Array(vec![Value::String("example.com".to_string())]),
672		);
673		// Note: secure_ssl_redirect is intentionally omitted
674
675		// Act
676		let result = validator.validate_settings(&settings);
677
678		// Assert
679		let err = result.unwrap_err();
680		let error_msg = err.to_string();
681		assert!(
682			error_msg.contains("SECURE_SSL_REDIRECT must be set in production"),
683			"Expected error about missing SECURE_SSL_REDIRECT, got: {error_msg}"
684		);
685	}
686
687	#[rstest]
688	fn test_security_validator_ssl_redirect_false_in_production() {
689		// Arrange
690		let validator = SecurityValidator::new(Profile::Production);
691		let mut settings = HashMap::new();
692		settings.insert("debug".to_string(), Value::Bool(false));
693		settings.insert(
694			"secret_key".to_string(),
695			Value::String("a-very-long-secure-random-key-that-is-at-least-32-chars".to_string()),
696		);
697		settings.insert(
698			"allowed_hosts".to_string(),
699			Value::Array(vec![Value::String("example.com".to_string())]),
700		);
701		settings.insert("secure_ssl_redirect".to_string(), Value::Bool(false));
702
703		// Act
704		let result = validator.validate_settings(&settings);
705
706		// Assert
707		let err = result.unwrap_err();
708		let error_msg = err.to_string();
709		assert!(
710			error_msg.contains("SECURE_SSL_REDIRECT should be true in production"),
711			"Expected error about SECURE_SSL_REDIRECT being false, got: {error_msg}"
712		);
713	}
714
715	#[rstest]
716	fn test_security_validator_ssl_redirect_true_in_production() {
717		// Arrange
718		let validator = SecurityValidator::new(Profile::Production);
719		let mut settings = HashMap::new();
720		settings.insert("debug".to_string(), Value::Bool(false));
721		settings.insert(
722			"secret_key".to_string(),
723			Value::String("a-very-long-secure-random-key-that-is-at-least-32-chars".to_string()),
724		);
725		settings.insert(
726			"allowed_hosts".to_string(),
727			Value::Array(vec![Value::String("example.com".to_string())]),
728		);
729		settings.insert("secure_ssl_redirect".to_string(), Value::Bool(true));
730
731		// Act
732		let result = validator.validate_settings(&settings);
733
734		// Assert
735		assert!(
736			result.is_ok(),
737			"Expected validation to pass with valid production settings, got: {result:?}"
738		);
739	}
740
741	#[test]
742	fn test_settings_validation_choice() {
743		let validator =
744			ChoiceValidator::new(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
745
746		assert!(
747			validator
748				.validate("key", &Value::String("a".to_string()))
749				.is_ok()
750		);
751		assert!(
752			validator
753				.validate("key", &Value::String("d".to_string()))
754				.is_err()
755		);
756	}
757}