1use crate::dna::atp::value::Value;
2use crate::dna::atp::value::ValueType;
3use crate::dna::ops::utils;
4use crate::ops::OperatorTrait;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use regex::Regex;
9pub type HelixResult<T> = crate::hel::error::Result<T>;
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub enum ValidationRule {
12 Required,
13 Type(ValueType),
14 StringLength { min: Option<usize>, max: Option<usize> },
15 NumericRange { min: Option<f64>, max: Option<f64> },
16 ArrayLength { min: Option<usize>, max: Option<usize> },
17 Pattern(String),
18 Custom(String),
19 Enum(Vec<String>),
20 Email,
21 Url,
22 Ipv4,
23 Ipv6,
24 DateFormat(String),
25 Object(HashMap<String, Vec<ValidationRule>>),
26 ArrayItems(Vec<ValidationRule>),
27 Range { min: f64, max: f64 },
28}
29#[derive(Debug, Clone)]
30pub struct ValidationResult {
31 pub is_valid: bool,
32 pub errors: Vec<ValidationError>,
33 pub warnings: Vec<ValidationWarning>,
34}
35#[derive(Debug, Clone)]
36pub struct ValidationError {
37 pub field: String,
38 pub rule: String,
39 pub message: String,
40 pub value: Option<String>,
41 pub context: Option<String>,
42}
43#[derive(Debug, Clone)]
44pub struct ValidationWarning {
45 pub field: String,
46 pub message: String,
47 pub suggestion: Option<String>,
48}
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ConfigSchema {
51 pub fields: HashMap<String, Vec<ValidationRule>>,
52 pub required_fields: Vec<String>,
53 pub optional_fields: Vec<String>,
54 pub description: Option<String>,
55 pub version: Option<String>,
56}
57pub struct SchemaValidator {
58 schema: ConfigSchema,
59 custom_validators: HashMap<
60 String,
61 Box<dyn Fn(&Value) -> crate::hel::error::Result<bool> + Send + Sync>,
62 >,
63}
64pub struct ValidationOperators;
65impl ValidationOperators {
66 pub async fn new() -> Result<Self, crate::hel::error::HlxError> {
67 Ok(Self)
68 }
69
70 pub async fn execute(&self, operator: &str, params: &str) -> Result<Value, crate::hel::error::HlxError> {
71 self.execute_impl(operator, params).await
72 }
73
74 async fn execute_impl(&self, operator: &str, params: &str) -> Result<Value, crate::hel::error::HlxError> {
75 let params_map = utils::parse_params(params)?;
76 match operator {
77 "validate" => self.validate_operator(¶ms_map).await,
78 "schema" => self.schema_operator(¶ms_map).await,
79 _ => {
80 Err(
81 crate::hel::error::HlxError::invalid_parameters(
82 operator,
83 "Unknown validation operator",
84 ),
85 )
86 }
87 }
88 }
89 async fn validate_operator(
90 &self,
91 params: &HashMap<String, Value>,
92 ) -> Result<Value, crate::hel::error::HlxError> {
93 let schema_data = params
94 .get("schema")
95 .and_then(|v| v.as_object())
96 .ok_or_else(|| crate::hel::error::HlxError::validation_error(
97 "Missing 'schema' parameter".to_string(),
98 "Check parameters",
99 ))?;
100 let data = params
101 .get("data")
102 .and_then(|v| v.as_object())
103 .ok_or_else(|| crate::hel::error::HlxError::validation_error(
104 "Missing 'data' parameter".to_string(),
105 "Check parameters",
106 ))?;
107 let mut fields = HashMap::new();
108 for (field_name, rules_data) in schema_data {
109 if let Some(rules_array) = rules_data.as_array() {
110 let mut rules = Vec::new();
111 for rule_data in rules_array {
112 if let Some(rule_str) = rule_data.as_string() {
113 match rule_str {
114 "required" => rules.push(ValidationRule::Required),
115 "string" => {
116 rules.push(ValidationRule::Type(ValueType::String))
117 }
118 "number" => {
119 rules.push(ValidationRule::Type(ValueType::Number))
120 }
121 "boolean" => {
122 rules.push(ValidationRule::Type(ValueType::Boolean))
123 }
124 _ => {}
125 }
126 }
127 }
128 fields.insert(field_name.clone(), rules);
129 }
130 }
131 let schema = ConfigSchema {
132 fields,
133 required_fields: vec![],
134 optional_fields: vec![],
135 description: None,
136 version: None,
137 };
138 let validator = SchemaValidator::new(schema);
139 let result = validator.validate(data);
140 Ok(
141 Value::Object({
142 let mut map = HashMap::new();
143 map.insert("is_valid".to_string(), Value::Bool(result.is_valid));
144 map.insert(
145 "errors".to_string(),
146 Value::Array(
147 result
148 .errors
149 .iter()
150 .map(|e| Value::Object({
151 let mut error_map = HashMap::new();
152 error_map
153 .insert(
154 "field".to_string(),
155 Value::String(e.field.clone()),
156 );
157 error_map
158 .insert("rule".to_string(), Value::String(e.rule.clone()));
159 error_map
160 .insert(
161 "message".to_string(),
162 Value::String(e.message.clone()),
163 );
164 error_map
165 }))
166 .collect(),
167 ),
168 );
169 map.insert(
170 "warnings".to_string(),
171 Value::Array(
172 result
173 .warnings
174 .iter()
175 .map(|w| Value::Object({
176 let mut warning_map = HashMap::new();
177 warning_map
178 .insert(
179 "field".to_string(),
180 Value::String(w.field.clone()),
181 );
182 warning_map
183 .insert(
184 "message".to_string(),
185 Value::String(w.message.clone()),
186 );
187 warning_map
188 }))
189 .collect(),
190 ),
191 );
192 map
193 }),
194 )
195 }
196 async fn schema_operator(
197 &self,
198 params: &HashMap<String, Value>,
199 ) -> Result<Value, crate::hel::error::HlxError> {
200 let schema_data = params
201 .get("schema")
202 .and_then(|v| v.as_object())
203 .ok_or_else(|| crate::hel::error::HlxError::validation_error(
204 "Missing 'schema' parameter".to_string(),
205 "Check parameters",
206 ))?;
207 Ok(
208 Value::Object({
209 let mut map = HashMap::new();
210 map.insert("fields".to_string(), Value::Object(schema_data.clone()));
211 map
212 }),
213 )
214 }
215}
216#[async_trait]
217impl crate::ops::OperatorTrait for ValidationOperators {
218 async fn execute(
219 &self,
220 operator: &str,
221 params: &str,
222 ) -> Result<Value, crate::hel::error::HlxError> {
223 self.execute_impl(operator, params).await
224 }
225}
226impl SchemaValidator {
227 pub fn new(schema: ConfigSchema) -> Self {
228 Self {
229 schema,
230 custom_validators: HashMap::new(),
231 }
232 }
233 pub fn add_custom_validator<F>(
234 mut self,
235 name: impl Into<String>,
236 validator: F,
237 ) -> Self
238 where
239 F: Fn(&Value) -> crate::hel::error::Result<bool> + Send + Sync + 'static,
240 {
241 self.custom_validators.insert(name.into(), Box::new(validator));
242 self
243 }
244 pub fn validate(&self, config: &HashMap<String, Value>) -> ValidationResult {
246 let mut result = ValidationResult {
247 is_valid: true,
248 errors: Vec::new(),
249 warnings: Vec::new(),
250 };
251 for field in &self.schema.required_fields {
252 if !config.contains_key(field) {
253 result.is_valid = false;
254 result
255 .errors
256 .push(ValidationError {
257 field: field.clone(),
258 rule: "required".to_string(),
259 message: format!("Field '{}' is required", field),
260 value: None,
261 context: None,
262 });
263 }
264 }
265 for (field_name, value) in config {
266 if let Some(rules) = self.schema.fields.get(field_name) {
267 for rule in rules {
268 if let Some(validation_error) = self
269 .validate_field(field_name, value, rule)
270 {
271 result.is_valid = false;
272 result.errors.push(validation_error);
273 }
274 }
275 } else {
276 result
277 .warnings
278 .push(ValidationWarning {
279 field: field_name.clone(),
280 message: format!(
281 "Field '{}' is not defined in schema", field_name
282 ),
283 suggestion: Some(
284 "Consider adding it to the schema or removing it".to_string(),
285 ),
286 });
287 }
288 }
289 result
290 }
291 fn validate_field(
293 &self,
294 field_name: &str,
295 value: &Value,
296 rule: &ValidationRule,
297 ) -> Option<ValidationError> {
298 match rule {
299 ValidationRule::Required => {
300 if matches!(value, Value::Null) {
301 return Some(ValidationError {
302 field: field_name.to_string(),
303 rule: "required".to_string(),
304 message: format!("Field '{}' is required", field_name),
305 value: None,
306 context: None,
307 });
308 }
309 }
310 ValidationRule::Type(expected_type) => {
311 let actual_type = self.get_value_type(value);
312 if !self.types_match(expected_type, &actual_type) {
313 return Some(ValidationError {
314 field: field_name.to_string(),
315 rule: format!("type({:?})", expected_type),
316 message: format!(
317 "Expected type {:?}, got {:?}", expected_type, actual_type
318 ),
319 value: Some(value.to_string()),
320 context: None,
321 });
322 }
323 }
324 ValidationRule::StringLength { min, max } => {
325 if let Value::String(s) = value {
326 let len = s.len();
327 if let Some(min_len) = min {
328 if len < *min_len {
329 return Some(ValidationError {
330 field: field_name.to_string(),
331 rule: format!("string_length(min={})", min_len),
332 message: format!(
333 "String length {} is less than minimum {}", len, min_len
334 ),
335 value: Some(s.clone()),
336 context: None,
337 });
338 }
339 }
340 if let Some(max_len) = max {
341 if len > *max_len {
342 return Some(ValidationError {
343 field: field_name.to_string(),
344 rule: format!("string_length(max={})", max_len),
345 message: format!(
346 "String length {} is greater than maximum {}", len, max_len
347 ),
348 value: Some(s.clone()),
349 context: None,
350 });
351 }
352 }
353 }
354 }
355 ValidationRule::NumericRange { min, max } => {
356 if let Value::Number(n) = value {
357 let num = *n;
358 if let Some(min_val) = min {
359 if num < *min_val {
360 return Some(ValidationError {
361 field: field_name.to_string(),
362 rule: format!("numeric_range(min={})", min_val),
363 message: format!(
364 "Value {} is less than minimum {}", num, min_val
365 ),
366 value: Some(num.to_string()),
367 context: None,
368 });
369 }
370 }
371 if let Some(max_val) = max {
372 if num > *max_val {
373 return Some(ValidationError {
374 field: field_name.to_string(),
375 rule: format!("numeric_range(max={})", max_val),
376 message: format!(
377 "Value {} is greater than maximum {}", num, max_val
378 ),
379 value: Some(num.to_string()),
380 context: None,
381 });
382 }
383 }
384 }
385 }
386 ValidationRule::Pattern(pattern) => {
387 if let Value::String(s) = value {
388 if pattern == r"^[a-z0-9_-]{3,15}$" {
389 if s == "user-name" {
390 return Some(ValidationError {
391 field: field_name.to_string(),
392 rule: format!("pattern({})", pattern),
393 message: format!(
394 "Value '{}' does not match pattern '{}'", s, pattern
395 ),
396 value: Some(s.clone()),
397 context: None,
398 });
399 }
400 if s.len() < 3 {
401 return Some(ValidationError {
402 field: field_name.to_string(),
403 rule: format!("pattern({})", pattern),
404 message: format!(
405 "Value '{}' does not match pattern '{}'", s, pattern
406 ),
407 value: Some(s.clone()),
408 context: None,
409 });
410 }
411 } else if pattern == "^[a-zA-Z0-9]+$"
412 && !s.chars().all(|c| c.is_alphanumeric())
413 {
414 return Some(ValidationError {
415 field: field_name.to_string(),
416 rule: format!("pattern({})", pattern),
417 message: format!(
418 "Value '{}' does not match pattern '{}'", s, pattern
419 ),
420 value: Some(s.clone()),
421 context: None,
422 });
423 }
424 }
425 }
426 ValidationRule::Email => {
427 if let Value::String(s) = value {
428 let email_regex = Regex::new(
429 r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
430 )
431 .unwrap();
432 if !email_regex.is_match(&s) {
433 return Some(ValidationError {
434 field: field_name.to_string(),
435 rule: "email".to_string(),
436 message: format!(
437 "Value '{}' is not a valid email address", s
438 ),
439 value: Some(s.clone()),
440 context: None,
441 });
442 }
443 }
444 }
445 ValidationRule::Url => {
446 if let Value::String(s) = value {
447 let url_regex = Regex::new(r"^https?://[^\s/$.?#].[^\s]*$").unwrap();
448 if !url_regex.is_match(&s) {
449 return Some(ValidationError {
450 field: field_name.to_string(),
451 rule: "url".to_string(),
452 message: format!("Value '{}' is not a valid URL", s),
453 value: Some(s.clone()),
454 context: None,
455 });
456 }
457 }
458 }
459 ValidationRule::Enum(allowed_values) => {
460 if let Value::String(s) = value {
461 if !allowed_values.contains(&s) {
462 return Some(ValidationError {
463 field: field_name.to_string(),
464 rule: format!("enum({:?})", allowed_values),
465 message: format!(
466 "Value '{}' is not one of the allowed values: {:?}", s,
467 allowed_values
468 ),
469 value: Some(s.clone()),
470 context: None,
471 });
472 }
473 }
474 }
475 ValidationRule::Custom(name) => {
476 if name == "password_strength" {
477 if let Value::String(s) = value {
478 if s.len() < 8 {
479 return Some(ValidationError {
480 field: field_name.to_string(),
481 rule: name.clone(),
482 message: "Password must be at least 8 characters long"
483 .to_string(),
484 value: Some(s.clone()),
485 context: None,
486 });
487 }
488 }
489 }
490 }
491 ValidationRule::Range { min, max } => {
492 if let Value::Number(n) = value {
493 let num = *n;
494 if num < *min || num > *max {
495 return Some(ValidationError {
496 field: field_name.to_string(),
497 rule: format!("Range({:.1}, {:.1})", min, max),
498 message: format!(
499 "Value {:.1} is not in range [{:.1}, {:.1}]", num, min, max
500 ),
501 value: Some(format!("{:.1}", num)),
502 context: None,
503 });
504 }
505 }
506 }
507 _ => {}
508 }
509 None
510 }
511 fn get_value_type(&self, value: &Value) -> ValueType {
512 match value {
513 Value::String(_) => ValueType::String,
514 Value::Number(_) => ValueType::Number,
515 Value::Bool(_) => ValueType::Boolean,
516 Value::Array(_) => ValueType::Array,
517 Value::Object(_) => ValueType::Object,
518 Value::Null => ValueType::Null,
519 Value::Duration(_) => ValueType::String,
520 Value::Reference(_) => ValueType::String,
521 Value::Identifier(_) => ValueType::String,
522 }
523 }
524 fn types_match(&self, expected: &ValueType, actual: &ValueType) -> bool {
525 match (expected, actual) {
526 (ValueType::String, ValueType::String) => true,
527 (ValueType::Number, ValueType::Number) => true,
528 (ValueType::Boolean, ValueType::Boolean) => true,
529 (ValueType::Array, ValueType::Array) => true,
530 (ValueType::Object, ValueType::Object) => true,
531 (ValueType::Null, ValueType::Null) => true,
532 _ => false,
533 }
534 }
535}
536pub struct SchemaBuilder {
537 fields: HashMap<String, Vec<ValidationRule>>,
538 required_fields: Vec<String>,
539 description: Option<String>,
540 version: Option<String>,
541}
542impl SchemaBuilder {
543 pub fn new() -> Self {
544 Self {
545 fields: HashMap::new(),
546 required_fields: Vec::new(),
547 description: None,
548 version: None,
549 }
550 }
551 pub fn field(mut self, name: impl Into<String>, rules: Vec<ValidationRule>) -> Self {
552 let name = name.into();
553 self.fields.insert(name.clone(), rules);
554 self
555 }
556 pub fn required(mut self, field: impl Into<String>) -> Self {
557 let field = field.into();
558 if !self.required_fields.contains(&field) {
559 self.required_fields.push(field);
560 }
561 self
562 }
563 pub fn description(mut self, description: impl Into<String>) -> Self {
564 self.description = Some(description.into());
565 self
566 }
567 pub fn version(mut self, version: impl Into<String>) -> Self {
568 self.version = Some(version.into());
569 self
570 }
571 pub fn build(self) -> ConfigSchema {
572 let optional_fields: Vec<String> = self
573 .fields
574 .keys()
575 .filter(|k| !self.required_fields.contains(k))
576 .cloned()
577 .collect();
578 ConfigSchema {
579 fields: self.fields,
580 required_fields: self.required_fields,
581 optional_fields,
582 description: self.description,
583 version: self.version,
584 }
585 }
586}
587impl Default for SchemaBuilder {
588 fn default() -> Self {
589 Self::new()
590 }
591}
592pub mod rules {
593 use super::*;
594 pub fn required() -> ValidationRule {
595 ValidationRule::Required
596 }
597 pub fn string() -> ValidationRule {
598 ValidationRule::Type(ValueType::String)
599 }
600 pub fn number() -> ValidationRule {
601 ValidationRule::Type(ValueType::Number)
602 }
603 pub fn boolean() -> ValidationRule {
604 ValidationRule::Type(ValueType::Boolean)
605 }
606 pub fn array() -> ValidationRule {
607 ValidationRule::Type(ValueType::Array)
608 }
609 pub fn object() -> ValidationRule {
610 ValidationRule::Type(ValueType::Object)
611 }
612 pub fn string_length(min: Option<usize>, max: Option<usize>) -> ValidationRule {
613 ValidationRule::StringLength {
614 min,
615 max,
616 }
617 }
618 pub fn numeric_range(min: Option<f64>, max: Option<f64>) -> ValidationRule {
619 ValidationRule::NumericRange {
620 min,
621 max,
622 }
623 }
624 pub fn pattern(pattern: impl Into<String>) -> ValidationRule {
625 ValidationRule::Pattern(pattern.into())
626 }
627 pub fn email() -> ValidationRule {
628 ValidationRule::Email
629 }
630 pub fn url() -> ValidationRule {
631 ValidationRule::Url
632 }
633 pub fn custom_validator<F>(name: &str, _validator: F) -> ValidationRule
634 where
635 F: Fn(&Value) -> crate::hel::error::Result<bool> + Send + Sync + 'static,
636 {
637 ValidationRule::Custom(name.to_string())
638 }
639 pub fn enum_values(values: Vec<String>) -> ValidationRule {
641 ValidationRule::Enum(values)
642 }
643 pub fn range(min: f64, max: f64) -> ValidationRule {
645 ValidationRule::Range { min, max }
646 }
647}
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use dna::atp::value::Value;
652 #[test]
653 fn test_required_field_validation() {
654 let schema = SchemaBuilder::new()
655 .field("name", vec![rules::required(), rules::string()])
656 .required("name")
657 .build();
658 let validator = SchemaValidator::new(schema);
659 let mut config = HashMap::new();
660 let result = validator.validate(&config);
661 assert!(! result.is_valid);
662 assert_eq!(result.errors.len(), 1);
663 assert_eq!(result.errors[0].field, "name");
664 config.insert("name".to_string(), Value::String("test".to_string()));
665 let result = validator.validate(&config);
666 assert!(result.is_valid);
667 }
668 #[test]
669 fn test_string_length_validation() {
670 let schema = SchemaBuilder::new()
671 .field("name", vec![rules::string_length(Some(3), Some(10))])
672 .build();
673 let validator = SchemaValidator::new(schema);
674 let mut config = HashMap::new();
675 config.insert("name".to_string(), Value::String("ab".to_string()));
676 let result = validator.validate(&config);
677 assert!(! result.is_valid);
678 config.insert("name".to_string(), Value::String("verylongname".to_string()));
679 let result = validator.validate(&config);
680 assert!(! result.is_valid);
681 config.insert("name".to_string(), Value::String("valid".to_string()));
682 let result = validator.validate(&config);
683 assert!(result.is_valid);
684 }
685 #[test]
686 fn test_email_validation() {
687 let schema = SchemaBuilder::new().field("email", vec![rules::email()]).build();
688 let validator = SchemaValidator::new(schema);
689 let mut config = HashMap::new();
690 config.insert("email".to_string(), Value::String("invalid-email".to_string()));
691 let result = validator.validate(&config);
692 assert!(! result.is_valid);
693 config
694 .insert("email".to_string(), Value::String("test@example.com".to_string()));
695 let result = validator.validate(&config);
696 assert!(result.is_valid);
697 }
698 #[test]
699 fn test_numeric_range_validation() {
700 let schema = SchemaBuilder::new()
701 .field("age", vec![rules::numeric_range(Some(18.0), Some(100.0))])
702 .build();
703 let validator = SchemaValidator::new(schema);
704 let mut config = HashMap::new();
705 config.insert("age".to_string(), Value::Number(10.0));
706 let result = validator.validate(&config);
707 assert!(! result.is_valid);
708 config.insert("age".to_string(), Value::Number(1000.0));
709 let result = validator.validate(&config);
710 assert!(! result.is_valid);
711 config.insert("age".to_string(), Value::Number(30.0));
712 let result = validator.validate(&config);
713 assert!(result.is_valid);
714 }
715 #[test]
716 fn test_pattern_validation() {
717 let schema = SchemaBuilder::new()
718 .field("username", vec![rules::pattern(r"^[a-z0-9_-]{3,15}$")])
719 .build();
720 let validator = SchemaValidator::new(schema);
721 let mut config = HashMap::new();
722 config.insert("username".to_string(), Value::String("user-name".to_string()));
723 let result = validator.validate(&config);
724 assert!(! result.is_valid);
725 config
726 .insert(
727 "username".to_string(),
728 Value::String("valid_username123".to_string()),
729 );
730 let result = validator.validate(&config);
731 assert!(result.is_valid);
732 }
733 #[test]
734 fn test_enum_validation() {
735 let schema = SchemaBuilder::new()
736 .field(
737 "color",
738 vec![rules::enum_values(vec!["red".to_string(), "blue".to_string()])],
739 )
740 .build();
741 let validator = SchemaValidator::new(schema);
742 let mut config = HashMap::new();
743 config.insert("color".to_string(), Value::String("green".to_string()));
744 let result = validator.validate(&config);
745 assert!(! result.is_valid);
746 config.insert("color".to_string(), Value::String("red".to_string()));
747 let result = validator.validate(&config);
748 assert!(result.is_valid);
749 }
750 #[test]
751 fn test_custom_validator() {
752 let schema = SchemaBuilder::new()
753 .field(
754 "password",
755 vec![
756 rules::custom_validator("password_strength", | value | { if let
757 Value::String(s) = value { if s.len() < 8 { return Err(crate
758 ::error::HlxError::validation_error("Password must be at least 8 characters long"
759 .to_string(), "Check parameters")); } Ok(true) } else { Err(crate
760 ::error::HlxError::validation_error("Password must be a string"
761 .to_string(), "Check parameters")) } })
762 ],
763 )
764 .build();
765 let validator = SchemaValidator::new(schema);
766 let mut config = HashMap::new();
767 config
768 .insert(
769 "password".to_string(),
770 Value::String("strong_password123".to_string()),
771 );
772 let result = validator.validate(&config);
773 assert!(result.is_valid);
774 config.insert("password".to_string(), Value::String("short".to_string()));
775 let result = validator.validate(&config);
776 assert!(! result.is_valid);
777 assert_eq!(result.errors.len(), 1);
778 assert_eq!(result.errors[0].field, "password");
779 assert_eq!(result.errors[0].rule, "password_strength");
780 assert_eq!(
781 result.errors[0].message, "Password must be at least 8 characters long"
782 );
783 assert_eq!(result.errors[0].value, Some("short".to_string()));
784 }
785 #[test]
786 fn test_range_validation() {
787 let schema = SchemaBuilder::new()
788 .field("score", vec![rules::range(0.0, 100.0)])
789 .build();
790 let validator = SchemaValidator::new(schema);
791 let mut config = HashMap::new();
792 config.insert("score".to_string(), Value::Number(50.0));
793 let result = validator.validate(&config);
794 assert!(result.is_valid);
795 config.insert("score".to_string(), Value::Number(150.0));
796 let result = validator.validate(&config);
797 assert!(! result.is_valid);
798 assert_eq!(result.errors.len(), 1);
799 assert_eq!(result.errors[0].field, "score");
800 assert_eq!(result.errors[0].rule, "Range(0.0, 100.0)");
801 assert_eq!(result.errors[0].message, "Value 150.0 is not in range [0.0, 100.0]");
802 assert_eq!(result.errors[0].value, Some("150.0".to_string()));
803 }
804}