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 RiValidationSeverity {
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 RiValidationError {
80 pub field: String,
81 pub message: String,
82 pub code: String,
83 pub severity: RiValidationSeverity,
84 pub value: Option<serde_json::Value>,
85}
86
87impl RiValidationError {
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: RiValidationSeverity::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: RiValidationSeverity::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: RiValidationSeverity::Warning,
114 value: None,
115 }
116 }
117}
118
119#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct RiValidationResult {
122 pub is_valid: bool,
123 pub errors: Vec<RiValidationError>,
124 pub warnings: Vec<RiValidationError>,
125}
126
127impl RiValidationResult {
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<RiValidationError>) -> Self {
137 let warnings: Vec<RiValidationError> = errors
138 .iter()
139 .filter(|e| e.severity == RiValidationSeverity::Warning)
140 .cloned()
141 .collect();
142
143 let errors: Vec<RiValidationError> = errors
144 .into_iter()
145 .filter(|e| e.severity == RiValidationSeverity::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: RiValidationError) {
156 if error.severity == RiValidationSeverity::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: RiValidationResult) {
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 RiValidationResult {
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<RiValidationError>) -> Self {
214 Self::invalid(errors)
215 }
216
217 fn __str__(&self) -> String {
218 self.to_string()
219 }
220}
221
222#[async_trait]
223pub trait RiValidator: Send + Sync {
224 async fn validate(&self, value: &str) -> RiValidationResult;
225 fn name(&self) -> &'static str;
226}
227
228#[async_trait]
229impl RiValidator for Box<dyn RiValidator> {
230 async fn validate(&self, value: &str) -> RiValidationResult {
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 RiValidationRule: Send + Sync {
240 fn validate(&self, value: &str) -> Option<RiValidationError>;
241 fn name(&self) -> &'static str;
242}
243
244#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
245pub struct RiValidatorBuilder {
246 field_name: String,
247 rules: Vec<Arc<dyn RiValidationRule>>,
248 nullable: bool,
249 optional: bool,
250}
251
252impl RiValidatorBuilder {
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 RiValidationRule + Send + Sync + 'static) -> Self {
365 self.rules.push(Arc::new(rule));
366 self
367 }
368
369 pub fn build(self) -> RiValidationRunner {
370 RiValidationRunner {
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 RiValidatorBuilder {
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 RiValidationRunner {
459 field_name: String,
460 rules: Vec<Arc<dyn RiValidationRule>>,
461 nullable: bool,
462 optional: bool,
463}
464
465impl RiValidationRunner {
466 pub fn new(field_name: &str) -> RiValidatorBuilder {
467 RiValidatorBuilder::new(field_name)
468 }
469
470 pub fn validate_value(&self, value: Option<&str>) -> RiValidationResult {
471 let value = match value {
472 Some(v) => v,
473 None if self.optional => return RiValidationResult::valid(),
474 None if self.nullable => return RiValidationResult::valid(),
475 None => {
476 return RiValidationResult::invalid(vec![RiValidationError::new(
477 &self.field_name,
478 "Value is required",
479 "REQUIRED",
480 )]);
481 }
482 };
483
484 let mut errors = Vec::with_capacity(4);
485
486 for rule in &self.rules {
487 if let Some(error) = rule.validate(value) {
488 errors.push(RiValidationError {
489 field: self.field_name.clone(),
490 ..error
491 });
492 }
493 }
494
495 if errors.is_empty() {
496 RiValidationResult::valid()
497 } else {
498 RiValidationResult::invalid(errors)
499 }
500 }
501}
502
503#[cfg(feature = "pyo3")]
504#[pymethods]
505impl RiValidationRunner {
506 #[new]
507 fn py_new(field_name: String) -> Self {
508 RiValidatorBuilder::new(&field_name).build()
509 }
510
511 fn validate(&self, value: String) -> RiValidationResult {
512 self.validate_value(Some(&value))
513 }
514
515 fn validate_optional(&self, value: Option<String>) -> RiValidationResult {
516 self.validate_value(value.as_deref())
517 }
518}
519
520struct NotEmptyRule;
521
522impl RiValidationRule for NotEmptyRule {
523 fn validate(&self, value: &str) -> Option<RiValidationError> {
524 if value.trim().is_empty() {
525 Some(RiValidationError::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 RiValidationRule for EmailRule {
554 fn validate(&self, value: &str) -> Option<RiValidationError> {
555 if !EMAIL_REGEX.is_match(value) {
556 Some(RiValidationError::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 RiValidationRule for UrlRule {
574 fn validate(&self, value: &str) -> Option<RiValidationError> {
575 if Url::parse(value).is_err() {
576 Some(RiValidationError::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 RiValidationRule for IpAddressRule {
594 fn validate(&self, value: &str) -> Option<RiValidationError> {
595 if !IP_REGEX.is_match(value) {
596 return Some(RiValidationError::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(RiValidationError::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 RiValidationRule for UuidRule {
627 fn validate(&self, value: &str) -> Option<RiValidationError> {
628 if !UUID_REGEX.is_match(value) {
629 Some(RiValidationError::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 RiValidationRule for Base64Rule {
647 fn validate(&self, value: &str) -> Option<RiValidationError> {
648 if !BASE64_REGEX.is_match(value) {
649 Some(RiValidationError::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 RiValidationRule for MinLengthRule {
667 fn validate(&self, value: &str) -> Option<RiValidationError> {
668 if value.len() < self.0 {
669 Some(RiValidationError::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 RiValidationRule for MaxLengthRule {
687 fn validate(&self, value: &str) -> Option<RiValidationError> {
688 if value.len() > self.0 {
689 Some(RiValidationError::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 RiValidationRule for ExactLengthRule {
707 fn validate(&self, value: &str) -> Option<RiValidationError> {
708 if value.len() != self.0 {
709 Some(RiValidationError::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 RiValidationRule for MinValueRule {
727 fn validate(&self, value: &str) -> Option<RiValidationError> {
728 if let Ok(num) = value.parse::<i64>() {
729 if num < self.0 {
730 return Some(RiValidationError::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 RiValidationRule for MaxValueRule {
748 fn validate(&self, value: &str) -> Option<RiValidationError> {
749 if let Ok(num) = value.parse::<i64>() {
750 if num > self.0 {
751 return Some(RiValidationError::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 RiValidationRule for RangeRule {
769 fn validate(&self, value: &str) -> Option<RiValidationError> {
770 if let Ok(num) = value.parse::<i64>() {
771 if num < self.0 || num > self.1 {
772 return Some(RiValidationError::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 RiValidationRule for RegexRule {
790 fn validate(&self, value: &str) -> Option<RiValidationError> {
791 const MAX_REGEX_INPUT_LENGTH: usize = 10 * 1024;
794
795 if value.len() > MAX_REGEX_INPUT_LENGTH {
796 return Some(RiValidationError::new(
797 "value",
798 &format!("Value too long for regex validation (max {} bytes)", MAX_REGEX_INPUT_LENGTH),
799 "REGEX_INPUT_TOO_LONG",
800 ));
801 }
802
803 match Regex::new(&self.0) {
807 Ok(regex) => {
808 if !regex.is_match(value) {
809 return Some(RiValidationError::new(
810 "value",
811 "Value does not match required pattern",
812 "REGEX",
813 ));
814 }
815 }
816 Err(e) => {
817 log::warn!("[Ri.Validation] Invalid regex pattern: {}", e);
819 return Some(RiValidationError::new(
820 "value",
821 "Invalid validation pattern",
822 "REGEX_INVALID",
823 ));
824 }
825 }
826 None
827 }
828
829 fn name(&self) -> &'static str {
830 "Regex"
831 }
832}
833
834struct AlphanumericRule;
835
836impl RiValidationRule for AlphanumericRule {
837 fn validate(&self, value: &str) -> Option<RiValidationError> {
838 if !ALPHANUMERIC_REGEX.is_match(value) {
839 Some(RiValidationError::new(
840 "value",
841 "Value must contain only alphanumeric characters",
842 "ALPHANUMERIC",
843 ))
844 } else {
845 None
846 }
847 }
848
849 fn name(&self) -> &'static str {
850 "Alphanumeric"
851 }
852}
853
854struct AlphabeticRule;
855
856impl RiValidationRule for AlphabeticRule {
857 fn validate(&self, value: &str) -> Option<RiValidationError> {
858 if !ALPHABETIC_REGEX.is_match(value) {
859 Some(RiValidationError::new(
860 "value",
861 "Value must contain only alphabetic characters",
862 "ALPHABETIC",
863 ))
864 } else {
865 None
866 }
867 }
868
869 fn name(&self) -> &'static str {
870 "Alphabetic"
871 }
872}
873
874struct NumericRule;
875
876impl RiValidationRule for NumericRule {
877 fn validate(&self, value: &str) -> Option<RiValidationError> {
878 if !NUMERIC_REGEX.is_match(value) {
879 Some(RiValidationError::new(
880 "value",
881 "Value must be a valid number",
882 "NUMERIC",
883 ))
884 } else {
885 None
886 }
887 }
888
889 fn name(&self) -> &'static str {
890 "Numeric"
891 }
892}
893
894struct LowercaseRule;
895
896impl RiValidationRule for LowercaseRule {
897 fn validate(&self, value: &str) -> Option<RiValidationError> {
898 if !value.chars().all(|c| !c.is_uppercase()) {
899 Some(RiValidationError::new(
900 "value",
901 "Value must be lowercase",
902 "LOWERCASE",
903 ))
904 } else {
905 None
906 }
907 }
908
909 fn name(&self) -> &'static str {
910 "Lowercase"
911 }
912}
913
914struct UppercaseRule;
915
916impl RiValidationRule for UppercaseRule {
917 fn validate(&self, value: &str) -> Option<RiValidationError> {
918 if !value.chars().all(|c| !c.is_lowercase()) {
919 Some(RiValidationError::new(
920 "value",
921 "Value must be uppercase",
922 "UPPERCASE",
923 ))
924 } else {
925 None
926 }
927 }
928
929 fn name(&self) -> &'static str {
930 "Uppercase"
931 }
932}
933
934struct ContainsRule(String);
935
936impl RiValidationRule for ContainsRule {
937 fn validate(&self, value: &str) -> Option<RiValidationError> {
938 if !value.contains(&self.0) {
939 Some(RiValidationError::new(
940 "value",
941 &format!("Value must contain '{}'", self.0),
942 "CONTAINS",
943 ))
944 } else {
945 None
946 }
947 }
948
949 fn name(&self) -> &'static str {
950 "Contains"
951 }
952}
953
954struct StartsWithRule(String);
955
956impl RiValidationRule for StartsWithRule {
957 fn validate(&self, value: &str) -> Option<RiValidationError> {
958 if !value.starts_with(&self.0) {
959 Some(RiValidationError::new(
960 "value",
961 &format!("Value must start with '{}'", self.0),
962 "STARTS_WITH",
963 ))
964 } else {
965 None
966 }
967 }
968
969 fn name(&self) -> &'static str {
970 "StartsWith"
971 }
972}
973
974struct EndsWithRule(String);
975
976impl RiValidationRule for EndsWithRule {
977 fn validate(&self, value: &str) -> Option<RiValidationError> {
978 if !value.ends_with(&self.0) {
979 Some(RiValidationError::new(
980 "value",
981 &format!("Value must end with '{}'", self.0),
982 "ENDS_WITH",
983 ))
984 } else {
985 None
986 }
987 }
988
989 fn name(&self) -> &'static str {
990 "EndsWith"
991 }
992}
993
994struct InRule(Vec<String>);
995
996impl RiValidationRule for InRule {
997 fn validate(&self, value: &str) -> Option<RiValidationError> {
998 if !self.0.contains(&value.to_string()) {
999 Some(RiValidationError::new(
1000 "value",
1001 &format!("Value must be one of: {}", self.0.join(", ")),
1002 "IN",
1003 ))
1004 } else {
1005 None
1006 }
1007 }
1008
1009 fn name(&self) -> &'static str {
1010 "In"
1011 }
1012}
1013
1014struct NotInRule(Vec<String>);
1015
1016impl RiValidationRule for NotInRule {
1017 fn validate(&self, value: &str) -> Option<RiValidationError> {
1018 if self.0.contains(&value.to_string()) {
1019 Some(RiValidationError::new(
1020 "value",
1021 "Value is not allowed",
1022 "NOT_IN",
1023 ))
1024 } else {
1025 None
1026 }
1027 }
1028
1029 fn name(&self) -> &'static str {
1030 "NotIn"
1031 }
1032}
1033
1034#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
1035#[derive(Debug, Clone, Serialize, Deserialize)]
1036pub struct RiSanitizationConfig {
1037 pub trim_whitespace: bool,
1038 pub lowercase: bool,
1039 pub uppercase: bool,
1040 pub remove_extra_spaces: bool,
1041 pub remove_html_tags: bool,
1042 pub escape_special_chars: bool,
1043}
1044
1045impl Default for RiSanitizationConfig {
1046 fn default() -> Self {
1047 Self {
1048 trim_whitespace: true,
1049 lowercase: false,
1050 uppercase: false,
1051 remove_extra_spaces: false,
1052 remove_html_tags: false,
1053 escape_special_chars: false,
1054 }
1055 }
1056}
1057
1058#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
1059#[derive(Clone)]
1060pub struct RiSanitizer {
1061 config: RiSanitizationConfig,
1062}
1063
1064impl RiSanitizer {
1065 pub fn new() -> Self {
1066 Self {
1067 config: RiSanitizationConfig::default(),
1068 }
1069 }
1070
1071 pub fn with_config(config: RiSanitizationConfig) -> Self {
1072 Self { config }
1073 }
1074
1075 pub fn sanitize(&self, input: &str) -> String {
1076 let mut result = input.to_string();
1077
1078 if self.config.trim_whitespace {
1079 result = result.trim().to_string();
1080 }
1081
1082 if self.config.lowercase {
1083 result = result.to_lowercase();
1084 }
1085
1086 if self.config.uppercase {
1087 result = result.to_uppercase();
1088 }
1089
1090 if self.config.remove_extra_spaces {
1091 let re = regex::Regex::new(r"\s+").unwrap();
1092 result = re.replace_all(&result, " ").to_string();
1093 }
1094
1095 if self.config.remove_html_tags {
1096 let re = regex::Regex::new(r"<[^>]*>").unwrap();
1097 result = re.replace_all(&result, "").to_string();
1098 }
1099
1100 if self.config.escape_special_chars {
1101 result = html_escape::encode_safe(&result).to_string();
1102 }
1103
1104 result
1105 }
1106
1107 pub fn sanitize_email(&self, input: &str) -> String {
1108 let re = regex::Regex::new(r"[^\w.%+-]").unwrap();
1109 re.replace_all(&self.sanitize(input), "").to_string()
1110 }
1111
1112 pub fn sanitize_filename(&self, input: &str) -> String {
1113 let re = regex::Regex::new(r"[^\w.-]").unwrap();
1114 re.replace_all(&input, "_").to_string()
1115 }
1116}
1117
1118impl Default for RiSanitizer {
1119 fn default() -> Self {
1120 Self::new()
1121 }
1122}
1123
1124#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
1125#[derive(Debug, Clone, Serialize, Deserialize)]
1126pub struct RiSchemaValidator {
1127 schema: serde_json::Value,
1128}
1129
1130impl RiSchemaValidator {
1131 pub fn new(schema: serde_json::Value) -> Self {
1132 Self { schema }
1133 }
1134
1135 pub fn validate(&self, data: &serde_json::Value) -> RiValidationResult {
1136 let mut result = RiValidationResult::valid();
1137
1138 if let Some(schema_type) = self.schema.get("type") {
1139 match schema_type {
1140 serde_json::Value::String(type_str) => {
1141 match type_str.as_str() {
1142 "string" => {
1143 if !data.is_string() {
1144 result.add_error(RiValidationError::new(
1145 "root",
1146 &format!("Expected string, got {}", data),
1147 "TYPE_MISMATCH",
1148 ));
1149 }
1150 }
1151 "number" => {
1152 if !data.is_number() {
1153 result.add_error(RiValidationError::new(
1154 "root",
1155 &format!("Expected number, got {}", data),
1156 "TYPE_MISMATCH",
1157 ));
1158 }
1159 }
1160 "integer" => {
1161 if !data.is_i64() {
1162 result.add_error(RiValidationError::new(
1163 "root",
1164 &format!("Expected integer, got {}", data),
1165 "TYPE_MISMATCH",
1166 ));
1167 }
1168 }
1169 "boolean" => {
1170 if !data.is_boolean() {
1171 result.add_error(RiValidationError::new(
1172 "root",
1173 &format!("Expected boolean, got {}", data),
1174 "TYPE_MISMATCH",
1175 ));
1176 }
1177 }
1178 "array" => {
1179 if !data.is_array() {
1180 result.add_error(RiValidationError::new(
1181 "root",
1182 &format!("Expected array, got {}", data),
1183 "TYPE_MISMATCH",
1184 ));
1185 }
1186 }
1187 "object" => {
1188 if !data.is_object() {
1189 result.add_error(RiValidationError::new(
1190 "root",
1191 &format!("Expected object, got {}", data),
1192 "TYPE_MISMATCH",
1193 ));
1194 }
1195 }
1196 _ => {}
1197 }
1198 }
1199 _ => {}
1200 }
1201 }
1202
1203 if let Some(required) = self.schema.get("required") {
1204 if let serde_json::Value::Array(req_fields) = required {
1205 if let serde_json::Value::Object(obj) = data {
1206 for field in req_fields {
1207 if let serde_json::Value::String(field_name) = field {
1208 if !obj.contains_key(field_name) {
1209 result.add_error(RiValidationError::new(
1210 field_name,
1211 "Field is required",
1212 "REQUIRED",
1213 ));
1214 }
1215 }
1216 }
1217 }
1218 }
1219 }
1220
1221 if let Some(min_length) = self.schema.get("minLength") {
1222 if let serde_json::Value::Number(min) = min_length {
1223 if let Some(str_val) = data.as_str() {
1224 if (str_val.len() as u64) < min.as_u64().unwrap_or(0) {
1225 result.add_error(RiValidationError::new(
1226 "root",
1227 &format!("String must be at least {} characters", min),
1228 "MIN_LENGTH",
1229 ));
1230 }
1231 }
1232 }
1233 }
1234
1235 if let Some(max_length) = self.schema.get("maxLength") {
1236 if let serde_json::Value::Number(max) = max_length {
1237 if let Some(str_val) = data.as_str() {
1238 if (str_val.len() as u64) > max.as_u64().unwrap_or(u64::MAX) {
1239 result.add_error(RiValidationError::new(
1240 "root",
1241 &format!("String must be at most {} characters", max),
1242 "MAX_LENGTH",
1243 ));
1244 }
1245 }
1246 }
1247 }
1248
1249 if let Some(pattern) = self.schema.get("pattern") {
1250 if let serde_json::Value::String(pattern_str) = pattern {
1251 if let Ok(regex) = Regex::new(pattern_str) {
1252 if let Some(str_val) = data.as_str() {
1253 if !regex.is_match(str_val) {
1254 result.add_error(RiValidationError::new(
1255 "root",
1256 "String does not match required pattern",
1257 "PATTERN",
1258 ));
1259 }
1260 }
1261 }
1262 }
1263 }
1264
1265 if let Some(enum_values) = self.schema.get("enum") {
1266 if let serde_json::Value::Array(enum_array) = enum_values {
1267 let mut found = false;
1268 for enum_val in enum_array {
1269 if enum_val == data {
1270 found = true;
1271 break;
1272 }
1273 }
1274 if !found {
1275 result.add_error(RiValidationError::new(
1276 "root",
1277 "Value must be one of the allowed values",
1278 "ENUM",
1279 ));
1280 }
1281 }
1282 }
1283
1284 result
1285 }
1286}
1287
1288#[cfg(feature = "pyo3")]
1289#[pymethods]
1290impl RiSchemaValidator {
1291 #[new]
1292 fn py_new(schema: String) -> Self {
1293 let json_value: serde_json::Value = serde_json::from_str(&schema).unwrap_or_default();
1294 Self::new(json_value)
1295 }
1296
1297 fn validate_json(&self, data: String) -> RiValidationResult {
1298 let json_value: serde_json::Value = serde_json::from_str(&data).unwrap_or(serde_json::Value::Null);
1299 self.validate(&json_value)
1300 }
1301}
1302
1303#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
1304#[derive(Debug, Clone)]
1305pub struct RiValidationModule;
1306
1307#[cfg(feature = "pyo3")]
1308#[pymethods]
1309impl RiValidationModule {
1310 #[staticmethod]
1311 fn validate_email(value: String) -> RiValidationResult {
1312 RiValidatorBuilder::new("email").is_email().max_length(255).build().validate_value(Some(&value))
1313 }
1314
1315 #[staticmethod]
1316 fn validate_username(value: String) -> RiValidationResult {
1317 RiValidatorBuilder::new("username")
1318 .not_empty()
1319 .min_length(3)
1320 .max_length(32)
1321 .alphanumeric()
1322 .build()
1323 .validate_value(Some(&value))
1324 }
1325
1326 #[staticmethod]
1327 fn validate_password(value: String) -> RiValidationResult {
1328 RiValidatorBuilder::new("password")
1329 .not_empty()
1330 .min_length(8)
1331 .build()
1332 .validate_value(Some(&value))
1333 }
1334
1335 #[staticmethod]
1336 fn validate_url(value: String) -> RiValidationResult {
1337 RiValidatorBuilder::new("url").is_url().build().validate_value(Some(&value))
1338 }
1339
1340 #[staticmethod]
1341 fn validate_ip(value: String) -> RiValidationResult {
1342 RiValidatorBuilder::new("ip").is_ip().build().validate_value(Some(&value))
1343 }
1344
1345 #[staticmethod]
1346 fn validate_not_empty(field_name: String, value: String) -> RiValidationResult {
1347 RiValidatorBuilder::new(&field_name).not_empty().build().validate_value(Some(&value))
1348 }
1349
1350 #[staticmethod]
1351 fn validate_length(field_name: String, value: String, min: usize, max: usize) -> RiValidationResult {
1352 RiValidatorBuilder::new(&field_name)
1353 .min_length(min)
1354 .max_length(max)
1355 .build()
1356 .validate_value(Some(&value))
1357 }
1358}