1#![allow(non_snake_case)]
19
20use 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}