1use super::profile::Profile;
7use serde_json::Value;
8use std::collections::HashMap;
9
10use reinhardt_core::validators::SettingsValidator as BaseSettingsValidator;
12
13pub type ValidationResult = Result<(), ValidationError>;
15
16#[non_exhaustive]
18#[derive(Debug, thiserror::Error)]
19pub enum ValidationError {
20 #[error("Security error: {0}")]
22 Security(String),
23
24 #[error("Invalid value for '{key}': {message}")]
26 InvalidValue {
27 key: String,
29 message: String,
31 },
32
33 #[error("Missing required field: {0}")]
35 MissingRequired(String),
36
37 #[error("Constraint violation: {0}")]
39 Constraint(String),
40
41 #[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
52pub trait Validator: Send + Sync {
54 fn validate(&self, key: &str, value: &Value) -> ValidationResult;
56
57 fn description(&self) -> String;
59}
60
61pub trait SettingsValidator: Send + Sync {
63 fn validate_settings(&self, settings: &HashMap<String, Value>) -> ValidationResult;
65
66 fn description(&self) -> String;
68}
69
70pub struct RequiredValidator {
72 fields: Vec<String>,
73}
74
75impl RequiredValidator {
76 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 Ok(())
125 }
126
127 fn description(&self) -> String {
128 format!("Required fields: {:?}", self.fields)
129 }
130}
131
132pub struct SecurityValidator {
134 profile: Profile,
135}
136
137impl SecurityValidator {
138 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 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 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 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 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
282pub struct RangeValidator {
284 min: Option<f64>,
285 max: Option<f64>,
286}
287
288impl RangeValidator {
289 pub fn new(min: Option<f64>, max: Option<f64>) -> Self {
300 Self { min, max }
301 }
302 pub fn min(min: f64) -> Self {
313 Self {
314 min: Some(min),
315 max: None,
316 }
317 }
318 pub fn max(max: f64) -> Self {
329 Self {
330 min: None,
331 max: Some(max),
332 }
333 }
334 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
436pub struct PatternValidator {
438 pattern: regex::Regex,
439}
440
441impl PatternValidator {
442 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
513pub struct ChoiceValidator {
515 choices: Vec<String>,
516}
517
518impl ChoiceValidator {
519 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 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 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 let result = validator.validate_settings(&settings);
677
678 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 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 let result = validator.validate_settings(&settings);
705
706 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 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 let result = validator.validate_settings(&settings);
733
734 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}