mx_message/
validation.rs

1// Plasmatic MX Message Parsing Library
2// https://github.com/GoPlasmatic/MXMessage
3//
4// Copyright (c) 2025 Plasmatic
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9//     http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16//
17// You may obtain a copy of this library at
18// https://github.com/GoPlasmatic/MXMessage
19
20use crate::parse_result::{ErrorCollector, ParserConfig};
21
22/// Trait for types that support validation with error collection
23pub trait Validate {
24    /// Validate the instance and collect errors with path information
25    fn validate(&self, path: &str, config: &ParserConfig, collector: &mut ErrorCollector);
26}
27
28/// Helper functions for validation
29pub mod helpers {
30    use crate::error::ValidationError;
31    use crate::parse_result::{ErrorCollector, ParserConfig};
32    use regex::Regex;
33
34    /// Validate string length
35    pub fn validate_length(
36        value: &str,
37        field_name: &str,
38        min: Option<usize>,
39        max: Option<usize>,
40        path: &str,
41        config: &ParserConfig,
42        collector: &mut ErrorCollector,
43    ) -> bool {
44        let mut valid = true;
45
46        if let Some(min_len) = min
47            && value.chars().count() < min_len
48        {
49            let error = ValidationError::new(
50                1001,
51                format!("{field_name} is shorter than the minimum length of {min_len}"),
52            )
53            .with_field(field_name.to_string())
54            .with_path(path.to_string());
55
56            if config.fail_fast {
57                collector.add_critical_error(error);
58                return false;
59            } else {
60                collector.add_error(error);
61                valid = false;
62            }
63        }
64
65        if let Some(max_len) = max
66            && value.chars().count() > max_len
67        {
68            let error = ValidationError::new(
69                1002,
70                format!("{field_name} exceeds the maximum length of {max_len}"),
71            )
72            .with_field(field_name.to_string())
73            .with_path(path.to_string());
74
75            if config.fail_fast {
76                collector.add_critical_error(error);
77                return false;
78            } else {
79                collector.add_error(error);
80                valid = false;
81            }
82        }
83
84        valid
85    }
86
87    /// Validate string pattern
88    pub fn validate_pattern(
89        value: &str,
90        field_name: &str,
91        pattern: &str,
92        path: &str,
93        config: &ParserConfig,
94        collector: &mut ErrorCollector,
95    ) -> bool {
96        // Trim whitespace before validation
97        let trimmed_value = value.trim();
98
99        let regex = match Regex::new(pattern) {
100            Ok(r) => r,
101            Err(_) => {
102                collector.add_critical_error(
103                    ValidationError::new(
104                        9999,
105                        format!("Invalid regex pattern for {field_name}: {pattern}"),
106                    )
107                    .with_field(field_name.to_string())
108                    .with_path(path.to_string()),
109                );
110                return false;
111            }
112        };
113
114        if !regex.is_match(trimmed_value) {
115            let error = ValidationError::new(
116                1005,
117                format!("{field_name} does not match the required pattern (value: '{value}')"),
118            )
119            .with_field(field_name.to_string())
120            .with_path(path.to_string());
121
122            if config.fail_fast {
123                collector.add_critical_error(error);
124                return false;
125            } else {
126                collector.add_error(error);
127                return false;
128            }
129        }
130
131        true
132    }
133
134    /// Validate required field
135    pub fn validate_required<T>(
136        value: &Option<T>,
137        field_name: &str,
138        path: &str,
139        _config: &ParserConfig,
140        collector: &mut ErrorCollector,
141    ) -> bool {
142        if value.is_none() {
143            let error = ValidationError::new(1003, format!("{field_name} is required"))
144                .with_field(field_name.to_string())
145                .with_path(path.to_string());
146
147            collector.add_critical_error(error);
148            return false;
149        }
150        true
151    }
152
153    /// Create a child path for nested validation
154    pub fn child_path(parent: &str, field: &str) -> String {
155        if parent.is_empty() {
156            field.to_string()
157        } else {
158            format!("{parent}.{field}")
159        }
160    }
161}