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" 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
280pub struct RangeValidator {
282 min: Option<f64>,
283 max: Option<f64>,
284}
285
286impl RangeValidator {
287 pub fn new(min: Option<f64>, max: Option<f64>) -> Self {
298 Self { min, max }
299 }
300 pub fn min(min: f64) -> Self {
311 Self {
312 min: Some(min),
313 max: None,
314 }
315 }
316 pub fn max(max: f64) -> Self {
327 Self {
328 min: None,
329 max: Some(max),
330 }
331 }
332 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
434pub struct PatternValidator {
436 pattern: regex::Regex,
437}
438
439impl PatternValidator {
440 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
511pub struct ChoiceValidator {
513 choices: Vec<String>,
514}
515
516impl ChoiceValidator {
517 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 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 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 let result = validator.validate_settings(&settings);
675
676 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 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 let result = validator.validate_settings(&settings);
703
704 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 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 let result = validator.validate_settings(&settings);
731
732 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}