1pub(crate) mod analysis;
10pub(crate) mod compilation;
11pub(crate) mod defaults;
12pub(crate) mod escaping;
13pub(crate) mod values;
14pub(crate) mod vap;
15
16use crate::validation::{
17 compilation::{CompilationError, CompilationResult, CompiledSchema, compile_schema_validations},
18 values::{json_to_cel_with_compiled, json_to_cel_with_schema},
19};
20use cel::Context;
21
22#[derive(Clone, Debug, Default)]
27pub struct RootContext {
28 pub api_version: String,
30 pub api_group: String,
32 pub kind: String,
34}
35
36#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
38#[non_exhaustive]
39pub enum ErrorKind {
40 CompilationFailure,
42 InvalidRule,
44 ValidationFailure,
46 InvalidResult,
48 EvaluationError,
50 UnsupportedReference,
58 SchemaTooDeep,
62}
63
64#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
70#[non_exhaustive]
71pub struct ValidationError {
72 pub rule: String,
74 pub message: String,
76 pub field_path: String,
78 pub reason: Option<String>,
80 pub kind: ErrorKind,
82 #[serde(skip)]
95 source: Option<std::sync::Arc<dyn std::error::Error + Send + Sync>>,
96}
97
98impl std::fmt::Display for ValidationError {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 if self.field_path.is_empty() {
101 write!(f, "{}", self.message)
102 } else {
103 write!(f, "{}: {}", self.field_path, self.message)
104 }
105 }
106}
107
108impl PartialEq for ValidationError {
111 fn eq(&self, other: &Self) -> bool {
112 self.rule == other.rule
113 && self.message == other.message
114 && self.field_path == other.field_path
115 && self.reason == other.reason
116 && self.kind == other.kind
117 }
118}
119
120impl Eq for ValidationError {}
121
122impl std::error::Error for ValidationError {
123 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
124 self.source
125 .as_ref()
126 .map(|s| &**s as &(dyn std::error::Error + 'static))
127 }
128}
129
130#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
141pub struct ValidationErrors(Vec<ValidationError>);
142
143impl ValidationErrors {
144 #[must_use]
146 pub fn into_vec(self) -> Vec<ValidationError> {
147 self.0
148 }
149
150 #[must_use]
152 pub fn as_slice(&self) -> &[ValidationError] {
153 &self.0
154 }
155
156 #[must_use]
160 pub fn is_empty(&self) -> bool {
161 self.0.is_empty()
162 }
163
164 #[must_use]
166 pub fn len(&self) -> usize {
167 self.0.len()
168 }
169}
170
171impl std::fmt::Display for ValidationErrors {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 writeln!(f, "{} validation error(s):", self.0.len())?;
174 for (i, err) in self.0.iter().enumerate() {
175 if i + 1 == self.0.len() {
176 write!(f, " - {err}")?;
177 } else {
178 writeln!(f, " - {err}")?;
179 }
180 }
181 Ok(())
182 }
183}
184
185impl std::error::Error for ValidationErrors {}
189
190impl std::ops::Deref for ValidationErrors {
191 type Target = [ValidationError];
192
193 fn deref(&self) -> &Self::Target {
194 &self.0
195 }
196}
197
198impl From<Vec<ValidationError>> for ValidationErrors {
199 fn from(errors: Vec<ValidationError>) -> Self {
200 Self(errors)
201 }
202}
203
204impl IntoIterator for ValidationErrors {
205 type IntoIter = std::vec::IntoIter<ValidationError>;
206 type Item = ValidationError;
207
208 fn into_iter(self) -> Self::IntoIter {
209 self.0.into_iter()
210 }
211}
212
213impl<'a> IntoIterator for &'a ValidationErrors {
214 type IntoIter = std::slice::Iter<'a, ValidationError>;
215 type Item = &'a ValidationError;
216
217 fn into_iter(self) -> Self::IntoIter {
218 self.0.iter()
219 }
220}
221
222fn into_result(errors: Vec<ValidationError>) -> Result<(), ValidationErrors> {
225 if errors.is_empty() {
226 Ok(())
227 } else {
228 Err(ValidationErrors(errors))
229 }
230}
231
232fn schema_too_deep_error(path: &str) -> ValidationError {
237 ValidationError {
238 rule: String::new(),
239 message: format!(
240 "schema nesting exceeds the maximum depth of {}",
241 crate::validation::compilation::MAX_SCHEMA_DEPTH
242 ),
243 field_path: path.to_string(),
244 reason: None,
245 kind: ErrorKind::SchemaTooDeep,
246 source: None,
247 }
248}
249
250pub struct Validator {
262 base_ctx: Context<'static>,
263}
264
265impl std::fmt::Debug for Validator {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 f.debug_struct("Validator").finish()
268 }
269}
270
271impl Validator {
272 pub fn new() -> Self {
274 let mut ctx = Context::default();
275 crate::register_all(&mut ctx);
276 Self { base_ctx: ctx }
277 }
278
279 pub fn validate(
289 &self,
290 schema: &serde_json::Value,
291 object: &serde_json::Value,
292 old_object: Option<&serde_json::Value>,
293 ) -> Result<(), ValidationErrors> {
294 self.validate_with_context(schema, object, old_object, None)
295 }
296
297 pub fn validate_with_context(
305 &self,
306 schema: &serde_json::Value,
307 object: &serde_json::Value,
308 old_object: Option<&serde_json::Value>,
309 root_ctx: Option<&RootContext>,
310 ) -> Result<(), ValidationErrors> {
311 let defaulted = crate::validation::defaults::apply_defaults(schema, object);
312 let defaulted_old = old_object.map(|o| crate::validation::defaults::apply_defaults(schema, o));
313 let mut errors = Vec::new();
314 self.walk_schema(
315 schema,
316 &defaulted,
317 defaulted_old.as_ref(),
318 String::new(),
319 &mut errors,
320 &self.base_ctx,
321 root_ctx,
322 0,
323 );
324 into_result(errors)
325 }
326
327 pub fn validate_compiled(
335 &self,
336 compiled: &CompiledSchema,
337 object: &serde_json::Value,
338 old_object: Option<&serde_json::Value>,
339 ) -> Result<(), ValidationErrors> {
340 self.validate_compiled_with_context(compiled, object, old_object, None)
341 }
342
343 pub fn validate_compiled_with_context(
348 &self,
349 compiled: &CompiledSchema,
350 object: &serde_json::Value,
351 old_object: Option<&serde_json::Value>,
352 root_ctx: Option<&RootContext>,
353 ) -> Result<(), ValidationErrors> {
354 let defaulted = crate::validation::defaults::apply_defaults_compiled(compiled, object);
355 let defaulted_old =
356 old_object.map(|o| crate::validation::defaults::apply_defaults_compiled(compiled, o));
357 let mut errors = Vec::new();
358 self.walk_compiled(
359 compiled,
360 &defaulted,
361 defaulted_old.as_ref(),
362 String::new(),
363 &mut errors,
364 &self.base_ctx,
365 root_ctx,
366 0,
367 );
368 into_result(errors)
369 }
370
371 pub fn validate_with_defaults(
377 &self,
378 schema: &serde_json::Value,
379 object: &serde_json::Value,
380 old_object: Option<&serde_json::Value>,
381 ) -> Result<(), ValidationErrors> {
382 self.validate(schema, object, old_object)
383 }
384
385 pub fn validate_with_defaults_and_context(
391 &self,
392 schema: &serde_json::Value,
393 object: &serde_json::Value,
394 old_object: Option<&serde_json::Value>,
395 root_ctx: Option<&RootContext>,
396 ) -> Result<(), ValidationErrors> {
397 self.validate_with_context(schema, object, old_object, root_ctx)
398 }
399
400 #[allow(clippy::too_many_arguments)]
403 fn walk_schema(
404 &self,
405 schema: &serde_json::Value,
406 value: &serde_json::Value,
407 old_value: Option<&serde_json::Value>,
408 path: String,
409 errors: &mut Vec<ValidationError>,
410 base_ctx: &Context<'_>,
411 root_ctx: Option<&RootContext>,
412 depth: usize,
413 ) {
414 if depth > crate::validation::compilation::MAX_SCHEMA_DEPTH {
415 errors.push(schema_too_deep_error(&path));
416 return;
417 }
418
419 let cel_value = json_to_cel_with_schema(value, schema);
420 let cel_old = old_value.map(|o| json_to_cel_with_schema(o, schema));
421 self.evaluate_validations(
422 schema,
423 &cel_value,
424 cel_old.as_ref(),
425 &path,
426 errors,
427 base_ctx,
428 root_ctx,
429 );
430
431 if let (Some(properties), Some(obj)) = (
432 schema.get("properties").and_then(|p| p.as_object()),
433 value.as_object(),
434 ) {
435 for (prop_name, prop_schema) in properties {
436 if let Some(child_value) = obj.get(prop_name) {
437 let child_old = old_value.and_then(|o| o.get(prop_name));
438 let child_path = join_path(&path, prop_name);
439 self.walk_schema(
440 prop_schema,
441 child_value,
442 child_old,
443 child_path,
444 errors,
445 base_ctx,
446 None,
447 depth + 1,
448 );
449 }
450 }
451 }
452
453 if let (Some(items_schema), Some(arr)) = (schema.get("items"), value.as_array()) {
454 for (i, item) in arr.iter().enumerate() {
455 let old_item = old_value.and_then(|o| o.as_array()).and_then(|a| a.get(i));
456 let item_path = join_path_index(&path, i);
457 self.walk_schema(
458 items_schema,
459 item,
460 old_item,
461 item_path,
462 errors,
463 base_ctx,
464 None,
465 depth + 1,
466 );
467 }
468 }
469
470 let preserve_unknown = schema
471 .get("x-kubernetes-preserve-unknown-fields")
472 .and_then(|v| v.as_bool())
473 == Some(true);
474
475 if !preserve_unknown
476 && let (Some(additional_schema), Some(obj)) = (
477 schema.get("additionalProperties").filter(|a| a.is_object()),
478 value.as_object(),
479 )
480 {
481 let known: std::collections::HashSet<&str> = schema
482 .get("properties")
483 .and_then(|p| p.as_object())
484 .map(|p| p.keys().map(|k| k.as_str()).collect())
485 .unwrap_or_default();
486
487 for (key, val) in obj {
488 if known.contains(key.as_str()) {
489 continue;
490 }
491 let old_val = old_value.and_then(|o| o.get(key));
492 let child_path = join_path(&path, key);
493 self.walk_schema(
494 additional_schema,
495 val,
496 old_val,
497 child_path,
498 errors,
499 base_ctx,
500 None,
501 depth + 1,
502 );
503 }
504 }
505
506 for keyword in &["allOf", "oneOf", "anyOf"] {
508 if let Some(branches) = schema.get(keyword).and_then(|v| v.as_array()) {
509 for branch in branches {
510 self.walk_schema(
511 branch,
512 value,
513 old_value,
514 path.clone(),
515 errors,
516 base_ctx,
517 root_ctx,
518 depth + 1,
519 );
520 }
521 }
522 }
523 }
524
525 #[allow(clippy::too_many_arguments)]
526 fn evaluate_validations(
527 &self,
528 schema: &serde_json::Value,
529 cel_value: &cel::Value,
530 cel_old: Option<&cel::Value>,
531 path: &str,
532 errors: &mut Vec<ValidationError>,
533 base_ctx: &Context<'_>,
534 root_ctx: Option<&RootContext>,
535 ) {
536 let compiled = compile_schema_validations(schema);
537 self.evaluate_compiled_results(&compiled, cel_value, cel_old, path, errors, base_ctx, root_ctx);
538 }
539
540 #[allow(clippy::too_many_arguments)]
543 fn walk_compiled(
544 &self,
545 compiled: &CompiledSchema,
546 value: &serde_json::Value,
547 old_value: Option<&serde_json::Value>,
548 path: String,
549 errors: &mut Vec<ValidationError>,
550 base_ctx: &Context<'_>,
551 root_ctx: Option<&RootContext>,
552 depth: usize,
553 ) {
554 if depth > crate::validation::compilation::MAX_SCHEMA_DEPTH {
555 errors.push(schema_too_deep_error(&path));
556 return;
557 }
558
559 let cel_value = json_to_cel_with_compiled(value, compiled);
560 let cel_old = old_value.map(|o| json_to_cel_with_compiled(o, compiled));
561 self.evaluate_compiled_results(
562 &compiled.validations,
563 &cel_value,
564 cel_old.as_ref(),
565 &path,
566 errors,
567 base_ctx,
568 root_ctx,
569 );
570
571 if let Some(obj) = value.as_object() {
572 for (prop_name, child_compiled) in &compiled.properties {
573 if let Some(child_value) = obj.get(prop_name) {
574 let child_old = old_value.and_then(|o| o.get(prop_name));
575 let child_path = join_path(&path, prop_name);
576 self.walk_compiled(
577 child_compiled,
578 child_value,
579 child_old,
580 child_path,
581 errors,
582 base_ctx,
583 None,
584 depth + 1,
585 );
586 }
587 }
588 }
589
590 if let (Some(items_compiled), Some(arr)) = (&compiled.items, value.as_array()) {
591 for (i, item) in arr.iter().enumerate() {
592 let old_item = old_value.and_then(|o| o.as_array()).and_then(|a| a.get(i));
593 let item_path = join_path_index(&path, i);
594 self.walk_compiled(
595 items_compiled,
596 item,
597 old_item,
598 item_path,
599 errors,
600 base_ctx,
601 None,
602 depth + 1,
603 );
604 }
605 }
606
607 if !compiled.preserve_unknown_fields
608 && let (Some(additional_compiled), Some(obj)) =
609 (&compiled.additional_properties, value.as_object())
610 {
611 for (key, val) in obj {
612 if compiled.properties.contains_key(key) {
613 continue;
614 }
615 let old_val = old_value.and_then(|o| o.get(key));
616 let child_path = join_path(&path, key);
617 self.walk_compiled(
618 additional_compiled,
619 val,
620 old_val,
621 child_path,
622 errors,
623 base_ctx,
624 None,
625 depth + 1,
626 );
627 }
628 }
629
630 for branch in compiled
631 .all_of
632 .iter()
633 .chain(compiled.one_of.iter())
634 .chain(compiled.any_of.iter())
635 {
636 self.walk_compiled(
637 branch,
638 value,
639 old_value,
640 path.clone(),
641 errors,
642 base_ctx,
643 root_ctx,
644 depth + 1,
645 );
646 }
647 }
648
649 #[allow(clippy::too_many_arguments)]
652 fn evaluate_compiled_results(
653 &self,
654 results: &[Result<CompilationResult, CompilationError>],
655 cel_value: &cel::Value,
656 cel_old: Option<&cel::Value>,
657 path: &str,
658 errors: &mut Vec<ValidationError>,
659 base_ctx: &Context<'_>,
660 root_ctx: Option<&RootContext>,
661 ) {
662 let mut node_ctx = base_ctx.new_inner_scope();
664 node_ctx.add_variable_from_value("self", cel_value.clone());
665 if let Some(old) = cel_old {
666 node_ctx.add_variable_from_value("oldSelf", old.clone());
667 }
668
669 if path.is_empty()
670 && let Some(rc) = root_ctx
671 {
672 node_ctx.add_variable_from_value(
673 "apiVersion",
674 cel::Value::String(std::sync::Arc::new(rc.api_version.clone())),
675 );
676 node_ctx.add_variable_from_value(
677 "apiGroup",
678 cel::Value::String(std::sync::Arc::new(rc.api_group.clone())),
679 );
680 node_ctx
681 .add_variable_from_value("kind", cel::Value::String(std::sync::Arc::new(rc.kind.clone())));
682 }
683
684 for result in results {
685 match result {
686 Ok(cr) => {
687 self.evaluate_rule(cr, &node_ctx, cel_old, path, errors);
688 }
689 Err(CompilationError::Parse { rule, source }) => {
690 errors.push(ValidationError {
691 rule: rule.clone(),
692 message: format!("failed to compile rule \"{rule}\": {source}"),
693 field_path: path.to_string(),
694 reason: None,
695 kind: ErrorKind::CompilationFailure,
696 source: None,
699 });
700 }
701 Err(CompilationError::MessageExpressionParse {
702 rule,
703 message_expression,
704 source,
705 }) => {
706 errors.push(ValidationError {
707 rule: rule.clone(),
708 message: format!(
709 "failed to compile messageExpression \"{message_expression}\" for rule \"{rule}\": {source}"
710 ),
711 field_path: path.to_string(),
712 reason: None,
713 kind: ErrorKind::CompilationFailure,
714 source: None,
717 });
718 }
719 Err(CompilationError::InvalidRule(e)) => {
720 errors.push(ValidationError {
721 rule: String::new(),
722 message: format!("invalid rule definition: {e}"),
723 field_path: path.to_string(),
724 reason: None,
725 kind: ErrorKind::InvalidRule,
726 source: None,
729 });
730 }
731 Err(CompilationError::SchemaTooDeep { .. }) => {
732 errors.push(schema_too_deep_error(path));
733 }
734 }
735 }
736 }
737
738 fn evaluate_rule(
739 &self,
740 cr: &CompilationResult,
741 node_ctx: &Context<'_>,
742 cel_old: Option<&cel::Value>,
743 path: &str,
744 errors: &mut Vec<ValidationError>,
745 ) {
746 if cr.is_transition_rule && cel_old.is_none() && cr.rule.optional_old_self != Some(true) {
748 return; }
750
751 let use_null_old_self = cel_old.is_none() && cr.rule.optional_old_self == Some(true);
753 let null_scope;
754 let effective_ctx: &Context<'_> = if use_null_old_self {
755 null_scope = {
756 let mut s = node_ctx.new_inner_scope();
757 s.add_variable_from_value("oldSelf", cel::Value::Null);
758 s
759 };
760 &null_scope
761 } else {
762 node_ctx
763 };
764
765 let result = cr.program.execute(effective_ctx);
766 let error_path = effective_path(path, cr.rule.field_path.as_deref());
767
768 match result {
769 Ok(cel::Value::Bool(true)) => {
770 }
772 Ok(cel::Value::Bool(false)) => {
773 let message = self.resolve_message(cr, effective_ctx);
774 errors.push(ValidationError {
775 rule: cr.rule.rule.clone(),
776 message,
777 field_path: error_path,
778 reason: cr.rule.reason.clone(),
779 kind: ErrorKind::ValidationFailure,
780 source: None,
781 });
782 }
783 Ok(_) => {
784 errors.push(ValidationError {
785 rule: cr.rule.rule.clone(),
786 message: format!("rule \"{}\" did not evaluate to bool", cr.rule.rule),
787 field_path: error_path,
788 reason: None,
789 kind: ErrorKind::InvalidResult,
790 source: None,
791 });
792 }
793 Err(e) => {
794 let (kind, message) = match &e {
802 cel::ExecutionError::UndeclaredReference(name) => (
803 ErrorKind::UnsupportedReference,
804 format!(
805 "rule references '{name}', which this kube-cel build does not support \
806 (an unsupported CEL macro, or a feature disabled at compile time); \
807 it cannot be evaluated client-side"
808 ),
809 ),
810 _ => (ErrorKind::EvaluationError, format!("rule evaluation error: {e}")),
811 };
812 errors.push(ValidationError {
813 rule: cr.rule.rule.clone(),
814 message,
815 field_path: error_path,
816 reason: None,
817 kind,
818 source: Some(std::sync::Arc::new(e)),
819 });
820 }
821 }
822 }
823
824 fn resolve_message(&self, cr: &CompilationResult, ctx: &Context<'_>) -> String {
827 if let Some(ref msg_prog) = cr.message_program
828 && let Ok(cel::Value::String(s)) = msg_prog.execute(ctx)
829 {
830 return (*s).clone();
831 }
832 cr.rule
833 .message
834 .clone()
835 .unwrap_or_else(|| format!("failed rule: {}", cr.rule.rule))
836 }
837}
838
839impl Default for Validator {
840 fn default() -> Self {
841 Self::new()
842 }
843}
844
845thread_local! {
846 static THREAD_VALIDATOR: Validator = Validator::new();
847}
848
849pub fn validate(
855 schema: &serde_json::Value,
856 object: &serde_json::Value,
857 old_object: Option<&serde_json::Value>,
858) -> Result<(), ValidationErrors> {
859 THREAD_VALIDATOR.with(|v| v.validate(schema, object, old_object))
860}
861
862pub fn validate_compiled(
868 compiled: &CompiledSchema,
869 object: &serde_json::Value,
870 old_object: Option<&serde_json::Value>,
871) -> Result<(), ValidationErrors> {
872 THREAD_VALIDATOR.with(|v| v.validate_compiled(compiled, object, old_object))
873}
874
875#[inline]
878fn effective_path(base_path: &str, rule_field_path: Option<&str>) -> String {
879 match rule_field_path {
880 Some(fp) if fp.starts_with('.') => format!("{base_path}{fp}"),
881 Some(fp) if !base_path.is_empty() => format!("{base_path}.{fp}"),
882 Some(fp) => fp.to_string(),
883 None => base_path.to_string(),
884 }
885}
886
887#[inline]
888fn join_path(base: &str, segment: &str) -> String {
889 if base.is_empty() {
890 segment.to_string()
891 } else {
892 format!("{base}.{segment}")
893 }
894}
895
896#[inline]
897fn join_path_index(base: &str, index: usize) -> String {
898 if base.is_empty() {
899 format!("[{index}]")
900 } else {
901 format!("{base}[{index}]")
902 }
903}
904
905#[cfg(test)]
906mod tests {
907 use super::*;
908 use crate::validation::compilation::compile_schema;
909 use serde_json::json;
910
911 fn make_schema(validations: serde_json::Value) -> serde_json::Value {
912 json!({
913 "type": "object",
914 "properties": {
915 "replicas": {"type": "integer"},
916 "name": {"type": "string"}
917 },
918 "x-kubernetes-validations": validations
919 })
920 }
921
922 #[test]
923 fn validation_passes() {
924 let schema = make_schema(json!([
925 {"rule": "self.replicas >= 0", "message": "must be non-negative"}
926 ]));
927 let obj = json!({"replicas": 3, "name": "app"});
928 assert!(validate(&schema, &obj, None).is_ok());
929 }
930
931 #[test]
932 fn validation_fails() {
933 let schema = make_schema(json!([
934 {"rule": "self.replicas >= 0", "message": "must be non-negative"}
935 ]));
936 let obj = json!({"replicas": -1, "name": "app"});
937 let errors = validate(&schema, &obj, None).unwrap_err();
938 assert_eq!(errors.len(), 1);
939 assert_eq!(errors[0].message, "must be non-negative");
940 assert_eq!(errors[0].rule, "self.replicas >= 0");
941 }
942
943 #[test]
944 fn default_message_when_none() {
945 let schema = make_schema(json!([
946 {"rule": "self.replicas >= 0"}
947 ]));
948 let obj = json!({"replicas": -1, "name": "app"});
949 let errors = validate(&schema, &obj, None).unwrap_err();
950 assert_eq!(errors.len(), 1);
951 assert!(errors[0].message.contains("self.replicas >= 0"));
952 }
953
954 #[test]
955 fn reason_preserved() {
956 let schema = make_schema(json!([
957 {"rule": "self.replicas >= 0", "message": "bad", "reason": "FieldValueInvalid"}
958 ]));
959 let obj = json!({"replicas": -1, "name": "app"});
960 let errors = validate(&schema, &obj, None).unwrap_err();
961 assert_eq!(errors[0].reason.as_deref(), Some("FieldValueInvalid"));
962 }
963
964 #[test]
965 fn transition_rule_skipped_without_old_object() {
966 let schema = make_schema(json!([
967 {"rule": "self.replicas >= oldSelf.replicas", "message": "cannot scale down"}
968 ]));
969 let obj = json!({"replicas": 1, "name": "app"});
970 assert!(validate(&schema, &obj, None).is_ok());
971 }
972
973 #[test]
974 fn transition_rule_evaluated_with_old_object() {
975 let schema = make_schema(json!([
976 {"rule": "self.replicas >= oldSelf.replicas", "message": "cannot scale down"}
977 ]));
978 let obj = json!({"replicas": 1, "name": "app"});
979 let old = json!({"replicas": 3, "name": "app"});
980 let errors = validate(&schema, &obj, Some(&old)).unwrap_err();
981 assert_eq!(errors.len(), 1);
982 assert_eq!(errors[0].message, "cannot scale down");
983 }
984
985 #[test]
986 fn transition_rule_passes() {
987 let schema = make_schema(json!([
988 {"rule": "self.replicas >= oldSelf.replicas", "message": "cannot scale down"}
989 ]));
990 let obj = json!({"replicas": 5, "name": "app"});
991 let old = json!({"replicas": 3, "name": "app"});
992 assert!(validate(&schema, &obj, Some(&old)).is_ok());
993 }
994
995 #[test]
996 fn nested_property_field_path() {
997 let schema = json!({
998 "type": "object",
999 "properties": {
1000 "spec": {
1001 "type": "object",
1002 "properties": {
1003 "replicas": {
1004 "type": "integer",
1005 "x-kubernetes-validations": [
1006 {"rule": "self >= 0", "message": "must be non-negative"}
1007 ]
1008 }
1009 }
1010 }
1011 }
1012 });
1013 let obj = json!({"spec": {"replicas": -1}});
1014 let errors = validate(&schema, &obj, None).unwrap_err();
1015 assert_eq!(errors.len(), 1);
1016 assert_eq!(errors[0].field_path, "spec.replicas");
1017 assert_eq!(errors[0].message, "must be non-negative");
1018 }
1019
1020 #[test]
1021 fn array_items_validation() {
1022 let schema = json!({
1023 "type": "object",
1024 "properties": {
1025 "items": {
1026 "type": "array",
1027 "items": {
1028 "type": "object",
1029 "properties": {
1030 "name": {"type": "string"}
1031 },
1032 "x-kubernetes-validations": [
1033 {"rule": "self.name.size() > 0", "message": "name required"}
1034 ]
1035 }
1036 }
1037 }
1038 });
1039 let obj = json!({
1040 "items": [
1041 {"name": "good"},
1042 {"name": ""},
1043 {"name": "also-good"}
1044 ]
1045 });
1046 let errors = validate(&schema, &obj, None).unwrap_err();
1047 assert_eq!(errors.len(), 1);
1048 assert_eq!(errors[0].field_path, "items[1]");
1049 assert_eq!(errors[0].message, "name required");
1050 }
1051
1052 #[test]
1053 fn missing_field_not_validated() {
1054 let schema = json!({
1055 "type": "object",
1056 "properties": {
1057 "optional_field": {
1058 "type": "integer",
1059 "x-kubernetes-validations": [
1060 {"rule": "self >= 0", "message": "must be non-negative"}
1061 ]
1062 }
1063 }
1064 });
1065 let obj = json!({});
1066 assert!(validate(&schema, &obj, None).is_ok());
1067 }
1068
1069 #[test]
1070 fn multiple_rules_partial_failure() {
1071 let schema = make_schema(json!([
1072 {"rule": "self.replicas >= 0", "message": "non-negative"},
1073 {"rule": "self.name.size() > 0", "message": "name required"}
1074 ]));
1075 let obj = json!({"replicas": -1, "name": ""});
1076 let errors = validate(&schema, &obj, None).unwrap_err();
1077 assert_eq!(errors.len(), 2);
1078 }
1079
1080 #[test]
1081 fn compilation_error_reported() {
1082 let schema = make_schema(json!([
1083 {"rule": "self.replicas >="}
1084 ]));
1085 let obj = json!({"replicas": 1, "name": "app"});
1086 let errors = validate(&schema, &obj, None).unwrap_err();
1087 assert_eq!(errors.len(), 1);
1088 assert!(errors[0].message.contains("failed to compile"));
1089 }
1090
1091 #[test]
1092 fn no_validations_no_errors() {
1093 let schema = json!({
1094 "type": "object",
1095 "properties": {
1096 "replicas": {"type": "integer"}
1097 }
1098 });
1099 let obj = json!({"replicas": -1});
1100 assert!(validate(&schema, &obj, None).is_ok());
1101 }
1102
1103 #[test]
1104 fn display_with_field_path() {
1105 let err = ValidationError {
1106 rule: "self >= 0".into(),
1107 message: "must be non-negative".into(),
1108 field_path: "spec.replicas".into(),
1109 reason: None,
1110 kind: ErrorKind::ValidationFailure,
1111 source: None,
1112 };
1113 assert_eq!(err.to_string(), "spec.replicas: must be non-negative");
1114 }
1115
1116 #[test]
1117 fn display_without_field_path() {
1118 let err = ValidationError {
1119 rule: "self >= 0".into(),
1120 message: "must be non-negative".into(),
1121 field_path: String::new(),
1122 reason: None,
1123 kind: ErrorKind::ValidationFailure,
1124 source: None,
1125 };
1126 assert_eq!(err.to_string(), "must be non-negative");
1127 }
1128
1129 #[test]
1130 fn validator_default() {
1131 let v = Validator::default();
1132 let schema = make_schema(json!([{"rule": "self.replicas >= 0"}]));
1133 let obj = json!({"replicas": 1, "name": "app"});
1134 assert!(v.validate(&schema, &obj, None).is_ok());
1135 }
1136
1137 #[test]
1138 fn additional_properties_walking() {
1139 let schema = json!({
1140 "type": "object",
1141 "additionalProperties": {
1142 "type": "integer",
1143 "x-kubernetes-validations": [
1144 {"rule": "self >= 0", "message": "must be non-negative"}
1145 ]
1146 }
1147 });
1148 let obj = json!({"a": 1, "b": -1, "c": 5});
1149 let errors = validate(&schema, &obj, None).unwrap_err();
1150 assert_eq!(errors.len(), 1);
1151 assert_eq!(errors[0].field_path, "b");
1152 }
1153
1154 #[test]
1157 fn message_expression_produces_dynamic_message() {
1158 let schema = make_schema(json!([{
1159 "rule": "self.replicas >= 0",
1160 "message": "static fallback",
1161 "messageExpression": "'replicas is ' + string(self.replicas) + ', must be >= 0'"
1162 }]));
1163 let obj = json!({"replicas": -5, "name": "app"});
1164 let errors = validate(&schema, &obj, None).unwrap_err();
1165 assert_eq!(errors.len(), 1);
1166 assert_eq!(errors[0].message, "replicas is -5, must be >= 0");
1167 }
1168
1169 #[test]
1170 fn invalid_message_expression_fails_closed() {
1171 let schema = make_schema(json!([{
1172 "rule": "self.replicas >= 0",
1173 "message": "static message",
1174 "messageExpression": "invalid >="
1175 }]));
1176 let obj = json!({"replicas": 5, "name": "app"});
1181 let errors = validate(&schema, &obj, None).unwrap_err();
1182 assert_eq!(errors.len(), 1);
1183 assert_eq!(errors[0].kind, ErrorKind::CompilationFailure);
1184 }
1185
1186 #[test]
1187 fn optional_old_self_evaluated_on_create() {
1188 let schema = make_schema(json!([{
1189 "rule": "oldSelf == null || self.replicas >= oldSelf.replicas",
1190 "message": "cannot scale down",
1191 "optionalOldSelf": true
1192 }]));
1193 let obj = json!({"replicas": 1, "name": "app"});
1195 assert!(validate(&schema, &obj, None).is_ok()); }
1197
1198 #[test]
1199 fn optional_old_self_with_old_object() {
1200 let schema = make_schema(json!([{
1201 "rule": "oldSelf == null || self.replicas >= oldSelf.replicas",
1202 "message": "cannot scale down",
1203 "optionalOldSelf": true
1204 }]));
1205 let obj = json!({"replicas": 1, "name": "app"});
1206 let old = json!({"replicas": 3, "name": "app"});
1207 let errors = validate(&schema, &obj, Some(&old)).unwrap_err();
1208 assert_eq!(errors.len(), 1);
1209 assert_eq!(errors[0].message, "cannot scale down");
1210 }
1211
1212 #[test]
1213 fn optional_old_self_false_still_skips() {
1214 let schema = make_schema(json!([{
1215 "rule": "self.replicas >= oldSelf.replicas",
1216 "message": "cannot scale down",
1217 "optionalOldSelf": false
1218 }]));
1219 let obj = json!({"replicas": 1, "name": "app"});
1220 assert!(validate(&schema, &obj, None).is_ok());
1222 }
1223
1224 #[test]
1225 fn validate_compiled_matches_validate() {
1226 let schema = json!({
1227 "type": "object",
1228 "properties": {
1229 "spec": {
1230 "type": "object",
1231 "x-kubernetes-validations": [
1232 {"rule": "self.replicas >= 0", "message": "non-negative"}
1233 ],
1234 "properties": {
1235 "replicas": {"type": "integer"}
1236 }
1237 }
1238 }
1239 });
1240 let obj = json!({"spec": {"replicas": -1}});
1241
1242 let errors_schema = validate(&schema, &obj, None).unwrap_err();
1243 let compiled = compile_schema(&schema);
1244 let errors_compiled = validate_compiled(&compiled, &obj, None).unwrap_err();
1245
1246 assert_eq!(errors_schema.len(), errors_compiled.len());
1247 assert_eq!(errors_schema[0].message, errors_compiled[0].message);
1248 assert_eq!(errors_schema[0].field_path, errors_compiled[0].field_path);
1249 }
1250
1251 #[test]
1252 fn validate_compiled_reuse() {
1253 let schema = json!({
1254 "type": "object",
1255 "x-kubernetes-validations": [
1256 {"rule": "self.x > 0", "message": "x must be positive"}
1257 ],
1258 "properties": {"x": {"type": "integer"}}
1259 });
1260 let compiled = compile_schema(&schema);
1261
1262 assert!(validate_compiled(&compiled, &json!({"x": 1}), None).is_ok());
1264 assert_eq!(
1265 validate_compiled(&compiled, &json!({"x": -1}), None)
1266 .unwrap_err()
1267 .len(),
1268 1
1269 );
1270 assert!(validate_compiled(&compiled, &json!({"x": 5}), None).is_ok());
1271 assert_eq!(
1272 validate_compiled(&compiled, &json!({"x": 0}), None)
1273 .unwrap_err()
1274 .len(),
1275 1
1276 );
1277 }
1278
1279 #[test]
1282 fn fieldpath_overrides_auto_path() {
1283 let schema = json!({
1284 "type": "object",
1285 "properties": {
1286 "spec": {
1287 "type": "object",
1288 "properties": {
1289 "x": {"type": "integer"}
1290 },
1291 "x-kubernetes-validations": [
1292 {"rule": "self.x >= 0", "message": "bad", "fieldPath": ".spec.x"}
1293 ]
1294 }
1295 }
1296 });
1297 let obj = json!({"spec": {"x": -1}});
1298 let errors = validate(&schema, &obj, None).unwrap_err();
1299 assert_eq!(errors.len(), 1);
1300 assert_eq!(errors[0].field_path, "spec.spec.x");
1301 }
1302
1303 #[test]
1304 fn fieldpath_without_dot() {
1305 let schema = json!({
1306 "type": "object",
1307 "properties": {
1308 "spec": {
1309 "type": "object",
1310 "properties": {
1311 "name": {"type": "string"}
1312 },
1313 "x-kubernetes-validations": [
1314 {"rule": "self.name.size() > 0", "message": "bad", "fieldPath": "name"}
1315 ]
1316 }
1317 }
1318 });
1319 let obj = json!({"spec": {"name": ""}});
1320 let errors = validate(&schema, &obj, None).unwrap_err();
1321 assert_eq!(errors.len(), 1);
1322 assert_eq!(errors[0].field_path, "spec.name");
1323 }
1324
1325 #[test]
1326 fn fieldpath_at_root() {
1327 let schema = json!({
1328 "type": "object",
1329 "properties": {
1330 "x": {"type": "integer"}
1331 },
1332 "x-kubernetes-validations": [
1333 {"rule": "self.x >= 0", "message": "bad", "fieldPath": ".spec.x"}
1334 ]
1335 });
1336 let obj = json!({"x": -1});
1337 let errors = validate(&schema, &obj, None).unwrap_err();
1338 assert_eq!(errors.len(), 1);
1339 assert_eq!(errors[0].field_path, ".spec.x");
1340 }
1341
1342 #[test]
1343 fn fieldpath_none_uses_auto() {
1344 let schema = json!({
1345 "type": "object",
1346 "properties": {
1347 "spec": {
1348 "type": "object",
1349 "properties": {
1350 "x": {"type": "integer"}
1351 },
1352 "x-kubernetes-validations": [
1353 {"rule": "self.x >= 0", "message": "bad"}
1354 ]
1355 }
1356 }
1357 });
1358 let obj = json!({"spec": {"x": -1}});
1359 let errors = validate(&schema, &obj, None).unwrap_err();
1360 assert_eq!(errors.len(), 1);
1361 assert_eq!(errors[0].field_path, "spec");
1362 }
1363
1364 #[test]
1367 fn error_kind_compilation_failure() {
1368 let schema = make_schema(json!([
1369 {"rule": "self.replicas >="}
1370 ]));
1371 let obj = json!({"replicas": 1, "name": "app"});
1372 let errors = validate(&schema, &obj, None).unwrap_err();
1373 assert_eq!(errors.len(), 1);
1374 assert_eq!(errors[0].kind, ErrorKind::CompilationFailure);
1375 }
1376
1377 #[test]
1378 fn error_kind_validation_failure() {
1379 let schema = make_schema(json!([
1380 {"rule": "self.replicas >= 0", "message": "must be non-negative"}
1381 ]));
1382 let obj = json!({"replicas": -1, "name": "app"});
1383 let errors = validate(&schema, &obj, None).unwrap_err();
1384 assert_eq!(errors.len(), 1);
1385 assert_eq!(errors[0].kind, ErrorKind::ValidationFailure);
1386 }
1387
1388 #[test]
1389 fn error_kind_evaluation_error() {
1390 let schema = make_schema(json!([
1391 {"rule": "self.missing_field > 0"}
1392 ]));
1393 let obj = json!({"replicas": 1, "name": "app"});
1394 let errors = validate(&schema, &obj, None).unwrap_err();
1395 assert_eq!(errors.len(), 1);
1396 assert_eq!(errors[0].kind, ErrorKind::EvaluationError);
1397 }
1398
1399 #[test]
1402 fn all_of_validations_evaluated() {
1403 let schema = json!({
1404 "type": "object",
1405 "properties": {
1406 "x": {"type": "integer"},
1407 "y": {"type": "integer"}
1408 },
1409 "allOf": [
1410 {
1411 "x-kubernetes-validations": [
1412 {"rule": "self.x >= 0", "message": "x must be non-negative"}
1413 ]
1414 },
1415 {
1416 "x-kubernetes-validations": [
1417 {"rule": "self.y >= 0", "message": "y must be non-negative"}
1418 ]
1419 }
1420 ]
1421 });
1422 let obj = json!({"x": -1, "y": -1});
1423 let errors = validate(&schema, &obj, None).unwrap_err();
1424 assert_eq!(errors.len(), 2);
1425 }
1426
1427 #[test]
1428 fn one_of_validations_evaluated() {
1429 let schema = json!({
1430 "type": "object",
1431 "properties": {"x": {"type": "integer"}},
1432 "oneOf": [{
1433 "x-kubernetes-validations": [
1434 {"rule": "self.x != 0", "message": "x must not be zero"}
1435 ]
1436 }]
1437 });
1438 let obj = json!({"x": 0});
1439 let errors = validate(&schema, &obj, None).unwrap_err();
1440 assert_eq!(errors.len(), 1);
1441 }
1442
1443 #[test]
1444 fn nested_all_of_properties_walked() {
1445 let schema = json!({
1446 "type": "object",
1447 "allOf": [{
1448 "properties": {
1449 "name": {
1450 "type": "string",
1451 "x-kubernetes-validations": [
1452 {"rule": "self.size() > 0", "message": "name required"}
1453 ]
1454 }
1455 }
1456 }]
1457 });
1458 let obj = json!({"name": ""});
1459 let errors = validate(&schema, &obj, None).unwrap_err();
1460 assert_eq!(errors.len(), 1);
1461 }
1462
1463 #[test]
1464 fn all_of_compiled_matches_schema() {
1465 let schema = json!({
1466 "type": "object",
1467 "properties": {"x": {"type": "integer"}},
1468 "allOf": [{
1469 "x-kubernetes-validations": [
1470 {"rule": "self.x >= 0", "message": "x must be non-negative"}
1471 ]
1472 }]
1473 });
1474 let obj = json!({"x": -1});
1475 let errors_schema = validate(&schema, &obj, None).unwrap_err();
1476 let compiled = compile_schema(&schema);
1477 let errors_compiled = validate_compiled(&compiled, &obj, None).unwrap_err();
1478 assert_eq!(errors_schema.len(), errors_compiled.len());
1479 assert_eq!(errors_schema[0].message, errors_compiled[0].message);
1480 }
1481
1482 #[test]
1485 fn preserve_unknown_fields_skips_additional_properties_walk() {
1486 let schema = json!({
1487 "type": "object",
1488 "x-kubernetes-preserve-unknown-fields": true,
1489 "additionalProperties": {
1490 "type": "integer",
1491 "x-kubernetes-validations": [
1492 {"rule": "self >= 0", "message": "must be non-negative"}
1493 ]
1494 }
1495 });
1496 let obj = json!({"unknown_field": -1});
1497 assert!(validate(&schema, &obj, None).is_ok());
1498 }
1499
1500 #[test]
1501 fn without_preserve_unknown_fields_additional_properties_still_walked() {
1502 let schema = json!({
1503 "type": "object",
1504 "additionalProperties": {
1505 "type": "integer",
1506 "x-kubernetes-validations": [
1507 {"rule": "self >= 0", "message": "must be non-negative"}
1508 ]
1509 }
1510 });
1511 let obj = json!({"unknown_field": -1});
1512 let errors = validate(&schema, &obj, None).unwrap_err();
1513 assert_eq!(errors.len(), 1);
1514 }
1515
1516 #[test]
1519 fn embedded_resource_fields_accessible() {
1520 let schema = json!({
1521 "type": "object",
1522 "x-kubernetes-embedded-resource": true,
1523 "properties": {
1524 "spec": {"type": "object"}
1525 },
1526 "x-kubernetes-validations": [{
1527 "rule": "self.apiVersion.size() >= 0",
1528 "message": "apiVersion must exist"
1529 }]
1530 });
1531 let obj = json!({"spec": {}});
1532 assert!(validate(&schema, &obj, None).is_ok());
1533 }
1534
1535 #[test]
1536 fn embedded_resource_preserves_existing_fields() {
1537 let schema = json!({
1538 "type": "object",
1539 "x-kubernetes-embedded-resource": true,
1540 "x-kubernetes-validations": [{
1541 "rule": "self.apiVersion == 'v1'",
1542 "message": "wrong version"
1543 }]
1544 });
1545 let obj = json!({"apiVersion": "v1", "kind": "Pod", "metadata": {"name": "test"}});
1546 assert!(validate(&schema, &obj, None).is_ok());
1547 }
1548
1549 #[test]
1550 fn embedded_resource_compiled_path() {
1551 let schema = json!({
1552 "type": "object",
1553 "x-kubernetes-embedded-resource": true,
1554 "x-kubernetes-validations": [{
1555 "rule": "self.kind.size() >= 0",
1556 "message": "kind must exist"
1557 }]
1558 });
1559 let obj = json!({"spec": {}});
1560 let compiled = compile_schema(&schema);
1561 assert!(validate_compiled(&compiled, &obj, None).is_ok());
1562 }
1563
1564 #[test]
1567 fn root_context_variables_bound() {
1568 let schema = json!({
1569 "type": "object",
1570 "properties": {"name": {"type": "string"}},
1571 "x-kubernetes-validations": [{
1572 "rule": "apiVersion == 'apps/v1'",
1573 "message": "wrong api version"
1574 }]
1575 });
1576 let obj = json!({"name": "test"});
1577 let root_ctx = RootContext {
1578 api_version: "apps/v1".into(),
1579 api_group: "apps".into(),
1580 kind: "Deployment".into(),
1581 };
1582 assert!(
1583 Validator::new()
1584 .validate_with_context(&schema, &obj, None, Some(&root_ctx))
1585 .is_ok()
1586 );
1587 }
1588
1589 #[test]
1590 fn root_context_empty_api_group_for_core() {
1591 let schema = json!({
1592 "type": "object",
1593 "properties": {"name": {"type": "string"}},
1594 "x-kubernetes-validations": [{
1595 "rule": "apiGroup == ''",
1596 "message": "not core"
1597 }]
1598 });
1599 let obj = json!({"name": "test"});
1600 let root_ctx = RootContext {
1601 api_version: "v1".into(),
1602 api_group: "".into(),
1603 kind: "Pod".into(),
1604 };
1605 assert!(
1606 Validator::new()
1607 .validate_with_context(&schema, &obj, None, Some(&root_ctx))
1608 .is_ok()
1609 );
1610 }
1611
1612 #[test]
1613 fn validate_without_root_context_still_works() {
1614 let schema = json!({
1615 "type": "object",
1616 "properties": {"x": {"type": "integer"}},
1617 "x-kubernetes-validations": [{"rule": "self.x >= 0", "message": "bad"}]
1618 });
1619 let obj = json!({"x": -1});
1620 let errors = validate(&schema, &obj, None).unwrap_err();
1621 assert_eq!(errors.len(), 1);
1622 }
1623
1624 #[test]
1625 fn root_context_compiled_path() {
1626 let schema = json!({
1627 "type": "object",
1628 "properties": {"x": {"type": "integer"}},
1629 "x-kubernetes-validations": [{
1630 "rule": "kind == 'MyResource'",
1631 "message": "wrong kind"
1632 }]
1633 });
1634 let obj = json!({"x": 1});
1635 let root_ctx = RootContext {
1636 api_version: "v1".into(),
1637 api_group: "example.com".into(),
1638 kind: "MyResource".into(),
1639 };
1640 let compiled = crate::validation::compilation::compile_schema(&schema);
1641 assert!(
1642 Validator::new()
1643 .validate_compiled_with_context(&compiled, &obj, None, Some(&root_ctx))
1644 .is_ok()
1645 );
1646 }
1647
1648 #[test]
1649 fn validate_with_defaults_fills_missing_then_validates() {
1650 let schema = json!({
1651 "type": "object",
1652 "properties": {
1653 "replicas": {
1654 "type": "integer",
1655 "default": 1,
1656 "x-kubernetes-validations": [
1657 {"rule": "self >= 0", "message": "must be non-negative"}
1658 ]
1659 }
1660 }
1661 });
1662 assert!(validate(&schema, &json!({}), None).is_ok());
1664
1665 assert!(
1667 Validator::new()
1668 .validate_with_defaults(&schema, &json!({}), None)
1669 .is_ok()
1670 );
1671 }
1672
1673 #[test]
1674 fn plain_validate_applies_defaults_closing_fail_open() {
1675 let schema = json!({
1679 "type": "object",
1680 "properties": { "x": { "type": "string", "default": "d" } },
1681 "x-kubernetes-validations": [{ "rule": "!has(self.x)", "message": "x must be absent" }]
1682 });
1683 assert!(
1684 validate(&schema, &json!({}), None).is_err(),
1685 "default makes x present → !has(self.x) false → REJECT"
1686 );
1687
1688 let compiled = crate::validation::compilation::compile_schema(&schema);
1690 assert!(
1691 Validator::new()
1692 .validate_compiled(&compiled, &json!({}), None)
1693 .is_err()
1694 );
1695
1696 let nested = json!({
1697 "type": "object",
1698 "properties": {
1699 "m": {
1700 "type": "object",
1701 "additionalProperties": {
1702 "type": "object",
1703 "properties": { "x": { "type": "string", "default": "d" } },
1704 "x-kubernetes-validations": [{ "rule": "!has(self.x)", "message": "x must be absent" }]
1705 }
1706 }
1707 }
1708 });
1709 assert!(
1710 validate(&nested, &json!({ "m": { "a": {} } }), None).is_err(),
1711 "nested map value defaulted → REJECT (was fail-open even via validate_with_defaults)"
1712 );
1713 }
1714
1715 #[test]
1716 fn validate_with_defaults_and_context_combined() {
1717 let schema = json!({
1718 "type": "object",
1719 "properties": {
1720 "replicas": {
1721 "type": "integer",
1722 "default": 1,
1723 "x-kubernetes-validations": [
1724 {"rule": "self >= 0", "message": "must be non-negative"}
1725 ]
1726 }
1727 },
1728 "x-kubernetes-validations": [
1729 {"rule": "kind == 'Deployment'", "message": "wrong kind"}
1730 ]
1731 });
1732 let root_ctx = RootContext {
1733 api_version: "apps/v1".into(),
1734 api_group: "apps".into(),
1735 kind: "Deployment".into(),
1736 };
1737 assert!(
1739 Validator::new()
1740 .validate_with_defaults_and_context(&schema, &json!({}), None, Some(&root_ctx))
1741 .is_ok()
1742 );
1743 }
1744
1745 #[test]
1746 fn validation_error_serializable() {
1747 let err = ValidationError {
1748 rule: "self.x >= 0".into(),
1749 message: "must be non-negative".into(),
1750 field_path: "spec.x".into(),
1751 reason: Some("FieldValueInvalid".into()),
1752 kind: ErrorKind::ValidationFailure,
1753 source: None,
1754 };
1755 let json = serde_json::to_value(&err).unwrap();
1756 assert_eq!(json["rule"], "self.x >= 0");
1757 assert_eq!(json["field_path"], "spec.x");
1758 assert_eq!(json["kind"], "ValidationFailure");
1759 }
1760}