fraiseql_core/validation/
rules.rs1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13#[serde(tag = "type", content = "value")]
14pub enum ValidationRule {
15 #[serde(rename = "required")]
17 Required,
18
19 #[serde(rename = "pattern")]
21 Pattern {
22 pattern: String,
24 message: Option<String>,
26 },
27
28 #[serde(rename = "length")]
30 Length {
31 min: Option<usize>,
33 max: Option<usize>,
35 },
36
37 #[serde(rename = "range")]
39 Range {
40 min: Option<i64>,
42 max: Option<i64>,
44 },
45
46 #[serde(rename = "enum")]
48 Enum {
49 values: Vec<String>,
51 },
52
53 #[serde(rename = "checksum")]
55 Checksum {
56 algorithm: String,
58 },
59
60 #[serde(rename = "cross_field")]
62 CrossField {
63 field: String,
65 operator: String,
67 },
68
69 #[serde(rename = "conditional")]
71 Conditional {
72 condition: String,
74 then_rules: Vec<Box<ValidationRule>>,
76 },
77
78 #[serde(rename = "all")]
80 All(Vec<ValidationRule>),
81
82 #[serde(rename = "any")]
84 Any(Vec<ValidationRule>),
85
86 #[serde(rename = "one_of")]
98 OneOf {
99 fields: Vec<String>,
101 },
102
103 #[serde(rename = "any_of")]
113 AnyOf {
114 fields: Vec<String>,
116 },
117
118 #[serde(rename = "conditional_required")]
131 ConditionalRequired {
132 if_field_present: String,
134 then_required: Vec<String>,
136 },
137
138 #[serde(rename = "required_if_absent")]
151 RequiredIfAbsent {
152 absent_field: String,
154 then_required: Vec<String>,
156 },
157}
158
159impl ValidationRule {
160 pub const fn is_required(&self) -> bool {
162 matches!(self, Self::Required)
163 }
164
165 pub fn description(&self) -> String {
167 match self {
168 Self::Required => "Field is required".to_string(),
169 Self::Pattern { message, .. } => {
170 message.clone().unwrap_or_else(|| "Must match pattern".to_string())
171 },
172 Self::Length { min, max } => match (min, max) {
173 (Some(m), Some(max_val)) => format!("Length between {} and {}", m, max_val),
174 (Some(m), None) => format!("Length at least {}", m),
175 (None, Some(max_val)) => format!("Length at most {}", max_val),
176 (None, None) => "Length constraint".to_string(),
177 },
178 Self::Range { min, max } => match (min, max) {
179 (Some(m), Some(max_val)) => format!("Value between {} and {}", m, max_val),
180 (Some(m), None) => format!("Value at least {}", m),
181 (None, Some(max_val)) => format!("Value at most {}", max_val),
182 (None, None) => "Range constraint".to_string(),
183 },
184 Self::Enum { values } => format!("Must be one of: {}", values.join(", ")),
185 Self::Checksum { algorithm } => format!("Invalid {}", algorithm),
186 Self::CrossField { field, operator } => format!("Must be {} {}", operator, field),
187 Self::Conditional { .. } => "Conditional validation".to_string(),
188 Self::All(_) => "All rules must pass".to_string(),
189 Self::Any(_) => "At least one rule must pass".to_string(),
190 Self::OneOf { fields } => {
191 format!("Exactly one of these must be provided: {}", fields.join(", "))
192 },
193 Self::AnyOf { fields } => {
194 format!("At least one of these must be provided: {}", fields.join(", "))
195 },
196 Self::ConditionalRequired {
197 if_field_present,
198 then_required,
199 } => {
200 format!(
201 "If '{}' is provided, then {} must be provided",
202 if_field_present,
203 then_required.join(", ")
204 )
205 },
206 Self::RequiredIfAbsent {
207 absent_field,
208 then_required,
209 } => {
210 format!(
211 "If '{}' is absent, then {} must be provided",
212 absent_field,
213 then_required.join(", ")
214 )
215 },
216 }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_required_rule() {
226 let rule = ValidationRule::Required;
227 assert!(rule.is_required());
228 }
229
230 #[test]
231 fn test_pattern_rule() {
232 let rule = ValidationRule::Pattern {
233 pattern: "^[a-z]+$".to_string(),
234 message: Some("Only lowercase letters allowed".to_string()),
235 };
236 assert!(!rule.is_required());
237 let desc = rule.description();
238 assert_eq!(desc, "Only lowercase letters allowed");
239 }
240
241 #[test]
242 fn test_length_rule() {
243 let rule = ValidationRule::Length {
244 min: Some(5),
245 max: Some(10),
246 };
247 let desc = rule.description();
248 assert!(desc.contains("5"));
249 assert!(desc.contains("10"));
250 }
251
252 #[test]
253 fn test_rule_serialization() {
254 let rule = ValidationRule::Enum {
255 values: vec!["active".to_string(), "inactive".to_string()],
256 };
257 let json = serde_json::to_string(&rule).expect("serialization failed");
258 let deserialized: ValidationRule =
259 serde_json::from_str(&json).expect("deserialization failed");
260 assert!(matches!(deserialized, ValidationRule::Enum { .. }));
261 }
262
263 #[test]
264 fn test_composite_all_rule() {
265 let rule = ValidationRule::All(vec![
266 ValidationRule::Required,
267 ValidationRule::Pattern {
268 pattern: "^[a-z]+$".to_string(),
269 message: None,
270 },
271 ]);
272 let desc = rule.description();
273 assert!(desc.contains("All rules"));
274 }
275
276 #[test]
277 fn test_one_of_rule() {
278 let rule = ValidationRule::OneOf {
279 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
280 };
281 assert!(!rule.is_required());
282 let desc = rule.description();
283 assert!(desc.contains("Exactly one"));
284 assert!(desc.contains("entityId"));
285 assert!(desc.contains("entityPayload"));
286 }
287
288 #[test]
289 fn test_any_of_rule() {
290 let rule = ValidationRule::AnyOf {
291 fields: vec![
292 "email".to_string(),
293 "phone".to_string(),
294 "address".to_string(),
295 ],
296 };
297 let desc = rule.description();
298 assert!(desc.contains("At least one"));
299 assert!(desc.contains("email"));
300 assert!(desc.contains("phone"));
301 assert!(desc.contains("address"));
302 }
303
304 #[test]
305 fn test_conditional_required_rule() {
306 let rule = ValidationRule::ConditionalRequired {
307 if_field_present: "entityId".to_string(),
308 then_required: vec!["createdAt".to_string(), "updatedAt".to_string()],
309 };
310 let desc = rule.description();
311 assert!(desc.contains("If"));
312 assert!(desc.contains("entityId"));
313 assert!(desc.contains("createdAt"));
314 assert!(desc.contains("updatedAt"));
315 }
316
317 #[test]
318 fn test_required_if_absent_rule() {
319 let rule = ValidationRule::RequiredIfAbsent {
320 absent_field: "addressId".to_string(),
321 then_required: vec!["street".to_string(), "city".to_string(), "zip".to_string()],
322 };
323 let desc = rule.description();
324 assert!(desc.contains("If"));
325 assert!(desc.contains("addressId"));
326 assert!(desc.contains("absent"));
327 assert!(desc.contains("street"));
328 }
329
330 #[test]
331 fn test_one_of_serialization() {
332 let rule = ValidationRule::OneOf {
333 fields: vec!["id".to_string(), "payload".to_string()],
334 };
335 let json = serde_json::to_string(&rule).expect("serialization failed");
336 let deserialized: ValidationRule =
337 serde_json::from_str(&json).expect("deserialization failed");
338 assert!(matches!(deserialized, ValidationRule::OneOf { .. }));
339 }
340
341 #[test]
342 fn test_conditional_required_serialization() {
343 let rule = ValidationRule::ConditionalRequired {
344 if_field_present: "isPremium".to_string(),
345 then_required: vec!["paymentMethod".to_string()],
346 };
347 let json = serde_json::to_string(&rule).expect("serialization failed");
348 let deserialized: ValidationRule =
349 serde_json::from_str(&json).expect("deserialization failed");
350 assert!(matches!(deserialized, ValidationRule::ConditionalRequired { .. }));
351 }
352
353 #[test]
354 fn test_required_if_absent_serialization() {
355 let rule = ValidationRule::RequiredIfAbsent {
356 absent_field: "presetId".to_string(),
357 then_required: vec!["settings".to_string()],
358 };
359 let json = serde_json::to_string(&rule).expect("serialization failed");
360 let deserialized: ValidationRule =
361 serde_json::from_str(&json).expect("deserialization failed");
362 assert!(matches!(deserialized, ValidationRule::RequiredIfAbsent { .. }));
363 }
364}