1use std::{collections::HashMap, sync::Arc};
13
14use base64::Engine;
15use cel::{
16 Value,
17 objects::{Key, Map},
18};
19
20use crate::validation::{compilation::CompiledSchema, escaping::escape_field_name};
21
22#[derive(Clone, Debug, Default, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum SchemaFormat {
26 DateTime,
28 Duration,
30 Bytes,
35 IntOrString,
38 #[default]
40 None,
41}
42
43impl SchemaFormat {
44 pub(crate) fn from_schema(schema: &serde_json::Value) -> Self {
46 if schema.get("x-kubernetes-int-or-string").and_then(|v| v.as_bool()) == Some(true) {
47 return SchemaFormat::IntOrString;
48 }
49 match schema.get("format").and_then(|f| f.as_str()) {
50 Some("date-time") => SchemaFormat::DateTime,
51 Some("duration") => SchemaFormat::Duration,
52 Some("byte") => SchemaFormat::Bytes,
53 _ => SchemaFormat::None,
54 }
55 }
56}
57
58#[must_use]
70pub(crate) fn json_to_cel(value: &serde_json::Value) -> Value {
71 match value {
72 serde_json::Value::Null => Value::Null,
73 serde_json::Value::Bool(b) => Value::Bool(*b),
74 serde_json::Value::Number(n) => convert_number(n),
75 serde_json::Value::String(s) => Value::String(Arc::new(s.clone())),
76 serde_json::Value::Array(arr) => {
77 let items: Vec<Value> = arr.iter().map(json_to_cel).collect();
78 Value::List(Arc::new(items))
79 }
80 serde_json::Value::Object(obj) => {
81 let mut map = HashMap::with_capacity(obj.len());
82 for (k, v) in obj {
83 map.insert(Key::String(Arc::new(escape_field_name(k))), json_to_cel(v));
84 }
85 Value::Map(Map { map: Arc::new(map) })
86 }
87 }
88}
89
90#[inline]
91fn convert_number(n: &serde_json::Number) -> Value {
92 if let Some(i) = n.as_i64() {
93 Value::Int(i)
94 } else if let Some(u) = n.as_u64() {
95 Value::UInt(u)
96 } else {
97 Value::Float(n.as_f64().unwrap_or(f64::NAN))
98 }
99}
100
101#[must_use]
109pub(crate) fn json_to_cel_with_schema(value: &serde_json::Value, schema: &serde_json::Value) -> Value {
110 let format = SchemaFormat::from_schema(schema);
111 match value {
112 serde_json::Value::Null => Value::Null,
113 serde_json::Value::Bool(b) => Value::Bool(*b),
114 serde_json::Value::Number(n) => convert_number(n),
115 serde_json::Value::String(s) => convert_string_with_format(s, &format),
116 serde_json::Value::Array(arr) => {
117 let items_schema = schema.get("items");
118 let items: Vec<Value> = arr
119 .iter()
120 .map(|item| match items_schema {
121 Some(s) => json_to_cel_with_schema(item, s),
122 None => json_to_cel(item),
123 })
124 .collect();
125 Value::List(Arc::new(items))
126 }
127 serde_json::Value::Object(obj) => {
128 let props = schema.get("properties").and_then(|p| p.as_object());
129 let additional = schema.get("additionalProperties");
130 let additional_schema = additional.filter(|a| a.is_object());
131 let is_map = !matches!(additional, None | Some(serde_json::Value::Bool(false)));
136
137 let mut map = HashMap::with_capacity(obj.len());
138 for (k, v) in obj {
139 let child_val = if let Some(prop_schema) = props.and_then(|p| p.get(k)) {
140 json_to_cel_with_schema(v, prop_schema)
141 } else if let Some(additional_schema) = additional_schema {
142 json_to_cel_with_schema(v, additional_schema)
143 } else {
144 json_to_cel(v)
145 };
146 let key = if is_map { k.clone() } else { escape_field_name(k) };
147 map.insert(Key::String(Arc::new(key)), child_val);
148 }
149
150 let is_embedded = schema
151 .get("x-kubernetes-embedded-resource")
152 .and_then(|v| v.as_bool())
153 == Some(true);
154 if is_embedded {
155 map.entry(Key::String(Arc::new("apiVersion".into())))
156 .or_insert_with(|| Value::String(Arc::new(String::new())));
157 map.entry(Key::String(Arc::new("kind".into())))
158 .or_insert_with(|| Value::String(Arc::new(String::new())));
159 map.entry(Key::String(Arc::new("metadata".into())))
160 .or_insert_with(|| {
161 Value::Map(Map {
162 map: Arc::new(HashMap::new()),
163 })
164 });
165 }
166
167 Value::Map(Map { map: Arc::new(map) })
168 }
169 }
170}
171
172#[must_use]
177pub(crate) fn json_to_cel_with_compiled(value: &serde_json::Value, compiled: &CompiledSchema) -> Value {
178 match value {
179 serde_json::Value::Null => Value::Null,
180 serde_json::Value::Bool(b) => Value::Bool(*b),
181 serde_json::Value::Number(n) => convert_number(n),
182 serde_json::Value::String(s) => convert_string_with_format(s, &compiled.format),
183 serde_json::Value::Array(arr) => {
184 let items: Vec<Value> = arr
185 .iter()
186 .map(|item| match &compiled.items {
187 Some(items_compiled) => json_to_cel_with_compiled(item, items_compiled),
188 None => json_to_cel(item),
189 })
190 .collect();
191 Value::List(Arc::new(items))
192 }
193 serde_json::Value::Object(obj) => {
194 let mut map = HashMap::with_capacity(obj.len());
195 for (k, v) in obj {
196 let child_val = if let Some(prop_compiled) = compiled.properties.get(k) {
197 json_to_cel_with_compiled(v, prop_compiled)
198 } else if let Some(ref additional) = compiled.additional_properties {
199 json_to_cel_with_compiled(v, additional)
200 } else {
201 json_to_cel(v)
202 };
203 let key = if compiled.is_map {
206 k.clone()
207 } else {
208 escape_field_name(k)
209 };
210 map.insert(Key::String(Arc::new(key)), child_val);
211 }
212
213 if compiled.embedded_resource {
214 map.entry(Key::String(Arc::new("apiVersion".into())))
215 .or_insert_with(|| Value::String(Arc::new(String::new())));
216 map.entry(Key::String(Arc::new("kind".into())))
217 .or_insert_with(|| Value::String(Arc::new(String::new())));
218 map.entry(Key::String(Arc::new("metadata".into())))
219 .or_insert_with(|| {
220 Value::Map(Map {
221 map: Arc::new(HashMap::new()),
222 })
223 });
224 }
225
226 Value::Map(Map { map: Arc::new(map) })
227 }
228 }
229}
230
231#[inline]
233fn convert_string_with_format(s: &str, format: &SchemaFormat) -> Value {
234 match format {
235 SchemaFormat::DateTime => {
236 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
237 return Value::Timestamp(dt);
238 }
239 Value::String(Arc::new(s.to_string()))
240 }
241 SchemaFormat::Duration => {
242 if let Some(d) = parse_go_duration(s) {
243 return Value::Duration(d);
244 }
245 Value::String(Arc::new(s.to_string()))
246 }
247 SchemaFormat::Bytes => match base64::engine::general_purpose::STANDARD.decode(s) {
248 Ok(bytes) => Value::Bytes(Arc::new(bytes)),
249 Err(_) => Value::String(Arc::new(s.to_string())),
252 },
253 SchemaFormat::IntOrString => Value::String(Arc::new(s.to_string())),
254 SchemaFormat::None => Value::String(Arc::new(s.to_string())),
255 }
256}
257
258pub(crate) fn parse_go_duration(input: &str) -> Option<chrono::Duration> {
267 let (input, negative) = if let Some(rest) = input.strip_prefix('-') {
268 (rest, true)
269 } else {
270 (input, false)
271 };
272
273 if input == "0" {
274 return Some(chrono::Duration::zero());
275 }
276
277 let mut remaining = input;
278 let mut total_nanos: i64 = 0;
279 let mut parsed_any = false;
280
281 while !remaining.is_empty() {
282 let num_end = remaining
284 .find(|c: char| !c.is_ascii_digit() && c != '.')
285 .unwrap_or(0);
286 if num_end == 0 {
287 return None; }
289 let num_str = &remaining[..num_end];
290 let num: f64 = num_str.parse().ok()?;
291 remaining = &remaining[num_end..];
292
293 let (unit_nanos, unit_len) = if remaining.starts_with("ms") {
295 (1_000_000i64, 2)
296 } else if remaining.starts_with("us") {
297 (1_000i64, 2)
298 } else if remaining.starts_with("ns") {
299 (1i64, 2)
300 } else if remaining.starts_with('h') {
301 (3_600_000_000_000i64, 1)
302 } else if remaining.starts_with('m') {
303 (60_000_000_000i64, 1)
304 } else if remaining.starts_with('s') {
305 (1_000_000_000i64, 1)
306 } else {
307 return None; };
309
310 remaining = &remaining[unit_len..];
311 total_nanos += (num * unit_nanos as f64).trunc() as i64;
312 parsed_any = true;
313 }
314
315 if !parsed_any {
316 return None;
317 }
318
319 if negative {
320 total_nanos = -total_nanos;
321 }
322 Some(chrono::Duration::nanoseconds(total_nanos))
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use serde_json::json;
329
330 #[test]
331 fn test_null() {
332 assert_eq!(json_to_cel(&json!(null)), Value::Null);
333 }
334
335 #[test]
336 fn test_bool() {
337 assert_eq!(json_to_cel(&json!(true)), Value::Bool(true));
338 assert_eq!(json_to_cel(&json!(false)), Value::Bool(false));
339 }
340
341 #[test]
342 fn test_i64() {
343 assert_eq!(json_to_cel(&json!(42)), Value::Int(42));
344 assert_eq!(json_to_cel(&json!(-1)), Value::Int(-1));
345 assert_eq!(json_to_cel(&json!(0)), Value::Int(0));
346 }
347
348 #[test]
349 fn test_u64_beyond_i64() {
350 let big: u64 = (i64::MAX as u64) + 1;
351 let v = json_to_cel(&serde_json::Value::Number(serde_json::Number::from(big)));
352 assert_eq!(v, Value::UInt(big));
353 }
354
355 #[test]
356 fn test_float() {
357 assert_eq!(json_to_cel(&json!(2.5)), Value::Float(2.5));
358 assert_eq!(json_to_cel(&json!(0.0)), Value::Float(0.0));
359 }
360
361 #[test]
362 fn test_string() {
363 assert_eq!(
364 json_to_cel(&json!("hello")),
365 Value::String(Arc::new("hello".into()))
366 );
367 }
368
369 #[test]
370 fn test_empty_string() {
371 assert_eq!(json_to_cel(&json!("")), Value::String(Arc::new(String::new())));
372 }
373
374 #[test]
375 fn test_array_mixed() {
376 let v = json_to_cel(&json!([1, "two", true, null]));
377 let expected = Value::List(Arc::new(vec![
378 Value::Int(1),
379 Value::String(Arc::new("two".into())),
380 Value::Bool(true),
381 Value::Null,
382 ]));
383 assert_eq!(v, expected);
384 }
385
386 #[test]
387 fn test_empty_array() {
388 assert_eq!(json_to_cel(&json!([])), Value::List(Arc::new(vec![])));
389 }
390
391 #[test]
392 fn test_object() {
393 let v = json_to_cel(&json!({"name": "test", "count": 5}));
394 if let Value::Map(map) = v {
395 assert_eq!(
396 map.map.get(&Key::String(Arc::new("name".into()))),
397 Some(&Value::String(Arc::new("test".into())))
398 );
399 assert_eq!(
400 map.map.get(&Key::String(Arc::new("count".into()))),
401 Some(&Value::Int(5))
402 );
403 } else {
404 panic!("expected Map");
405 }
406 }
407
408 #[test]
409 fn test_empty_object() {
410 let v = json_to_cel(&json!({}));
411 if let Value::Map(map) = v {
412 assert!(map.map.is_empty());
413 } else {
414 panic!("expected Map");
415 }
416 }
417
418 #[test]
419 fn test_nested_structure() {
420 let v = json_to_cel(&json!({
421 "spec": {
422 "replicas": 3,
423 "items": [1, 2, 3]
424 }
425 }));
426 if let Value::Map(outer) = v {
427 let spec = outer.map.get(&Key::String(Arc::new("spec".into()))).unwrap();
428 if let Value::Map(inner) = spec {
429 assert_eq!(
430 inner.map.get(&Key::String(Arc::new("replicas".into()))),
431 Some(&Value::Int(3))
432 );
433 assert_eq!(
434 inner.map.get(&Key::String(Arc::new("items".into()))),
435 Some(&Value::List(Arc::new(vec![
436 Value::Int(1),
437 Value::Int(2),
438 Value::Int(3),
439 ])))
440 );
441 } else {
442 panic!("expected inner Map");
443 }
444 } else {
445 panic!("expected outer Map");
446 }
447 }
448
449 #[test]
450 fn test_number_priority() {
451 assert_eq!(json_to_cel(&json!(42)), Value::Int(42));
453 let big: u64 = (i64::MAX as u64) + 1;
455 assert_eq!(
456 json_to_cel(&serde_json::Value::Number(serde_json::Number::from(big))),
457 Value::UInt(big)
458 );
459 assert_eq!(json_to_cel(&json!(1.5)), Value::Float(1.5));
461 }
462
463 #[test]
466 fn parse_duration_hours() {
467 assert_eq!(parse_go_duration("1h"), Some(chrono::Duration::hours(1)));
468 }
469
470 #[test]
471 fn parse_duration_minutes() {
472 assert_eq!(parse_go_duration("30m"), Some(chrono::Duration::minutes(30)));
473 }
474
475 #[test]
476 fn parse_duration_seconds() {
477 assert_eq!(parse_go_duration("45s"), Some(chrono::Duration::seconds(45)));
478 }
479
480 #[test]
481 fn parse_duration_milliseconds() {
482 assert_eq!(
483 parse_go_duration("500ms"),
484 Some(chrono::Duration::milliseconds(500))
485 );
486 }
487
488 #[test]
489 fn parse_duration_microseconds() {
490 assert_eq!(
491 parse_go_duration("100us"),
492 Some(chrono::Duration::microseconds(100))
493 );
494 }
495
496 #[test]
497 fn parse_duration_nanoseconds() {
498 assert_eq!(
499 parse_go_duration("999ns"),
500 Some(chrono::Duration::nanoseconds(999))
501 );
502 }
503
504 #[test]
505 fn parse_duration_compound() {
506 assert_eq!(
507 parse_go_duration("1h30m"),
508 Some(chrono::Duration::hours(1) + chrono::Duration::minutes(30))
509 );
510 assert_eq!(
511 parse_go_duration("1h30m10s"),
512 Some(chrono::Duration::hours(1) + chrono::Duration::minutes(30) + chrono::Duration::seconds(10))
513 );
514 }
515
516 #[test]
517 fn parse_duration_negative() {
518 assert_eq!(parse_go_duration("-1h"), Some(chrono::Duration::hours(-1)));
519 assert_eq!(parse_go_duration("-30s"), Some(chrono::Duration::seconds(-30)));
520 }
521
522 #[test]
523 fn parse_duration_zero() {
524 assert_eq!(parse_go_duration("0"), Some(chrono::Duration::zero()));
525 }
526
527 #[test]
528 fn parse_duration_invalid() {
529 assert_eq!(parse_go_duration(""), None);
530 assert_eq!(parse_go_duration("abc"), None);
531 assert_eq!(parse_go_duration("1x"), None);
532 assert_eq!(parse_go_duration("h"), None);
533 }
534
535 #[test]
538 fn timestamp_parsed_from_schema() {
539 let schema = json!({
540 "type": "string",
541 "format": "date-time"
542 });
543 let value = json!("2024-01-01T00:00:00Z");
544 let result = json_to_cel_with_schema(&value, &schema);
545 assert!(matches!(result, Value::Timestamp(_)));
546 }
547
548 #[test]
549 fn byte_format_decodes_to_bytes() {
550 let schema = json!({ "type": "string", "format": "byte" });
551 let result = json_to_cel_with_schema(&json!("aGVsbG8="), &schema);
554 assert_eq!(result, Value::Bytes(Arc::new(b"hello".to_vec())));
555 }
556
557 #[test]
558 fn byte_format_invalid_falls_back_to_string() {
559 let schema = json!({ "type": "string", "format": "byte" });
560 let result = json_to_cel_with_schema(&json!("!!!not-base64!!!"), &schema);
561 assert_eq!(result, Value::String(Arc::new("!!!not-base64!!!".into())));
562 }
563
564 #[test]
565 fn timestamp_parse_failure_falls_back_to_string() {
566 let schema = json!({
567 "type": "string",
568 "format": "date-time"
569 });
570 let value = json!("not-a-date");
571 let result = json_to_cel_with_schema(&value, &schema);
572 assert_eq!(result, Value::String(Arc::new("not-a-date".into())));
573 }
574
575 #[test]
576 fn duration_parsed_from_schema() {
577 let schema = json!({
578 "type": "string",
579 "format": "duration"
580 });
581 let value = json!("1h30m");
582 let result = json_to_cel_with_schema(&value, &schema);
583 assert!(matches!(result, Value::Duration(_)));
584 }
585
586 #[test]
587 fn duration_parse_failure_falls_back_to_string() {
588 let schema = json!({
589 "type": "string",
590 "format": "duration"
591 });
592 let value = json!("not-a-duration");
593 let result = json_to_cel_with_schema(&value, &schema);
594 assert_eq!(result, Value::String(Arc::new("not-a-duration".into())));
595 }
596
597 #[test]
598 fn nested_object_properties_format() {
599 let schema = json!({
600 "type": "object",
601 "properties": {
602 "createdAt": {
603 "type": "string",
604 "format": "date-time"
605 },
606 "name": {
607 "type": "string"
608 },
609 "timeout": {
610 "type": "string",
611 "format": "duration"
612 }
613 }
614 });
615 let value = json!({
616 "createdAt": "2024-06-15T10:30:00Z",
617 "name": "test",
618 "timeout": "30s"
619 });
620 let result = json_to_cel_with_schema(&value, &schema);
621 if let Value::Map(map) = result {
622 assert!(matches!(
623 map.map.get(&Key::String(Arc::new("createdAt".into()))),
624 Some(Value::Timestamp(_))
625 ));
626 assert!(matches!(
627 map.map.get(&Key::String(Arc::new("name".into()))),
628 Some(Value::String(_))
629 ));
630 assert!(matches!(
631 map.map.get(&Key::String(Arc::new("timeout".into()))),
632 Some(Value::Duration(_))
633 ));
634 } else {
635 panic!("expected Map");
636 }
637 }
638
639 #[test]
640 fn array_items_format() {
641 let schema = json!({
642 "type": "array",
643 "items": {
644 "type": "string",
645 "format": "date-time"
646 }
647 });
648 let value = json!(["2024-01-01T00:00:00Z", "2024-06-15T12:00:00+09:00"]);
649 let result = json_to_cel_with_schema(&value, &schema);
650 if let Value::List(items) = result {
651 assert!(matches!(items[0], Value::Timestamp(_)));
652 assert!(matches!(items[1], Value::Timestamp(_)));
653 } else {
654 panic!("expected List");
655 }
656 }
657
658 #[test]
659 fn no_format_leaves_string_unchanged() {
660 let schema = json!({
661 "type": "string"
662 });
663 let value = json!("2024-01-01T00:00:00Z");
664 let result = json_to_cel_with_schema(&value, &schema);
665 assert_eq!(result, Value::String(Arc::new("2024-01-01T00:00:00Z".into())));
666 }
667
668 #[test]
669 fn json_to_cel_unchanged_with_no_schema() {
670 let value = json!("2024-01-01T00:00:00Z");
672 let result = json_to_cel(&value);
673 assert_eq!(result, Value::String(Arc::new("2024-01-01T00:00:00Z".into())));
674 }
675
676 #[test]
677 fn int_or_string_schema_format_detected() {
678 let schema = json!({"x-kubernetes-int-or-string": true});
679 assert_eq!(SchemaFormat::from_schema(&schema), SchemaFormat::IntOrString);
680 }
681
682 #[test]
683 fn int_or_string_int_value_preserved() {
684 let schema = json!({"x-kubernetes-int-or-string": true});
685 let result = json_to_cel_with_schema(&json!(8080), &schema);
686 assert_eq!(result, Value::Int(8080));
687 }
688
689 #[test]
690 fn int_or_string_string_value_preserved() {
691 let schema = json!({"x-kubernetes-int-or-string": true});
692 let result = json_to_cel_with_schema(&json!("http"), &schema);
693 assert_eq!(result, Value::String(Arc::new("http".into())));
694 }
695
696 #[test]
697 fn int_or_string_overrides_format() {
698 let schema = json!({"x-kubernetes-int-or-string": true, "format": "date-time"});
700 assert_eq!(SchemaFormat::from_schema(&schema), SchemaFormat::IntOrString);
701 }
702
703 fn map_has_key(value: &Value, key: &str) -> bool {
710 match value {
711 Value::Map(m) => m.map.contains_key(&Key::String(Arc::new(key.into()))),
712 _ => panic!("expected Map"),
713 }
714 }
715
716 #[test]
717 fn additional_properties_object_keys_not_escaped() {
718 let schema = json!({
720 "type": "object",
721 "additionalProperties": {"type": "string"}
722 });
723 let value = json!({"rio.build/fetcher": "true"});
724 let result = json_to_cel_with_schema(&value, &schema);
725 assert!(map_has_key(&result, "rio.build/fetcher"));
727 assert!(!map_has_key(&result, "rio__dot__build__slash__fetcher"));
728 }
729
730 #[test]
731 fn additional_properties_true_keys_not_escaped() {
732 let schema = json!({
734 "type": "object",
735 "additionalProperties": true
736 });
737 let value = json!({"a.b/c": "x"});
738 let result = json_to_cel_with_schema(&value, &schema);
739 assert!(map_has_key(&result, "a.b/c"));
740 assert!(!map_has_key(&result, "a__dot__b__slash__c"));
741 }
742
743 #[test]
744 fn declared_properties_still_escaped() {
745 let schema = json!({
748 "type": "object",
749 "additionalProperties": false,
750 "properties": {
751 "app.kubernetes.io/name": {"type": "string"}
752 }
753 });
754 let value = json!({"app.kubernetes.io/name": "web"});
755 let result = json_to_cel_with_schema(&value, &schema);
756 assert!(map_has_key(&result, "app__dot__kubernetes__dot__io__slash__name"));
757 assert!(!map_has_key(&result, "app.kubernetes.io/name"));
758 }
759
760 #[test]
761 fn nested_map_under_property_keys_not_escaped() {
762 let schema = json!({
764 "type": "object",
765 "properties": {
766 "nodeSelector": {
767 "type": "object",
768 "additionalProperties": {"type": "string"}
769 }
770 }
771 });
772 let value = json!({"nodeSelector": {"rio.build/fetcher": "true"}});
773 let result = json_to_cel_with_schema(&value, &schema);
774 let Value::Map(outer) = &result else {
775 panic!("expected Map")
776 };
777 let inner = outer
778 .map
779 .get(&Key::String(Arc::new("nodeSelector".into())))
780 .unwrap();
781 assert!(map_has_key(inner, "rio.build/fetcher"));
782 assert!(!map_has_key(inner, "rio__dot__build__slash__fetcher"));
783 }
784
785 #[test]
786 fn compiled_additional_properties_keys_not_escaped() {
787 let schema = json!({
789 "type": "object",
790 "additionalProperties": {"type": "string"}
791 });
792 let compiled = crate::validation::compilation::compile_schema(&schema);
793 let value = json!({"rio.build/fetcher": "true"});
794 let result = json_to_cel_with_compiled(&value, &compiled);
795 assert!(map_has_key(&result, "rio.build/fetcher"));
796 assert!(!map_has_key(&result, "rio__dot__build__slash__fetcher"));
797 }
798
799 #[test]
800 fn compiled_additional_properties_true_keys_not_escaped() {
801 let schema = json!({
802 "type": "object",
803 "additionalProperties": true
804 });
805 let compiled = crate::validation::compilation::compile_schema(&schema);
806 let value = json!({"a.b/c": "x"});
807 let result = json_to_cel_with_compiled(&value, &compiled);
808 assert!(map_has_key(&result, "a.b/c"));
809 assert!(!map_has_key(&result, "a__dot__b__slash__c"));
810 }
811
812 #[test]
813 fn compiled_declared_properties_still_escaped() {
814 let schema = json!({
815 "type": "object",
816 "additionalProperties": false,
817 "properties": {
818 "app.kubernetes.io/name": {"type": "string"}
819 }
820 });
821 let compiled = crate::validation::compilation::compile_schema(&schema);
822 let value = json!({"app.kubernetes.io/name": "web"});
823 let result = json_to_cel_with_compiled(&value, &compiled);
824 assert!(map_has_key(&result, "app__dot__kubernetes__dot__io__slash__name"));
825 assert!(!map_has_key(&result, "app.kubernetes.io/name"));
826 }
827}
828
829#[cfg(test)]
834mod cel_evaluation_tests {
835 use std::sync::Arc;
836
837 use cel::{Context, Program, Value};
838 use serde_json::json;
839
840 use super::{json_to_cel, json_to_cel_with_compiled};
841 use crate::{register_all, validation::compilation::compile_schema};
842
843 fn eval_self(json_val: serde_json::Value, expr: &str) -> Value {
846 let mut ctx = Context::default();
847 register_all(&mut ctx);
848 ctx.add_variable_from_value("self", json_to_cel(&json_val));
849 Program::compile(expr).unwrap().execute(&ctx).unwrap()
850 }
851
852 #[test]
856 fn in_operator_matches_unescaped_map_key() {
857 let schema = json!({
858 "type": "object",
859 "properties": {
860 "m": {"type": "object", "additionalProperties": {"type": "string"}}
861 }
862 });
863 let compiled = compile_schema(&schema);
864 let self_val = json!({"m": {"a.b/c": "x"}});
865
866 let mut ctx = Context::default();
867 register_all(&mut ctx);
868 ctx.add_variable_from_value("self", json_to_cel_with_compiled(&self_val, &compiled));
869
870 let prog = Program::compile("!('a.b/c' in self.m)").unwrap();
872 assert_eq!(prog.execute(&ctx).unwrap(), Value::Bool(false));
873 }
874
875 fn eval_transition(json_self: serde_json::Value, json_old: serde_json::Value, expr: &str) -> Value {
877 let mut ctx = Context::default();
878 register_all(&mut ctx);
879 ctx.add_variable_from_value("self", json_to_cel(&json_self));
880 ctx.add_variable_from_value("oldSelf", json_to_cel(&json_old));
881 Program::compile(expr).unwrap().execute(&ctx).unwrap()
882 }
883
884 #[test]
885 fn scalar_comparison() {
886 assert_eq!(eval_self(json!(10), "self >= 0"), Value::Bool(true));
887 assert_eq!(eval_self(json!(-1), "self >= 0"), Value::Bool(false));
888 }
889
890 #[test]
891 fn field_access_int() {
892 let obj = json!({"replicas": 3});
893 assert_eq!(eval_self(obj, "self.replicas"), Value::Int(3));
894 }
895
896 #[test]
897 fn field_access_string() {
898 let obj = json!({"name": "my-app"});
899 assert_eq!(
900 eval_self(obj, "self.name"),
901 Value::String(Arc::new("my-app".into()))
902 );
903 }
904
905 #[test]
906 fn nested_field_comparison() {
907 let obj = json!({"spec": {"replicas": 5, "minReplicas": 2}});
908 assert_eq!(
909 eval_self(obj, "self.spec.replicas >= self.spec.minReplicas"),
910 Value::Bool(true)
911 );
912 }
913
914 #[test]
915 fn transition_rule_oldself() {
916 let new = json!({"replicas": 5});
917 let old = json!({"replicas": 3});
918 assert_eq!(
919 eval_transition(new, old, "self.replicas >= oldSelf.replicas"),
920 Value::Bool(true)
921 );
922 }
923
924 #[test]
925 fn transition_rule_downscale_rejected() {
926 let new = json!({"replicas": 1});
927 let old = json!({"replicas": 3});
928 assert_eq!(
929 eval_transition(new, old, "self.replicas >= oldSelf.replicas"),
930 Value::Bool(false)
931 );
932 }
933
934 #[test]
935 fn detect_oldself_reference() {
936 let prog1 = Program::compile("self.replicas >= oldSelf.replicas").unwrap();
937 assert!(prog1.references().has_variable("oldSelf"));
938 assert!(prog1.references().has_variable("self"));
939
940 let prog2 = Program::compile("self.replicas >= 0").unwrap();
941 assert!(!prog2.references().has_variable("oldSelf"));
942 assert!(prog2.references().has_variable("self"));
943 }
944
945 #[test]
946 #[cfg(feature = "strings")]
947 fn extension_trim_lower_ascii() {
948 let obj = json!({"name": " Hello World "});
949 assert_eq!(
950 eval_self(obj, "self.name.trim().lowerAscii()"),
951 Value::String(Arc::new("hello world".into()))
952 );
953 }
954
955 #[test]
956 #[cfg(feature = "lists")]
957 fn extension_is_sorted() {
958 let obj = json!({"items": [1, 2, 3, 4]});
959 assert_eq!(eval_self(obj, "self.items.isSorted()"), Value::Bool(true));
960
961 let obj2 = json!({"items": [3, 1, 2]});
962 assert_eq!(eval_self(obj2, "self.items.isSorted()"), Value::Bool(false));
963 }
964
965 #[test]
966 fn array_indexing() {
967 let obj = json!({"containers": [{"name": "nginx"}, {"name": "sidecar"}]});
968 assert_eq!(
969 eval_self(obj, "self.containers[0].name"),
970 Value::String(Arc::new("nginx".into()))
971 );
972 }
973
974 #[test]
975 fn null_comparison() {
976 let obj = json!({"extra": null});
977 assert_eq!(eval_self(obj, "self.extra == null"), Value::Bool(true));
978 }
979
980 #[test]
981 fn non_null_comparison() {
982 let obj = json!({"extra": "present"});
983 assert_eq!(eval_self(obj, "self.extra == null"), Value::Bool(false));
984 }
985
986 #[test]
987 fn has_macro_present() {
988 let obj = json!({"name": "test"});
989 assert_eq!(eval_self(obj, "has(self.name)"), Value::Bool(true));
990 }
991
992 #[test]
993 fn has_macro_missing() {
994 let obj = json!({"name": "test"});
995 assert_eq!(eval_self(obj, "has(self.missing)"), Value::Bool(false));
996 }
997
998 #[test]
999 fn size_of_list() {
1000 let obj = json!({"items": [1, 2, 3]});
1001 assert_eq!(eval_self(obj, "size(self.items)"), Value::Int(3));
1002 }
1003
1004 #[test]
1005 fn size_of_string() {
1006 let obj = json!({"name": "hello"});
1007 assert_eq!(eval_self(obj, "size(self.name)"), Value::Int(5));
1008 }
1009}