1use crate::context_data_api::error::ValueMappingError;
12
13use super::CedarType;
14use cedar_policy::{EntityId, EntityTypeName, EntityUid, RestrictedExpression};
15use serde_json::{Map, Value};
16use std::collections::HashMap;
17use std::net::IpAddr;
18use std::str::FromStr;
19
20#[derive(Debug, Clone, PartialEq)]
22pub(super) struct EntityReference {
23 pub entity_type: String,
25 pub entity_id: String,
27}
28
29#[derive(Debug, Clone, PartialEq)]
31pub enum ExtensionValue {
32 IpAddr(String),
34 Decimal(String),
36 DateTime(String),
38 Duration(String),
40}
41
42#[derive(Debug, Clone)]
48pub struct CedarValueMapper {
49 auto_detect_extensions: bool,
51 max_value_size: usize,
53}
54
55impl Default for CedarValueMapper {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl CedarValueMapper {
62 #[must_use]
64 pub fn new() -> Self {
65 Self {
66 auto_detect_extensions: true,
67 max_value_size: 0,
68 }
69 }
70
71 #[must_use]
73 pub fn new_without_auto_detect() -> Self {
74 Self {
75 auto_detect_extensions: false,
76 max_value_size: 0,
77 }
78 }
79
80 #[must_use]
84 pub fn with_max_size(mut self, max_size: usize) -> Self {
85 self.max_value_size = max_size;
86 self
87 }
88
89 pub fn json_to_cedar(
94 &self,
95 value: &Value,
96 ) -> Result<Option<RestrictedExpression>, ValueMappingError> {
97 if self.max_value_size > 0 {
99 let size = Self::estimate_value_size(value);
100 if size > self.max_value_size {
101 return Err(ValueMappingError::ValueTooLarge {
102 size,
103 limit: self.max_value_size,
104 });
105 }
106 }
107
108 self.convert_value(value)
109 }
110
111 pub fn json_to_cedar_with_type(
115 &self,
116 value: &Value,
117 ) -> Result<Option<(RestrictedExpression, CedarType)>, ValueMappingError> {
118 let cedar_type = CedarType::from_value(value);
119 let expr = self.json_to_cedar(value)?;
120 Ok(expr.map(|e| (e, cedar_type)))
121 }
122
123 pub fn cedar_to_json(expr_json: &Value) -> Result<Value, ValueMappingError> {
134 Self::normalize_cedar_json(expr_json)
137 }
138
139 pub fn get_nested<'a>(value: &'a Value, path: &str) -> Result<&'a Value, ValueMappingError> {
141 if path.is_empty() {
142 return Ok(value);
143 }
144
145 let mut current = value;
146 for component in path.split('.') {
147 if component.is_empty() {
148 return Err(ValueMappingError::InvalidPath {
149 path: path.to_string(),
150 });
151 }
152
153 current = match current {
154 Value::Object(obj) => {
155 obj.get(component)
156 .ok_or_else(|| ValueMappingError::PathNotFound {
157 path: path.to_string(),
158 })?
159 },
160 Value::Array(arr) => {
161 let index: usize =
163 component
164 .parse()
165 .map_err(|_| ValueMappingError::PathNotFound {
166 path: path.to_string(),
167 })?;
168 arr.get(index)
169 .ok_or_else(|| ValueMappingError::PathNotFound {
170 path: path.to_string(),
171 })?
172 },
173 _ => {
174 return Err(ValueMappingError::PathNotFound {
175 path: path.to_string(),
176 });
177 },
178 };
179 }
180
181 Ok(current)
182 }
183
184 pub fn set_nested(
186 value: &mut Value,
187 path: &str,
188 new_value: Value,
189 ) -> Result<(), ValueMappingError> {
190 if path.is_empty() {
191 *value = new_value;
192 return Ok(());
193 }
194
195 let components: Vec<&str> = path.split('.').collect();
196 let mut current = value;
197
198 for (i, component) in components.iter().enumerate() {
199 if component.is_empty() {
200 return Err(ValueMappingError::InvalidPath {
201 path: path.to_string(),
202 });
203 }
204
205 let is_last = i == components.len() - 1;
206
207 if is_last {
208 let Value::Object(obj) = current else {
210 return Err(ValueMappingError::TypeMismatch {
211 expected: "object".to_string(),
212 actual: Self::value_type_name(current).to_string(),
213 });
214 };
215 obj.insert((*component).to_string(), new_value);
216 return Ok(());
217 }
218
219 let Value::Object(obj) = current else {
221 return Err(ValueMappingError::TypeMismatch {
222 expected: "object".to_string(),
223 actual: Self::value_type_name(current).to_string(),
224 });
225 };
226 current = obj
227 .entry((*component).to_string())
228 .or_insert_with(|| Value::Object(Map::new()));
229 }
230
231 Ok(())
232 }
233
234 #[must_use]
244 pub fn detect_extension(value: &str) -> Option<ExtensionValue> {
245 if IpAddr::from_str(value).is_ok() {
247 return Some(ExtensionValue::IpAddr(value.to_string()));
248 }
249
250 if let Some((ip_part, prefix_part)) = value.split_once('/')
252 && let Ok(ip) = IpAddr::from_str(ip_part)
253 && let Ok(prefix_len) = prefix_part.parse::<u8>()
254 {
255 let max_prefix = if ip.is_ipv4() { 32 } else { 128 };
257 if prefix_len <= max_prefix {
258 return Some(ExtensionValue::IpAddr(value.to_string()));
259 }
260 }
261
262 if Self::is_datetime_format(value) {
265 return Some(ExtensionValue::DateTime(value.to_string()));
266 }
267
268 if Self::is_duration_format(value) {
270 return Some(ExtensionValue::Duration(value.to_string()));
271 }
272
273 if value.contains('.')
276 && !value.contains('e')
277 && !value.contains('E')
278 && !value.ends_with('.')
279 && value.chars().filter(|&c| c == '.').count() == 1
280 {
281 if value.parse::<f64>().is_ok() {
283 if let Some(dot_pos) = value.find('.') {
285 let before_dot = &value[..dot_pos];
286 let after_dot = &value[dot_pos + 1..];
287 let before_has_digit = before_dot.chars().any(|c| c.is_ascii_digit());
289 let before_valid =
290 if before_dot.is_empty() || before_dot == "+" || before_dot == "-" {
291 false } else {
293 let has_leading_sign =
295 before_dot.starts_with('+') || before_dot.starts_with('-');
296 let sign_count = before_dot
297 .chars()
298 .filter(|c| *c == '+' || *c == '-')
299 .count();
300 before_has_digit
301 && before_dot
302 .chars()
303 .all(|c| c.is_ascii_digit() || c == '+' || c == '-')
304 && (!has_leading_sign || sign_count == 1)
305 };
306 let after_ok = !after_dot.is_empty()
308 && after_dot.chars().all(|c| c.is_ascii_digit())
309 && after_dot.chars().any(|c| c.is_ascii_digit());
310 if before_valid && after_ok {
312 return Some(ExtensionValue::Decimal(value.to_string()));
313 }
314 }
315 }
316 }
317
318 None
319 }
320
321 fn is_datetime_format(value: &str) -> bool {
331 use chrono::{DateTime, NaiveDate};
332
333 if DateTime::parse_from_rfc3339(value).is_ok() {
335 return true;
336 }
337
338 if DateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%z").is_ok() {
340 return true;
341 }
342
343 if DateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%.f%z").is_ok() {
345 return true;
346 }
347
348 if NaiveDate::parse_from_str(value, "%Y-%m-%d").is_ok() {
350 return true;
351 }
352
353 false
354 }
355
356 fn is_duration_format(value: &str) -> bool {
366 use crate::context_data_api::entry::UnitRank;
367
368 if value.is_empty() {
369 return false;
370 }
371
372 let bytes = value.as_bytes();
373 let mut i = 0;
374
375 if bytes[i] == b'-' {
376 i += 1;
377 if i == bytes.len() {
378 return false;
379 }
380 }
381
382 let mut last_rank = UnitRank::Start;
383
384 while i < bytes.len() {
385 let start = i;
386 while i < bytes.len() && bytes[i].is_ascii_digit() {
387 i += 1;
388 }
389 if start == i {
390 return false;
391 }
392
393 let (current_rank, consumed) = match bytes.get(i) {
394 Some(b'd') if last_rank < UnitRank::Days => (UnitRank::Days, 1),
395 Some(b'h') if last_rank < UnitRank::Hours => (UnitRank::Hours, 1),
396 Some(b's') if last_rank < UnitRank::Seconds => (UnitRank::Seconds, 1),
397 Some(b'm') => {
398 if i + 1 < bytes.len() && bytes[i + 1] == b's' {
399 if last_rank < UnitRank::Millis {
400 (UnitRank::Millis, 2)
401 } else {
402 return false;
403 }
404 } else if last_rank < UnitRank::Minutes {
405 (UnitRank::Minutes, 1)
406 } else {
407 return false;
408 }
409 },
410 _ => return false,
411 };
412
413 last_rank = current_rank;
414 i += consumed;
415 }
416
417 true
418 }
419
420 #[must_use]
422 pub fn is_entity_reference(value: &Value) -> bool {
423 if let Value::Object(obj) = value {
424 obj.len() == 2
425 && obj.get("type").is_some_and(serde_json::Value::is_string)
426 && obj.get("id").is_some_and(serde_json::Value::is_string)
427 } else {
428 false
429 }
430 }
431
432 pub(super) fn parse_entity_reference(
434 value: &Value,
435 ) -> Result<EntityReference, ValueMappingError> {
436 if let Value::Object(obj) = value {
437 let entity_type = obj.get("type").and_then(|v| v.as_str()).ok_or_else(|| {
438 ValueMappingError::InvalidEntityReference {
439 reason: "missing or invalid 'type' field".to_string(),
440 }
441 })?;
442
443 let entity_id = obj.get("id").and_then(|v| v.as_str()).ok_or_else(|| {
444 ValueMappingError::InvalidEntityReference {
445 reason: "missing or invalid 'id' field".to_string(),
446 }
447 })?;
448
449 Ok(EntityReference {
450 entity_type: entity_type.to_string(),
451 entity_id: entity_id.to_string(),
452 })
453 } else {
454 Err(ValueMappingError::InvalidEntityReference {
455 reason: "expected object with 'type' and 'id' fields".to_string(),
456 })
457 }
458 }
459
460 #[must_use]
462 pub fn value_type_name(value: &Value) -> &'static str {
463 match value {
464 Value::Null => "null",
465 Value::Bool(_) => "bool",
466 Value::Number(_) => "number",
467 Value::String(_) => "string",
468 Value::Array(_) => "array",
469 Value::Object(_) => "object",
470 }
471 }
472
473 fn convert_value(
475 &self,
476 value: &Value,
477 ) -> Result<Option<RestrictedExpression>, ValueMappingError> {
478 let expr = match value {
479 Value::Null => return Err(ValueMappingError::NullNotSupported),
480 Value::Bool(b) => RestrictedExpression::new_bool(*b),
481 Value::Number(n) => Self::convert_number(n)?,
482 Value::String(s) => self.convert_string(s),
483 Value::Array(arr) => self.convert_array(arr)?,
484 Value::Object(obj) => return self.convert_object(value, obj),
485 };
486
487 Ok(Some(expr))
488 }
489
490 fn convert_number(n: &serde_json::Number) -> Result<RestrictedExpression, ValueMappingError> {
492 if let Some(i) = n.as_i64() {
493 Ok(RestrictedExpression::new_long(i))
494 } else if let Some(f) = n.as_f64() {
495 let decimal_str = format!("{f:.4}");
498 Ok(RestrictedExpression::new_decimal(decimal_str))
499 } else {
500 Err(ValueMappingError::NumberNotRepresentable {
501 value: n.to_string(),
502 })
503 }
504 }
505
506 fn convert_string(&self, s: &str) -> RestrictedExpression {
508 if self.auto_detect_extensions {
509 match Self::detect_extension(s) {
510 Some(ExtensionValue::IpAddr(ip)) => RestrictedExpression::new_ip(ip),
511 Some(ExtensionValue::Decimal(d)) => RestrictedExpression::new_decimal(d),
512 Some(ExtensionValue::DateTime(dt)) => RestrictedExpression::new_datetime(dt),
513 Some(ExtensionValue::Duration(dur)) => RestrictedExpression::new_duration(dur),
514 None => RestrictedExpression::new_string(s.to_string()),
515 }
516 } else {
517 RestrictedExpression::new_string(s.to_string())
518 }
519 }
520
521 fn convert_array(&self, arr: &[Value]) -> Result<RestrictedExpression, ValueMappingError> {
523 let mut exprs = Vec::with_capacity(arr.len());
524
525 for item in arr {
526 match self.convert_value(item)? {
527 Some(expr) => exprs.push(expr),
528 None => {
529 return Err(ValueMappingError::NullNotSupported);
530 },
531 }
532 }
533
534 Ok(RestrictedExpression::new_set(exprs))
535 }
536
537 fn convert_object(
539 &self,
540 value: &Value,
541 obj: &serde_json::Map<String, Value>,
542 ) -> Result<Option<RestrictedExpression>, ValueMappingError> {
543 if Self::is_entity_reference(value) {
545 return Self::convert_entity_reference(value);
546 }
547
548 if let Some(extn) = obj.get("__extn") {
550 return Self::convert_extension_marker(extn);
551 }
552
553 let mut fields = HashMap::with_capacity(obj.len());
555
556 for (key, val) in obj {
557 let expr = self.convert_value(val)?;
558 fields.insert(
559 key.clone(),
560 expr.expect("convert_value should always return Some"),
561 );
562 }
563
564 Ok(Some(RestrictedExpression::new_record(fields)?))
565 }
566
567 fn convert_entity_reference(
569 value: &Value,
570 ) -> Result<Option<RestrictedExpression>, ValueMappingError> {
571 let entity_ref = Self::parse_entity_reference(value)?;
572
573 let entity_type = EntityTypeName::from_str(&entity_ref.entity_type).map_err(|e| {
574 ValueMappingError::InvalidEntityReference {
575 reason: format!("invalid entity type '{}': {}", entity_ref.entity_type, e),
576 }
577 })?;
578
579 let entity_id = EntityId::from_str(&entity_ref.entity_id).map_err(|e| {
580 ValueMappingError::InvalidEntityReference {
581 reason: format!("invalid entity id '{}': {}", entity_ref.entity_id, e),
582 }
583 })?;
584
585 let uid = EntityUid::from_type_name_and_id(entity_type, entity_id);
586 Ok(Some(RestrictedExpression::new_entity_uid(uid)))
587 }
588
589 fn convert_extension_marker(
591 extn: &Value,
592 ) -> Result<Option<RestrictedExpression>, ValueMappingError> {
593 let extn_obj =
594 extn.as_object()
595 .ok_or_else(|| ValueMappingError::InvalidExtensionFormat {
596 extension_type: "__extn".to_string(),
597 value: extn.to_string(),
598 })?;
599
600 let fn_name = extn_obj.get("fn").and_then(|v| v.as_str()).ok_or_else(|| {
601 ValueMappingError::InvalidExtensionFormat {
602 extension_type: "__extn".to_string(),
603 value: format!(
604 "missing or invalid 'fn' field in {}",
605 serde_json::to_string(extn_obj).unwrap_or_default()
606 ),
607 }
608 })?;
609
610 let arg = extn_obj
611 .get("arg")
612 .and_then(|v| v.as_str())
613 .ok_or_else(|| ValueMappingError::InvalidExtensionFormat {
614 extension_type: fn_name.to_string(),
615 value: format!(
616 "missing or invalid 'arg' field in {}",
617 serde_json::to_string(extn_obj).unwrap_or_default()
618 ),
619 })?;
620
621 match fn_name {
622 "decimal" => Ok(Some(RestrictedExpression::new_decimal(arg))),
623 "ip" | "ipaddr" => Ok(Some(RestrictedExpression::new_ip(arg))),
624 "datetime" => Ok(Some(RestrictedExpression::new_datetime(arg))),
625 "duration" => Ok(Some(RestrictedExpression::new_duration(arg))),
626 _ => Err(ValueMappingError::InvalidExtensionFormat {
627 extension_type: fn_name.to_string(),
628 value: arg.to_string(),
629 }),
630 }
631 }
632
633 fn estimate_value_size(value: &Value) -> usize {
637 match value {
638 Value::Null => 4,
640 Value::Bool(_) => 5,
642 Value::Number(n) => n.to_string().len(),
644 Value::String(s) => s.len() + 2,
646 Value::Array(arr) => {
648 2 + arr
649 .iter()
650 .map(|v| Self::estimate_value_size(v) + 1) .sum::<usize>()
652 },
653 Value::Object(obj) => {
655 2 + obj
656 .iter()
657 .map(|(k, v)| k.len() + 3 + Self::estimate_value_size(v) + 1) .sum::<usize>()
659 },
660 }
661 }
662
663 fn normalize_cedar_json(value: &Value) -> Result<Value, ValueMappingError> {
665 match value {
666 Value::Object(obj) => {
667 if let Some(entity) = obj.get("__entity")
669 && let Some(entity_obj) = entity.as_object()
670 {
671 return Ok(serde_json::json!({
672 "type": entity_obj.get("type"),
673 "id": entity_obj.get("id")
674 }));
675 }
676
677 if obj.contains_key("__extn") {
680 return Ok(Value::Object(obj.clone()));
681 }
682
683 let mut normalized = Map::new();
685 for (key, val) in obj {
686 normalized.insert(key.clone(), Self::normalize_cedar_json(val)?);
687 }
688 Ok(Value::Object(normalized))
689 },
690 Value::Array(arr) => {
691 let normalized: Result<Vec<_>, _> =
692 arr.iter().map(Self::normalize_cedar_json).collect();
693 Ok(Value::Array(normalized?))
694 },
695 _ => Ok(value.clone()),
697 }
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704 use serde_json::json;
705 use test_utils::assert_eq;
706
707 #[test]
708 fn test_json_to_cedar_primitives() {
709 let mapper = CedarValueMapper::new();
710
711 let result = mapper.json_to_cedar(&json!(true));
713 assert!(result.is_ok());
714 assert!(result.unwrap().is_some());
715
716 let result = mapper.json_to_cedar(&json!(42));
718 assert!(result.is_ok());
719 assert!(result.unwrap().is_some());
720
721 let result = mapper.json_to_cedar(&json!("hello"));
723 assert!(result.is_ok());
724 assert!(result.unwrap().is_some());
725 }
726
727 #[test]
728 fn test_json_to_cedar_null_error() {
729 let mapper = CedarValueMapper::new();
730 let result = mapper.json_to_cedar(&json!(null));
731 assert!(
732 matches!(result, Err(ValueMappingError::NullNotSupported)),
733 "expected Err(ValueMappingError::NullNotSupported), got: {result:?}"
734 );
735 }
736
737 #[test]
738 fn test_json_to_cedar_collections() {
739 let mapper = CedarValueMapper::new();
740
741 let result = mapper.json_to_cedar(&json!([1, 2, 3]));
743 assert!(result.is_ok());
744 assert!(result.unwrap().is_some());
745
746 let result = mapper.json_to_cedar(&json!({"name": "Alice", "age": 30}));
748 assert!(result.is_ok());
749 assert!(result.unwrap().is_some());
750 }
751
752 #[test]
753 fn test_extension_detection_ipaddr() {
754 assert!(matches!(
756 CedarValueMapper::detect_extension("192.168.1.1"),
757 Some(ExtensionValue::IpAddr(_))
758 ));
759
760 assert!(matches!(
762 CedarValueMapper::detect_extension("::1"),
763 Some(ExtensionValue::IpAddr(_))
764 ));
765
766 assert!(matches!(
768 CedarValueMapper::detect_extension("10.0.0.0/8"),
769 Some(ExtensionValue::IpAddr(_))
770 ));
771 assert!(matches!(
772 CedarValueMapper::detect_extension("192.168.1.0/24"),
773 Some(ExtensionValue::IpAddr(_))
774 ));
775
776 assert!(matches!(
778 CedarValueMapper::detect_extension("fe80::/10"),
779 Some(ExtensionValue::IpAddr(_))
780 ));
781 assert!(matches!(
782 CedarValueMapper::detect_extension("2001:db8::/32"),
783 Some(ExtensionValue::IpAddr(_))
784 ));
785
786 assert!(CedarValueMapper::detect_extension("192.168.1.0/33").is_none());
788
789 assert!(CedarValueMapper::detect_extension("hello").is_none());
791 }
792
793 #[test]
794 fn test_extension_detection_decimal() {
795 assert!(matches!(
796 CedarValueMapper::detect_extension("3.14"),
797 Some(ExtensionValue::Decimal(_))
798 ));
799
800 assert!(
802 CedarValueMapper::detect_extension("42").is_none(),
803 "integer should not be detected as decimal"
804 );
805
806 assert!(
808 CedarValueMapper::detect_extension("1.2.3.4.5").is_none(),
809 "multiple dots should not be detected as decimal"
810 );
811
812 assert!(
814 CedarValueMapper::detect_extension(".5").is_none(),
815 "decimal without digits before dot should be rejected"
816 );
817 assert!(
818 CedarValueMapper::detect_extension("-.5").is_none(),
819 "decimal with only sign before dot should be rejected"
820 );
821 assert!(
822 CedarValueMapper::detect_extension("5.").is_none(),
823 "decimal with trailing dot should be rejected"
824 );
825 assert!(
826 CedarValueMapper::detect_extension("1e5").is_none(),
827 "scientific notation should be rejected"
828 );
829 assert!(
830 CedarValueMapper::detect_extension("1.2e-3").is_none(),
831 "scientific notation with decimal should be rejected"
832 );
833 }
834
835 #[test]
836 fn test_extension_detection_datetime() {
837 assert!(matches!(
839 CedarValueMapper::detect_extension("2024-10-15"),
840 Some(ExtensionValue::DateTime(_))
841 ));
842
843 assert!(matches!(
845 CedarValueMapper::detect_extension("2024-10-15T11:35:00Z"),
846 Some(ExtensionValue::DateTime(_))
847 ));
848
849 assert!(matches!(
851 CedarValueMapper::detect_extension("2024-10-15T11:35:00.000Z"),
852 Some(ExtensionValue::DateTime(_))
853 ));
854
855 assert!(matches!(
857 CedarValueMapper::detect_extension("2024-10-15T11:35:00+01:00"),
858 Some(ExtensionValue::DateTime(_))
859 ));
860
861 assert!(!matches!(
863 CedarValueMapper::detect_extension("not-a-date"),
864 Some(ExtensionValue::DateTime(_))
865 ));
866 }
867
868 #[test]
869 fn test_extension_detection_duration() {
870 assert!(matches!(
872 CedarValueMapper::detect_extension("2h30m"),
873 Some(ExtensionValue::Duration(_))
874 ));
875
876 assert!(matches!(
878 CedarValueMapper::detect_extension("-1d12h"),
879 Some(ExtensionValue::Duration(_))
880 ));
881
882 assert!(matches!(
884 CedarValueMapper::detect_extension("1h30m45s"),
885 Some(ExtensionValue::Duration(_))
886 ));
887
888 assert!(matches!(
890 CedarValueMapper::detect_extension("500ms"),
891 Some(ExtensionValue::Duration(_))
892 ));
893
894 assert!(matches!(
896 CedarValueMapper::detect_extension("1d"),
897 Some(ExtensionValue::Duration(_))
898 ));
899
900 assert!(!matches!(
902 CedarValueMapper::detect_extension("not-a-duration"),
903 Some(ExtensionValue::Duration(_))
904 ));
905 }
906
907 #[test]
908 fn test_json_to_cedar_with_auto_detect() {
909 let mapper = CedarValueMapper::new();
910
911 let result = mapper.json_to_cedar(&json!("192.168.1.1"));
913 assert!(result.is_ok());
914 }
915
916 #[test]
917 fn test_json_to_cedar_without_auto_detect() {
918 let mapper = CedarValueMapper::new_without_auto_detect();
919
920 let result = mapper.json_to_cedar(&json!("192.168.1.1"));
922 assert!(result.is_ok());
923 }
924
925 #[test]
926 fn test_is_entity_reference() {
927 assert!(CedarValueMapper::is_entity_reference(&json!({
928 "type": "User",
929 "id": "123"
930 })));
931
932 assert!(!CedarValueMapper::is_entity_reference(&json!({
934 "id": "123"
935 })));
936
937 assert!(!CedarValueMapper::is_entity_reference(&json!({
939 "type": "User",
940 "id": "123",
941 "extra": true
942 })));
943
944 assert!(!CedarValueMapper::is_entity_reference(&json!({
946 "type": 123,
947 "id": "123"
948 })));
949 }
950
951 #[test]
952 fn test_parse_entity_reference() {
953 let value = json!({"type": "User", "id": "alice"});
954 let result = CedarValueMapper::parse_entity_reference(&value);
955 assert!(result.is_ok());
956 let entity_ref = result.expect("should parse");
957 assert_eq!(entity_ref.entity_type, "User");
958 assert_eq!(entity_ref.entity_id, "alice");
959 }
960
961 #[test]
962 fn test_dot_notation_access() {
963 let data = json!({
964 "user": {
965 "profile": {
966 "name": "Alice",
967 "age": 30
968 }
969 }
970 });
971
972 let name = CedarValueMapper::get_nested(&data, "user.profile.name");
974 assert!(name.is_ok());
975 assert_eq!(name.unwrap(), &json!("Alice"));
976
977 let age = CedarValueMapper::get_nested(&data, "user.profile.age");
978 assert!(age.is_ok());
979 assert_eq!(age.unwrap(), &json!(30));
980
981 let missing = CedarValueMapper::get_nested(&data, "user.missing.field");
983 assert!(matches!(
984 missing,
985 Err(ValueMappingError::PathNotFound { .. })
986 ));
987 }
988
989 #[test]
990 fn test_dot_notation_array_access() {
991 let data = json!({
992 "items": ["a", "b", "c"]
993 });
994
995 let item = CedarValueMapper::get_nested(&data, "items.1");
996 assert!(item.is_ok());
997 assert_eq!(item.unwrap(), &json!("b"));
998 }
999
1000 #[test]
1001 fn test_set_nested() {
1002 let mut data = json!({});
1003
1004 CedarValueMapper::set_nested(&mut data, "user.profile.name", json!("Alice"))
1005 .expect("should set nested value");
1006
1007 assert_eq!(data, json!({"user": {"profile": {"name": "Alice"}}}));
1008 }
1009
1010 #[test]
1011 fn test_value_size_limit() {
1012 let mapper = CedarValueMapper::new().with_max_size(10);
1013
1014 let result = mapper.json_to_cedar(&json!("hi"));
1016 assert!(result.is_ok());
1017
1018 let result = mapper.json_to_cedar(&json!("this is a very long string"));
1020 assert!(matches!(
1021 result,
1022 Err(ValueMappingError::ValueTooLarge { .. })
1023 ));
1024 }
1025
1026 #[test]
1027 fn test_explicit_extension_marker() {
1028 let mapper = CedarValueMapper::new();
1029
1030 let decimal = json!({"__extn": {"fn": "decimal", "arg": "3.14159"}});
1032 let result = mapper.json_to_cedar(&decimal);
1033 assert!(result.is_ok(), "decimal extension should parse");
1034
1035 let ip = json!({"__extn": {"fn": "ip", "arg": "10.0.0.1"}});
1037 let result = mapper.json_to_cedar(&ip);
1038 assert!(result.is_ok(), "ip extension should parse");
1039
1040 let ip_cidr = json!({"__extn": {"fn": "ip", "arg": "192.168.0.0/16"}});
1042 let result = mapper.json_to_cedar(&ip_cidr);
1043 assert!(result.is_ok(), "ip CIDR extension should parse");
1044
1045 let datetime = json!({"__extn": {"fn": "datetime", "arg": "2024-10-15T11:35:00Z"}});
1047 let result = mapper.json_to_cedar(&datetime);
1048 assert!(result.is_ok(), "datetime extension should parse");
1049
1050 let duration = json!({"__extn": {"fn": "duration", "arg": "2h30m"}});
1052 let result = mapper.json_to_cedar(&duration);
1053 assert!(result.is_ok(), "duration extension should parse");
1054 }
1055
1056 #[test]
1057 fn test_json_to_cedar_with_type() {
1058 let mapper = CedarValueMapper::new();
1059
1060 let result = mapper.json_to_cedar_with_type(&json!("hello"));
1061 assert!(result.is_ok());
1062 let (_, cedar_type) = result.expect("should convert").expect("should have value");
1063 assert_eq!(cedar_type, CedarType::String);
1064
1065 let result = mapper.json_to_cedar_with_type(&json!(42));
1066 assert!(result.is_ok());
1067 let (_, cedar_type) = result.expect("should convert").expect("should have value");
1068 assert_eq!(cedar_type, CedarType::Long);
1069
1070 let result = mapper.json_to_cedar_with_type(&json!({"a": 1}));
1071 assert!(result.is_ok());
1072 let (_, cedar_type) = result.expect("should convert").expect("should have value");
1073 assert_eq!(cedar_type, CedarType::Record);
1074 }
1075
1076 #[test]
1077 fn test_nested_structures() {
1078 let mapper = CedarValueMapper::new();
1079
1080 let complex = json!({
1081 "user": {
1082 "name": "Alice",
1083 "roles": ["admin", "user"],
1084 "profile": {
1085 "age": 30,
1086 "verified": true
1087 }
1088 },
1089 "metadata": {
1090 "version": 1
1091 }
1092 });
1093
1094 let result = mapper.json_to_cedar(&complex);
1095 assert!(result.is_ok());
1096 assert!(result.unwrap().is_some());
1097 }
1098}