dmsc/validation/
mod.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18#![allow(non_snake_case)]
19
20//! # Validation Module
21//!
22//! This module provides comprehensive validation utilities for DMSC, including
23//! input validation, schema validation, and data verification. It supports
24//! various validation rules and provides detailed error messages.
25//!
26//! ## Key Components
27//!
28//! - **DMSCValidator**: Core validation trait
29//! - **DMSCValidationRule**: Individual validation rule
30//! - **DMSCValidationResult**: Validation result with details
31//! - **Built-in Validators**: Email, URL, length, pattern, range, etc.
32//!
33//! ## Design Principles
34//!
35//! 1. **Composable**: Rules can be combined using `and`, `or`, `not`
36//! 2. **Extensible**: Easy to implement custom validation rules
37//! 3. **Type-safe**: Strongly typed validation for different data types
38//! 4. **Informative**: Detailed error messages with field locations
39//! 5. **Async Support**: Async validation for I/O-bound checks
40//! 6. **Schema Validation**: JSON Schema support for complex structures
41//!
42//! ## Usage
43//!
44//! ```rust,ignore
45//! use dmsc::validation::{Validator, ValidationRule, DMSCValidator};
46//! use dmsc::prelude::*;
47//!
48//! let validator = DMSCValidator::new("user_email")
49//!     .not_empty()
50//!     .is_email()
51//!     .max_length(255);
52//!
53//! let result = validator.validate("test@example.com");
54//! assert!(result.is_valid());
55//! ```
56
57
58use async_trait::async_trait;
59use serde::{Deserialize, Serialize};
60use std::sync::Arc;
61use regex::Regex;
62use url::Url;
63use lazy_static::lazy_static;
64
65#[cfg(feature = "pyo3")]
66use pyo3::prelude::*;
67
68#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70pub enum DMSCValidationSeverity {
71    Error,
72    Warning,
73    Info,
74    Critical,
75}
76
77#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct DMSCValidationError {
80    pub field: String,
81    pub message: String,
82    pub code: String,
83    pub severity: DMSCValidationSeverity,
84    pub value: Option<serde_json::Value>,
85}
86
87impl DMSCValidationError {
88    pub fn new(field: &str, message: &str, code: &str) -> Self {
89        Self {
90            field: field.to_string(),
91            message: message.to_string(),
92            code: code.to_string(),
93            severity: DMSCValidationSeverity::Error,
94            value: None,
95        }
96    }
97
98    pub fn with_value(field: &str, message: &str, code: &str, value: serde_json::Value) -> Self {
99        Self {
100            field: field.to_string(),
101            message: message.to_string(),
102            code: code.to_string(),
103            severity: DMSCValidationSeverity::Error,
104            value: Some(value),
105        }
106    }
107
108    pub fn warning(field: &str, message: &str, code: &str) -> Self {
109        Self {
110            field: field.to_string(),
111            message: message.to_string(),
112            code: code.to_string(),
113            severity: DMSCValidationSeverity::Warning,
114            value: None,
115        }
116    }
117}
118
119#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct DMSCValidationResult {
122    pub is_valid: bool,
123    pub errors: Vec<DMSCValidationError>,
124    pub warnings: Vec<DMSCValidationError>,
125}
126
127impl DMSCValidationResult {
128    pub fn valid() -> Self {
129        Self {
130            is_valid: true,
131            errors: Vec::new(),
132            warnings: Vec::new(),
133        }
134    }
135
136    pub fn invalid(errors: Vec<DMSCValidationError>) -> Self {
137        let warnings: Vec<DMSCValidationError> = errors
138            .iter()
139            .filter(|e| e.severity == DMSCValidationSeverity::Warning)
140            .cloned()
141            .collect();
142
143        let errors: Vec<DMSCValidationError> = errors
144            .into_iter()
145            .filter(|e| e.severity == DMSCValidationSeverity::Error)
146            .collect();
147
148        Self {
149            is_valid: errors.is_empty(),
150            errors,
151            warnings,
152        }
153    }
154
155    pub fn add_error(&mut self, error: DMSCValidationError) {
156        if error.severity == DMSCValidationSeverity::Error {
157            self.is_valid = false;
158            self.errors.push(error);
159        } else {
160            self.warnings.push(error);
161        }
162    }
163
164    pub fn merge(&mut self, other: DMSCValidationResult) {
165        self.errors.extend(other.errors);
166        self.warnings.extend(other.warnings);
167        self.is_valid = self.errors.is_empty();
168    }
169
170    pub fn error_count(&self) -> usize {
171        self.errors.len()
172    }
173
174    pub fn warning_count(&self) -> usize {
175        self.warnings.len()
176    }
177
178    pub fn to_string(&self) -> String {
179        if self.is_valid {
180            "Validation passed".to_string()
181        } else {
182            format!(
183                "Validation failed with {} error(s): {}",
184                self.error_count(),
185                self.errors
186                    .iter()
187                    .map(|e| format!("{}: {}", e.field, e.message))
188                    .collect::<Vec<_>>()
189                    .join(", ")
190            )
191        }
192    }
193}
194
195#[cfg(feature = "pyo3")]
196#[pymethods]
197impl DMSCValidationResult {
198    #[new]
199    fn py_new(is_valid: bool) -> Self {
200        if is_valid {
201            Self::valid()
202        } else {
203            Self::invalid(vec![])
204        }
205    }
206
207    #[staticmethod]
208    fn success() -> Self {
209        Self::valid()
210    }
211
212    #[staticmethod]
213    fn failure(errors: Vec<DMSCValidationError>) -> Self {
214        Self::invalid(errors)
215    }
216
217    fn __str__(&self) -> String {
218        self.to_string()
219    }
220}
221
222#[async_trait]
223pub trait DMSCValidator: Send + Sync {
224    async fn validate(&self, value: &str) -> DMSCValidationResult;
225    fn name(&self) -> &'static str;
226}
227
228#[async_trait]
229impl DMSCValidator for Box<dyn DMSCValidator> {
230    async fn validate(&self, value: &str) -> DMSCValidationResult {
231        self.as_ref().validate(value).await
232    }
233
234    fn name(&self) -> &'static str {
235        self.as_ref().name()
236    }
237}
238
239pub trait DMSCValidationRule: Send + Sync {
240    fn validate(&self, value: &str) -> Option<DMSCValidationError>;
241    fn name(&self) -> &'static str;
242}
243
244#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
245pub struct DMSCValidatorBuilder {
246    field_name: String,
247    rules: Vec<Arc<dyn DMSCValidationRule>>,
248    nullable: bool,
249    optional: bool,
250}
251
252impl DMSCValidatorBuilder {
253    pub fn new(field_name: &str) -> Self {
254        Self {
255            field_name: field_name.to_string(),
256            rules: Vec::new(),
257            nullable: false,
258            optional: false,
259        }
260    }
261
262    pub fn with_nullable(mut self, nullable: bool) -> Self {
263        self.nullable = nullable;
264        self
265    }
266
267    pub fn with_optional(mut self, optional: bool) -> Self {
268        self.optional = optional;
269        self
270    }
271
272    pub fn not_empty(self) -> Self {
273        self.add_rule(NotEmptyRule)
274    }
275
276    pub fn is_email(self) -> Self {
277        self.add_rule(EmailRule)
278    }
279
280    pub fn is_url(self) -> Self {
281        self.add_rule(UrlRule)
282    }
283
284    pub fn is_ip(self) -> Self {
285        self.add_rule(IpAddressRule)
286    }
287
288    pub fn is_uuid(self) -> Self {
289        self.add_rule(UuidRule)
290    }
291
292    pub fn is_base64(self) -> Self {
293        self.add_rule(Base64Rule)
294    }
295
296    pub fn min_length(self, min: usize) -> Self {
297        self.add_rule(MinLengthRule(min))
298    }
299
300    pub fn max_length(self, max: usize) -> Self {
301        self.add_rule(MaxLengthRule(max))
302    }
303
304    pub fn exact_length(self, length: usize) -> Self {
305        self.add_rule(ExactLengthRule(length))
306    }
307
308    pub fn min_value(self, min: i64) -> Self {
309        self.add_rule(MinValueRule(min))
310    }
311
312    pub fn max_value(self, max: i64) -> Self {
313        self.add_rule(MaxValueRule(max))
314    }
315
316    pub fn range(self, min: i64, max: i64) -> Self {
317        self.add_rule(RangeRule(min, max))
318    }
319
320    pub fn matches_regex(self, pattern: &str) -> Self {
321        self.add_rule(RegexRule(pattern.to_string()))
322    }
323
324    pub fn alphanumeric(self) -> Self {
325        self.add_rule(AlphanumericRule)
326    }
327
328    pub fn alphabetic(self) -> Self {
329        self.add_rule(AlphabeticRule)
330    }
331
332    pub fn numeric(self) -> Self {
333        self.add_rule(NumericRule)
334    }
335
336    pub fn lowercase(self) -> Self {
337        self.add_rule(LowercaseRule)
338    }
339
340    pub fn uppercase(self) -> Self {
341        self.add_rule(UppercaseRule)
342    }
343
344    pub fn contains(self, substring: &str) -> Self {
345        self.add_rule(ContainsRule(substring.to_string()))
346    }
347
348    pub fn starts_with(self, prefix: &str) -> Self {
349        self.add_rule(StartsWithRule(prefix.to_string()))
350    }
351
352    pub fn ends_with(self, suffix: &str) -> Self {
353        self.add_rule(EndsWithRule(suffix.to_string()))
354    }
355
356    pub fn is_in(self, values: Vec<String>) -> Self {
357        self.add_rule(InRule(values))
358    }
359
360    pub fn not_in(self, values: Vec<String>) -> Self {
361        self.add_rule(NotInRule(values))
362    }
363
364    fn add_rule(mut self, rule: impl DMSCValidationRule + Send + Sync + 'static) -> Self {
365        self.rules.push(Arc::new(rule));
366        self
367    }
368
369    pub fn build(self) -> DMSCValidationRunner {
370        DMSCValidationRunner {
371            field_name: self.field_name,
372            rules: self.rules,
373            nullable: self.nullable,
374            optional: self.optional,
375        }
376    }
377}
378
379#[cfg(feature = "pyo3")]
380#[pymethods]
381impl DMSCValidatorBuilder {
382    #[new]
383    fn py_new(field_name: String) -> Self {
384        Self {
385            field_name,
386            rules: Vec::new(),
387            nullable: false,
388            optional: false,
389        }
390    }
391
392    fn set_nullable(&mut self, nullable: bool) {
393        self.nullable = nullable;
394    }
395
396    fn set_optional(&mut self, optional: bool) {
397        self.optional = optional;
398    }
399
400    fn add_not_empty(&mut self) {
401        self.rules.push(Arc::new(NotEmptyRule));
402    }
403
404    fn add_email(&mut self) {
405        self.rules.push(Arc::new(EmailRule));
406    }
407
408    fn add_url(&mut self) {
409        self.rules.push(Arc::new(UrlRule));
410    }
411
412    fn add_ip(&mut self) {
413        self.rules.push(Arc::new(IpAddressRule));
414    }
415
416    fn add_uuid(&mut self) {
417        self.rules.push(Arc::new(UuidRule));
418    }
419
420    fn add_base64(&mut self) {
421        self.rules.push(Arc::new(Base64Rule));
422    }
423
424    fn add_min_length(&mut self, min: usize) {
425        self.rules.push(Arc::new(MinLengthRule(min)));
426    }
427
428    fn add_max_length(&mut self, max: usize) {
429        self.rules.push(Arc::new(MaxLengthRule(max)));
430    }
431
432    fn add_min_value(&mut self, min: i64) {
433        self.rules.push(Arc::new(MinValueRule(min)));
434    }
435
436    fn add_max_value(&mut self, max: i64) {
437        self.rules.push(Arc::new(MaxValueRule(max)));
438    }
439
440    fn add_range(&mut self, min: i64, max: i64) {
441        self.rules.push(Arc::new(RangeRule(min, max)));
442    }
443
444    fn add_alphanumeric(&mut self) {
445        self.rules.push(Arc::new(AlphanumericRule));
446    }
447
448    fn add_numeric(&mut self) {
449        self.rules.push(Arc::new(NumericRule));
450    }
451
452    fn add_contains(&mut self, substring: String) {
453        self.rules.push(Arc::new(ContainsRule(substring)));
454    }
455}
456
457#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
458pub struct DMSCValidationRunner {
459    field_name: String,
460    rules: Vec<Arc<dyn DMSCValidationRule>>,
461    nullable: bool,
462    optional: bool,
463}
464
465impl DMSCValidationRunner {
466    pub fn new(field_name: &str) -> DMSCValidatorBuilder {
467        DMSCValidatorBuilder::new(field_name)
468    }
469
470    pub fn validate_value(&self, value: Option<&str>) -> DMSCValidationResult {
471        let value = match value {
472            Some(v) => v,
473            None if self.optional => return DMSCValidationResult::valid(),
474            None if self.nullable => return DMSCValidationResult::valid(),
475            None => {
476                return DMSCValidationResult::invalid(vec![DMSCValidationError::new(
477                    &self.field_name,
478                    "Value is required",
479                    "REQUIRED",
480                )]);
481            }
482        };
483
484        let mut errors = Vec::new();
485
486        for rule in &self.rules {
487            if let Some(error) = rule.validate(value) {
488                errors.push(DMSCValidationError {
489                    field: self.field_name.clone(),
490                    ..error
491                });
492            }
493        }
494
495        if errors.is_empty() {
496            DMSCValidationResult::valid()
497        } else {
498            DMSCValidationResult::invalid(errors)
499        }
500    }
501}
502
503#[cfg(feature = "pyo3")]
504#[pymethods]
505impl DMSCValidationRunner {
506    #[new]
507    fn py_new(field_name: String) -> Self {
508        DMSCValidatorBuilder::new(&field_name).build()
509    }
510
511    fn validate(&self, value: String) -> DMSCValidationResult {
512        self.validate_value(Some(&value))
513    }
514
515    fn validate_optional(&self, value: Option<String>) -> DMSCValidationResult {
516        self.validate_value(value.as_deref())
517    }
518}
519
520struct NotEmptyRule;
521
522impl DMSCValidationRule for NotEmptyRule {
523    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
524        if value.trim().is_empty() {
525            Some(DMSCValidationError::new(
526                "value",
527                "Value cannot be empty",
528                "NOT_EMPTY",
529            ))
530        } else {
531            None
532        }
533    }
534
535    fn name(&self) -> &'static str {
536        "NotEmpty"
537    }
538}
539
540lazy_static! {
541    static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
542    static ref URL_REGEX: Regex = Regex::new(r"^https?://[^\s]+$").unwrap();
543    static ref IP_REGEX: Regex = Regex::new(r"^(\d{1,3}\.){3}\d{1,3}$").unwrap();
544    static ref UUID_REGEX: Regex = Regex::new(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$").unwrap();
545    static ref BASE64_REGEX: Regex = Regex::new(r"^[A-Za-z0-9+/]*={0,2}$").unwrap();
546    static ref ALPHANUMERIC_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9]+$").unwrap();
547    static ref ALPHABETIC_REGEX: Regex = Regex::new(r"^[a-zA-Z]+$").unwrap();
548    static ref NUMERIC_REGEX: Regex = Regex::new(r"^-?\d+(\.\d+)?$").unwrap();
549}
550
551struct EmailRule;
552
553impl DMSCValidationRule for EmailRule {
554    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
555        if !EMAIL_REGEX.is_match(value) {
556            Some(DMSCValidationError::new(
557                "value",
558                "Invalid email format",
559                "EMAIL",
560            ))
561        } else {
562            None
563        }
564    }
565
566    fn name(&self) -> &'static str {
567        "Email"
568    }
569}
570
571struct UrlRule;
572
573impl DMSCValidationRule for UrlRule {
574    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
575        if Url::parse(value).is_err() {
576            Some(DMSCValidationError::new(
577                "value",
578                "Invalid URL format",
579                "URL",
580            ))
581        } else {
582            None
583        }
584    }
585
586    fn name(&self) -> &'static str {
587        "Url"
588    }
589}
590
591struct IpAddressRule;
592
593impl DMSCValidationRule for IpAddressRule {
594    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
595        if !IP_REGEX.is_match(value) {
596            return Some(DMSCValidationError::new(
597                "value",
598                "Invalid IP address format",
599                "IP_ADDRESS",
600            ));
601        }
602
603        let parts: Vec<&str> = value.split('.').collect();
604        for part in parts {
605            if let Ok(num) = part.parse::<u32>() {
606                if num > 255 {
607                    return Some(DMSCValidationError::new(
608                        "value",
609                        "IP address octet out of range",
610                        "IP_ADDRESS_RANGE",
611                    ));
612                }
613            }
614        }
615
616        None
617    }
618
619    fn name(&self) -> &'static str {
620        "IpAddress"
621    }
622}
623
624struct UuidRule;
625
626impl DMSCValidationRule for UuidRule {
627    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
628        if !UUID_REGEX.is_match(value) {
629            Some(DMSCValidationError::new(
630                "value",
631                "Invalid UUID format",
632                "UUID",
633            ))
634        } else {
635            None
636        }
637    }
638
639    fn name(&self) -> &'static str {
640        "Uuid"
641    }
642}
643
644struct Base64Rule;
645
646impl DMSCValidationRule for Base64Rule {
647    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
648        if !BASE64_REGEX.is_match(value) {
649            Some(DMSCValidationError::new(
650                "value",
651                "Invalid Base64 format",
652                "BASE64",
653            ))
654        } else {
655            None
656        }
657    }
658
659    fn name(&self) -> &'static str {
660        "Base64"
661    }
662}
663
664struct MinLengthRule(usize);
665
666impl DMSCValidationRule for MinLengthRule {
667    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
668        if value.len() < self.0 {
669            Some(DMSCValidationError::new(
670                "value",
671                &format!("Value must be at least {} characters", self.0),
672                "MIN_LENGTH",
673            ))
674        } else {
675            None
676        }
677    }
678
679    fn name(&self) -> &'static str {
680        "MinLength"
681    }
682}
683
684struct MaxLengthRule(usize);
685
686impl DMSCValidationRule for MaxLengthRule {
687    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
688        if value.len() > self.0 {
689            Some(DMSCValidationError::new(
690                "value",
691                &format!("Value must be at most {} characters", self.0),
692                "MAX_LENGTH",
693            ))
694        } else {
695            None
696        }
697    }
698
699    fn name(&self) -> &'static str {
700        "MaxLength"
701    }
702}
703
704struct ExactLengthRule(usize);
705
706impl DMSCValidationRule for ExactLengthRule {
707    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
708        if value.len() != self.0 {
709            Some(DMSCValidationError::new(
710                "value",
711                &format!("Value must be exactly {} characters", self.0),
712                "EXACT_LENGTH",
713            ))
714        } else {
715            None
716        }
717    }
718
719    fn name(&self) -> &'static str {
720        "ExactLength"
721    }
722}
723
724struct MinValueRule(i64);
725
726impl DMSCValidationRule for MinValueRule {
727    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
728        if let Ok(num) = value.parse::<i64>() {
729            if num < self.0 {
730                return Some(DMSCValidationError::new(
731                    "value",
732                    &format!("Value must be at least {}", self.0),
733                    "MIN_VALUE",
734                ));
735            }
736        }
737        None
738    }
739
740    fn name(&self) -> &'static str {
741        "MinValue"
742    }
743}
744
745struct MaxValueRule(i64);
746
747impl DMSCValidationRule for MaxValueRule {
748    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
749        if let Ok(num) = value.parse::<i64>() {
750            if num > self.0 {
751                return Some(DMSCValidationError::new(
752                    "value",
753                    &format!("Value must be at most {}", self.0),
754                    "MAX_VALUE",
755                ));
756            }
757        }
758        None
759    }
760
761    fn name(&self) -> &'static str {
762        "MaxValue"
763    }
764}
765
766struct RangeRule(i64, i64);
767
768impl DMSCValidationRule for RangeRule {
769    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
770        if let Ok(num) = value.parse::<i64>() {
771            if num < self.0 || num > self.1 {
772                return Some(DMSCValidationError::new(
773                    "value",
774                    &format!("Value must be between {} and {}", self.0, self.1),
775                    "RANGE",
776                ));
777            }
778        }
779        None
780    }
781
782    fn name(&self) -> &'static str {
783        "Range"
784    }
785}
786
787struct RegexRule(String);
788
789impl DMSCValidationRule for RegexRule {
790    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
791        if let Ok(regex) = Regex::new(&self.0) {
792            if !regex.is_match(value) {
793                return Some(DMSCValidationError::new(
794                    "value",
795                    "Value does not match required pattern",
796                    "REGEX",
797                ));
798            }
799        }
800        None
801    }
802
803    fn name(&self) -> &'static str {
804        "Regex"
805    }
806}
807
808struct AlphanumericRule;
809
810impl DMSCValidationRule for AlphanumericRule {
811    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
812        if !ALPHANUMERIC_REGEX.is_match(value) {
813            Some(DMSCValidationError::new(
814                "value",
815                "Value must contain only alphanumeric characters",
816                "ALPHANUMERIC",
817            ))
818        } else {
819            None
820        }
821    }
822
823    fn name(&self) -> &'static str {
824        "Alphanumeric"
825    }
826}
827
828struct AlphabeticRule;
829
830impl DMSCValidationRule for AlphabeticRule {
831    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
832        if !ALPHABETIC_REGEX.is_match(value) {
833            Some(DMSCValidationError::new(
834                "value",
835                "Value must contain only alphabetic characters",
836                "ALPHABETIC",
837            ))
838        } else {
839            None
840        }
841    }
842
843    fn name(&self) -> &'static str {
844        "Alphabetic"
845    }
846}
847
848struct NumericRule;
849
850impl DMSCValidationRule for NumericRule {
851    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
852        if !NUMERIC_REGEX.is_match(value) {
853            Some(DMSCValidationError::new(
854                "value",
855                "Value must be a valid number",
856                "NUMERIC",
857            ))
858        } else {
859            None
860        }
861    }
862
863    fn name(&self) -> &'static str {
864        "Numeric"
865    }
866}
867
868struct LowercaseRule;
869
870impl DMSCValidationRule for LowercaseRule {
871    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
872        if !value.chars().all(|c| !c.is_uppercase()) {
873            Some(DMSCValidationError::new(
874                "value",
875                "Value must be lowercase",
876                "LOWERCASE",
877            ))
878        } else {
879            None
880        }
881    }
882
883    fn name(&self) -> &'static str {
884        "Lowercase"
885    }
886}
887
888struct UppercaseRule;
889
890impl DMSCValidationRule for UppercaseRule {
891    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
892        if !value.chars().all(|c| !c.is_lowercase()) {
893            Some(DMSCValidationError::new(
894                "value",
895                "Value must be uppercase",
896                "UPPERCASE",
897            ))
898        } else {
899            None
900        }
901    }
902
903    fn name(&self) -> &'static str {
904        "Uppercase"
905    }
906}
907
908struct ContainsRule(String);
909
910impl DMSCValidationRule for ContainsRule {
911    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
912        if !value.contains(&self.0) {
913            Some(DMSCValidationError::new(
914                "value",
915                &format!("Value must contain '{}'", self.0),
916                "CONTAINS",
917            ))
918        } else {
919            None
920        }
921    }
922
923    fn name(&self) -> &'static str {
924        "Contains"
925    }
926}
927
928struct StartsWithRule(String);
929
930impl DMSCValidationRule for StartsWithRule {
931    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
932        if !value.starts_with(&self.0) {
933            Some(DMSCValidationError::new(
934                "value",
935                &format!("Value must start with '{}'", self.0),
936                "STARTS_WITH",
937            ))
938        } else {
939            None
940        }
941    }
942
943    fn name(&self) -> &'static str {
944        "StartsWith"
945    }
946}
947
948struct EndsWithRule(String);
949
950impl DMSCValidationRule for EndsWithRule {
951    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
952        if !value.ends_with(&self.0) {
953            Some(DMSCValidationError::new(
954                "value",
955                &format!("Value must end with '{}'", self.0),
956                "ENDS_WITH",
957            ))
958        } else {
959            None
960        }
961    }
962
963    fn name(&self) -> &'static str {
964        "EndsWith"
965    }
966}
967
968struct InRule(Vec<String>);
969
970impl DMSCValidationRule for InRule {
971    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
972        if !self.0.contains(&value.to_string()) {
973            Some(DMSCValidationError::new(
974                "value",
975                &format!("Value must be one of: {}", self.0.join(", ")),
976                "IN",
977            ))
978        } else {
979            None
980        }
981    }
982
983    fn name(&self) -> &'static str {
984        "In"
985    }
986}
987
988struct NotInRule(Vec<String>);
989
990impl DMSCValidationRule for NotInRule {
991    fn validate(&self, value: &str) -> Option<DMSCValidationError> {
992        if self.0.contains(&value.to_string()) {
993            Some(DMSCValidationError::new(
994                "value",
995                "Value is not allowed",
996                "NOT_IN",
997            ))
998        } else {
999            None
1000        }
1001    }
1002
1003    fn name(&self) -> &'static str {
1004        "NotIn"
1005    }
1006}
1007
1008#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
1009#[derive(Debug, Clone, Serialize, Deserialize)]
1010pub struct DMSCSanitizationConfig {
1011    pub trim_whitespace: bool,
1012    pub lowercase: bool,
1013    pub uppercase: bool,
1014    pub remove_extra_spaces: bool,
1015    pub remove_html_tags: bool,
1016    pub escape_special_chars: bool,
1017}
1018
1019impl Default for DMSCSanitizationConfig {
1020    fn default() -> Self {
1021        Self {
1022            trim_whitespace: true,
1023            lowercase: false,
1024            uppercase: false,
1025            remove_extra_spaces: false,
1026            remove_html_tags: false,
1027            escape_special_chars: false,
1028        }
1029    }
1030}
1031
1032#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
1033#[derive(Clone)]
1034pub struct DMSCSanitizer {
1035    config: DMSCSanitizationConfig,
1036}
1037
1038impl DMSCSanitizer {
1039    pub fn new() -> Self {
1040        Self {
1041            config: DMSCSanitizationConfig::default(),
1042        }
1043    }
1044
1045    pub fn with_config(config: DMSCSanitizationConfig) -> Self {
1046        Self { config }
1047    }
1048
1049    pub fn sanitize(&self, input: &str) -> String {
1050        let mut result = input.to_string();
1051
1052        if self.config.trim_whitespace {
1053            result = result.trim().to_string();
1054        }
1055
1056        if self.config.lowercase {
1057            result = result.to_lowercase();
1058        }
1059
1060        if self.config.uppercase {
1061            result = result.to_uppercase();
1062        }
1063
1064        if self.config.remove_extra_spaces {
1065            let re = regex::Regex::new(r"\s+").unwrap();
1066            result = re.replace_all(&result, " ").to_string();
1067        }
1068
1069        if self.config.remove_html_tags {
1070            let re = regex::Regex::new(r"<[^>]*>").unwrap();
1071            result = re.replace_all(&result, "").to_string();
1072        }
1073
1074        if self.config.escape_special_chars {
1075            result = html_escape::encode_safe(&result).to_string();
1076        }
1077
1078        result
1079    }
1080
1081    pub fn sanitize_email(&self, input: &str) -> String {
1082        let re = regex::Regex::new(r"[^\w.%+-]").unwrap();
1083        re.replace_all(&self.sanitize(input), "").to_string()
1084    }
1085
1086    pub fn sanitize_filename(&self, input: &str) -> String {
1087        let re = regex::Regex::new(r"[^\w.-]").unwrap();
1088        re.replace_all(&input, "_").to_string()
1089    }
1090}
1091
1092impl Default for DMSCSanitizer {
1093    fn default() -> Self {
1094        Self::new()
1095    }
1096}
1097
1098#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
1099#[derive(Debug, Clone, Serialize, Deserialize)]
1100pub struct DMSCSchemaValidator {
1101    schema: serde_json::Value,
1102}
1103
1104impl DMSCSchemaValidator {
1105    pub fn new(schema: serde_json::Value) -> Self {
1106        Self { schema }
1107    }
1108
1109    pub fn validate(&self, data: &serde_json::Value) -> DMSCValidationResult {
1110        let mut result = DMSCValidationResult::valid();
1111
1112        if let Some(schema_type) = self.schema.get("type") {
1113            match schema_type {
1114                serde_json::Value::String(type_str) => {
1115                    match type_str.as_str() {
1116                        "string" => {
1117                            if !data.is_string() {
1118                                result.add_error(DMSCValidationError::new(
1119                                    "root",
1120                                    &format!("Expected string, got {}", data),
1121                                    "TYPE_MISMATCH",
1122                                ));
1123                            }
1124                        }
1125                        "number" => {
1126                            if !data.is_number() {
1127                                result.add_error(DMSCValidationError::new(
1128                                    "root",
1129                                    &format!("Expected number, got {}", data),
1130                                    "TYPE_MISMATCH",
1131                                ));
1132                            }
1133                        }
1134                        "integer" => {
1135                            if !data.is_i64() {
1136                                result.add_error(DMSCValidationError::new(
1137                                    "root",
1138                                    &format!("Expected integer, got {}", data),
1139                                    "TYPE_MISMATCH",
1140                                ));
1141                            }
1142                        }
1143                        "boolean" => {
1144                            if !data.is_boolean() {
1145                                result.add_error(DMSCValidationError::new(
1146                                    "root",
1147                                    &format!("Expected boolean, got {}", data),
1148                                    "TYPE_MISMATCH",
1149                                ));
1150                            }
1151                        }
1152                        "array" => {
1153                            if !data.is_array() {
1154                                result.add_error(DMSCValidationError::new(
1155                                    "root",
1156                                    &format!("Expected array, got {}", data),
1157                                    "TYPE_MISMATCH",
1158                                ));
1159                            }
1160                        }
1161                        "object" => {
1162                            if !data.is_object() {
1163                                result.add_error(DMSCValidationError::new(
1164                                    "root",
1165                                    &format!("Expected object, got {}", data),
1166                                    "TYPE_MISMATCH",
1167                                ));
1168                            }
1169                        }
1170                        _ => {}
1171                    }
1172                }
1173                _ => {}
1174            }
1175        }
1176
1177        if let Some(required) = self.schema.get("required") {
1178            if let serde_json::Value::Array(req_fields) = required {
1179                if let serde_json::Value::Object(obj) = data {
1180                    for field in req_fields {
1181                        if let serde_json::Value::String(field_name) = field {
1182                            if !obj.contains_key(field_name) {
1183                                result.add_error(DMSCValidationError::new(
1184                                    field_name,
1185                                    "Field is required",
1186                                    "REQUIRED",
1187                                ));
1188                            }
1189                        }
1190                    }
1191                }
1192            }
1193        }
1194
1195        if let Some(min_length) = self.schema.get("minLength") {
1196            if let serde_json::Value::Number(min) = min_length {
1197                if let Some(str_val) = data.as_str() {
1198                    if (str_val.len() as u64) < min.as_u64().unwrap_or(0) {
1199                        result.add_error(DMSCValidationError::new(
1200                            "root",
1201                            &format!("String must be at least {} characters", min),
1202                            "MIN_LENGTH",
1203                        ));
1204                    }
1205                }
1206            }
1207        }
1208
1209        if let Some(max_length) = self.schema.get("maxLength") {
1210            if let serde_json::Value::Number(max) = max_length {
1211                if let Some(str_val) = data.as_str() {
1212                    if (str_val.len() as u64) > max.as_u64().unwrap_or(u64::MAX) {
1213                        result.add_error(DMSCValidationError::new(
1214                            "root",
1215                            &format!("String must be at most {} characters", max),
1216                            "MAX_LENGTH",
1217                        ));
1218                    }
1219                }
1220            }
1221        }
1222
1223        if let Some(pattern) = self.schema.get("pattern") {
1224            if let serde_json::Value::String(pattern_str) = pattern {
1225                if let Ok(regex) = Regex::new(pattern_str) {
1226                    if let Some(str_val) = data.as_str() {
1227                        if !regex.is_match(str_val) {
1228                            result.add_error(DMSCValidationError::new(
1229                                "root",
1230                                "String does not match required pattern",
1231                                "PATTERN",
1232                            ));
1233                        }
1234                    }
1235                }
1236            }
1237        }
1238
1239        if let Some(enum_values) = self.schema.get("enum") {
1240            if let serde_json::Value::Array(enum_array) = enum_values {
1241                let mut found = false;
1242                for enum_val in enum_array {
1243                    if enum_val == data {
1244                        found = true;
1245                        break;
1246                    }
1247                }
1248                if !found {
1249                    result.add_error(DMSCValidationError::new(
1250                        "root",
1251                        "Value must be one of the allowed values",
1252                        "ENUM",
1253                    ));
1254                }
1255            }
1256        }
1257
1258        result
1259    }
1260}
1261
1262#[cfg(feature = "pyo3")]
1263#[pymethods]
1264impl DMSCSchemaValidator {
1265    #[new]
1266    fn py_new(schema: String) -> Self {
1267        let json_value: serde_json::Value = serde_json::from_str(&schema).unwrap_or_default();
1268        Self::new(json_value)
1269    }
1270
1271    fn validate_json(&self, data: String) -> DMSCValidationResult {
1272        let json_value: serde_json::Value = serde_json::from_str(&data).unwrap_or(serde_json::Value::Null);
1273        self.validate(&json_value)
1274    }
1275}
1276
1277#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
1278#[derive(Debug, Clone)]
1279pub struct DMSCValidationModule;
1280
1281#[cfg(feature = "pyo3")]
1282#[pymethods]
1283impl DMSCValidationModule {
1284    #[staticmethod]
1285    fn validate_email(value: String) -> DMSCValidationResult {
1286        DMSCValidatorBuilder::new("email").is_email().max_length(255).build().validate_value(Some(&value))
1287    }
1288
1289    #[staticmethod]
1290    fn validate_username(value: String) -> DMSCValidationResult {
1291        DMSCValidatorBuilder::new("username")
1292            .not_empty()
1293            .min_length(3)
1294            .max_length(32)
1295            .alphanumeric()
1296            .build()
1297            .validate_value(Some(&value))
1298    }
1299
1300    #[staticmethod]
1301    fn validate_password(value: String) -> DMSCValidationResult {
1302        DMSCValidatorBuilder::new("password")
1303            .not_empty()
1304            .min_length(8)
1305            .build()
1306            .validate_value(Some(&value))
1307    }
1308
1309    #[staticmethod]
1310    fn validate_url(value: String) -> DMSCValidationResult {
1311        DMSCValidatorBuilder::new("url").is_url().build().validate_value(Some(&value))
1312    }
1313
1314    #[staticmethod]
1315    fn validate_ip(value: String) -> DMSCValidationResult {
1316        DMSCValidatorBuilder::new("ip").is_ip().build().validate_value(Some(&value))
1317    }
1318
1319    #[staticmethod]
1320    fn validate_not_empty(field_name: String, value: String) -> DMSCValidationResult {
1321        DMSCValidatorBuilder::new(&field_name).not_empty().build().validate_value(Some(&value))
1322    }
1323
1324    #[staticmethod]
1325    fn validate_length(field_name: String, value: String, min: usize, max: usize) -> DMSCValidationResult {
1326        DMSCValidatorBuilder::new(&field_name)
1327            .min_length(min)
1328            .max_length(max)
1329            .build()
1330            .validate_value(Some(&value))
1331    }
1332}