use crate::context_data_api::error::ValueMappingError;
use super::CedarType;
use cedar_policy::{EntityId, EntityTypeName, EntityUid, RestrictedExpression};
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::net::IpAddr;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq)]
pub(super) struct EntityReference {
pub entity_type: String,
pub entity_id: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExtensionValue {
IpAddr(String),
Decimal(String),
DateTime(String),
Duration(String),
}
#[derive(Debug, Clone)]
pub struct CedarValueMapper {
auto_detect_extensions: bool,
max_value_size: usize,
}
impl Default for CedarValueMapper {
fn default() -> Self {
Self::new()
}
}
impl CedarValueMapper {
#[must_use]
pub fn new() -> Self {
Self {
auto_detect_extensions: true,
max_value_size: 0,
}
}
#[must_use]
pub fn new_without_auto_detect() -> Self {
Self {
auto_detect_extensions: false,
max_value_size: 0,
}
}
#[must_use]
pub fn with_max_size(mut self, max_size: usize) -> Self {
self.max_value_size = max_size;
self
}
pub fn json_to_cedar(
&self,
value: &Value,
) -> Result<Option<RestrictedExpression>, ValueMappingError> {
if self.max_value_size > 0 {
let size = Self::estimate_value_size(value);
if size > self.max_value_size {
return Err(ValueMappingError::ValueTooLarge {
size,
limit: self.max_value_size,
});
}
}
self.convert_value(value)
}
pub fn json_to_cedar_with_type(
&self,
value: &Value,
) -> Result<Option<(RestrictedExpression, CedarType)>, ValueMappingError> {
let cedar_type = CedarType::from_value(value);
let expr = self.json_to_cedar(value)?;
Ok(expr.map(|e| (e, cedar_type)))
}
pub fn cedar_to_json(expr_json: &Value) -> Result<Value, ValueMappingError> {
Self::normalize_cedar_json(expr_json)
}
pub fn get_nested<'a>(value: &'a Value, path: &str) -> Result<&'a Value, ValueMappingError> {
if path.is_empty() {
return Ok(value);
}
let mut current = value;
for component in path.split('.') {
if component.is_empty() {
return Err(ValueMappingError::InvalidPath {
path: path.to_string(),
});
}
current = match current {
Value::Object(obj) => {
obj.get(component)
.ok_or_else(|| ValueMappingError::PathNotFound {
path: path.to_string(),
})?
},
Value::Array(arr) => {
let index: usize =
component
.parse()
.map_err(|_| ValueMappingError::PathNotFound {
path: path.to_string(),
})?;
arr.get(index)
.ok_or_else(|| ValueMappingError::PathNotFound {
path: path.to_string(),
})?
},
_ => {
return Err(ValueMappingError::PathNotFound {
path: path.to_string(),
});
},
};
}
Ok(current)
}
pub fn set_nested(
value: &mut Value,
path: &str,
new_value: Value,
) -> Result<(), ValueMappingError> {
if path.is_empty() {
*value = new_value;
return Ok(());
}
let components: Vec<&str> = path.split('.').collect();
let mut current = value;
for (i, component) in components.iter().enumerate() {
if component.is_empty() {
return Err(ValueMappingError::InvalidPath {
path: path.to_string(),
});
}
let is_last = i == components.len() - 1;
if is_last {
let Value::Object(obj) = current else {
return Err(ValueMappingError::TypeMismatch {
expected: "object".to_string(),
actual: Self::value_type_name(current).to_string(),
});
};
obj.insert((*component).to_string(), new_value);
return Ok(());
}
let Value::Object(obj) = current else {
return Err(ValueMappingError::TypeMismatch {
expected: "object".to_string(),
actual: Self::value_type_name(current).to_string(),
});
};
current = obj
.entry((*component).to_string())
.or_insert_with(|| Value::Object(Map::new()));
}
Ok(())
}
#[must_use]
pub fn detect_extension(value: &str) -> Option<ExtensionValue> {
if IpAddr::from_str(value).is_ok() {
return Some(ExtensionValue::IpAddr(value.to_string()));
}
if let Some((ip_part, prefix_part)) = value.split_once('/')
&& let Ok(ip) = IpAddr::from_str(ip_part)
&& let Ok(prefix_len) = prefix_part.parse::<u8>()
{
let max_prefix = if ip.is_ipv4() { 32 } else { 128 };
if prefix_len <= max_prefix {
return Some(ExtensionValue::IpAddr(value.to_string()));
}
}
if Self::is_datetime_format(value) {
return Some(ExtensionValue::DateTime(value.to_string()));
}
if Self::is_duration_format(value) {
return Some(ExtensionValue::Duration(value.to_string()));
}
if value.contains('.')
&& !value.contains('e')
&& !value.contains('E')
&& !value.ends_with('.')
&& value.chars().filter(|&c| c == '.').count() == 1
{
if value.parse::<f64>().is_ok() {
if let Some(dot_pos) = value.find('.') {
let before_dot = &value[..dot_pos];
let after_dot = &value[dot_pos + 1..];
let before_has_digit = before_dot.chars().any(|c| c.is_ascii_digit());
let before_valid =
if before_dot.is_empty() || before_dot == "+" || before_dot == "-" {
false } else {
let has_leading_sign =
before_dot.starts_with('+') || before_dot.starts_with('-');
let sign_count = before_dot
.chars()
.filter(|c| *c == '+' || *c == '-')
.count();
before_has_digit
&& before_dot
.chars()
.all(|c| c.is_ascii_digit() || c == '+' || c == '-')
&& (!has_leading_sign || sign_count == 1)
};
let after_ok = !after_dot.is_empty()
&& after_dot.chars().all(|c| c.is_ascii_digit())
&& after_dot.chars().any(|c| c.is_ascii_digit());
if before_valid && after_ok {
return Some(ExtensionValue::Decimal(value.to_string()));
}
}
}
}
None
}
fn is_datetime_format(value: &str) -> bool {
use chrono::{DateTime, NaiveDate};
if DateTime::parse_from_rfc3339(value).is_ok() {
return true;
}
if DateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%z").is_ok() {
return true;
}
if DateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%.f%z").is_ok() {
return true;
}
if NaiveDate::parse_from_str(value, "%Y-%m-%d").is_ok() {
return true;
}
false
}
fn is_duration_format(value: &str) -> bool {
use crate::context_data_api::entry::UnitRank;
if value.is_empty() {
return false;
}
let bytes = value.as_bytes();
let mut i = 0;
if bytes[i] == b'-' {
i += 1;
if i == bytes.len() {
return false;
}
}
let mut last_rank = UnitRank::Start;
while i < bytes.len() {
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if start == i {
return false;
}
let (current_rank, consumed) = match bytes.get(i) {
Some(b'd') if last_rank < UnitRank::Days => (UnitRank::Days, 1),
Some(b'h') if last_rank < UnitRank::Hours => (UnitRank::Hours, 1),
Some(b's') if last_rank < UnitRank::Seconds => (UnitRank::Seconds, 1),
Some(b'm') => {
if i + 1 < bytes.len() && bytes[i + 1] == b's' {
if last_rank < UnitRank::Millis {
(UnitRank::Millis, 2)
} else {
return false;
}
} else if last_rank < UnitRank::Minutes {
(UnitRank::Minutes, 1)
} else {
return false;
}
},
_ => return false,
};
last_rank = current_rank;
i += consumed;
}
true
}
#[must_use]
pub fn is_entity_reference(value: &Value) -> bool {
if let Value::Object(obj) = value {
obj.len() == 2
&& obj.get("type").is_some_and(serde_json::Value::is_string)
&& obj.get("id").is_some_and(serde_json::Value::is_string)
} else {
false
}
}
pub(super) fn parse_entity_reference(
value: &Value,
) -> Result<EntityReference, ValueMappingError> {
if let Value::Object(obj) = value {
let entity_type = obj.get("type").and_then(|v| v.as_str()).ok_or_else(|| {
ValueMappingError::InvalidEntityReference {
reason: "missing or invalid 'type' field".to_string(),
}
})?;
let entity_id = obj.get("id").and_then(|v| v.as_str()).ok_or_else(|| {
ValueMappingError::InvalidEntityReference {
reason: "missing or invalid 'id' field".to_string(),
}
})?;
Ok(EntityReference {
entity_type: entity_type.to_string(),
entity_id: entity_id.to_string(),
})
} else {
Err(ValueMappingError::InvalidEntityReference {
reason: "expected object with 'type' and 'id' fields".to_string(),
})
}
}
#[must_use]
pub fn value_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
fn convert_value(
&self,
value: &Value,
) -> Result<Option<RestrictedExpression>, ValueMappingError> {
let expr = match value {
Value::Null => return Err(ValueMappingError::NullNotSupported),
Value::Bool(b) => RestrictedExpression::new_bool(*b),
Value::Number(n) => Self::convert_number(n)?,
Value::String(s) => self.convert_string(s),
Value::Array(arr) => self.convert_array(arr)?,
Value::Object(obj) => return self.convert_object(value, obj),
};
Ok(Some(expr))
}
fn convert_number(n: &serde_json::Number) -> Result<RestrictedExpression, ValueMappingError> {
if let Some(i) = n.as_i64() {
Ok(RestrictedExpression::new_long(i))
} else if let Some(f) = n.as_f64() {
let decimal_str = format!("{f:.4}");
Ok(RestrictedExpression::new_decimal(decimal_str))
} else {
Err(ValueMappingError::NumberNotRepresentable {
value: n.to_string(),
})
}
}
fn convert_string(&self, s: &str) -> RestrictedExpression {
if self.auto_detect_extensions {
match Self::detect_extension(s) {
Some(ExtensionValue::IpAddr(ip)) => RestrictedExpression::new_ip(ip),
Some(ExtensionValue::Decimal(d)) => RestrictedExpression::new_decimal(d),
Some(ExtensionValue::DateTime(dt)) => RestrictedExpression::new_datetime(dt),
Some(ExtensionValue::Duration(dur)) => RestrictedExpression::new_duration(dur),
None => RestrictedExpression::new_string(s.to_string()),
}
} else {
RestrictedExpression::new_string(s.to_string())
}
}
fn convert_array(&self, arr: &[Value]) -> Result<RestrictedExpression, ValueMappingError> {
let mut exprs = Vec::with_capacity(arr.len());
for item in arr {
match self.convert_value(item)? {
Some(expr) => exprs.push(expr),
None => {
return Err(ValueMappingError::NullNotSupported);
},
}
}
Ok(RestrictedExpression::new_set(exprs))
}
fn convert_object(
&self,
value: &Value,
obj: &serde_json::Map<String, Value>,
) -> Result<Option<RestrictedExpression>, ValueMappingError> {
if Self::is_entity_reference(value) {
return Self::convert_entity_reference(value);
}
if let Some(extn) = obj.get("__extn") {
return Self::convert_extension_marker(extn);
}
let mut fields = HashMap::with_capacity(obj.len());
for (key, val) in obj {
let expr = self.convert_value(val)?;
fields.insert(
key.clone(),
expr.expect("convert_value should always return Some"),
);
}
Ok(Some(RestrictedExpression::new_record(fields)?))
}
fn convert_entity_reference(
value: &Value,
) -> Result<Option<RestrictedExpression>, ValueMappingError> {
let entity_ref = Self::parse_entity_reference(value)?;
let entity_type = EntityTypeName::from_str(&entity_ref.entity_type).map_err(|e| {
ValueMappingError::InvalidEntityReference {
reason: format!("invalid entity type '{}': {}", entity_ref.entity_type, e),
}
})?;
let entity_id = EntityId::from_str(&entity_ref.entity_id).map_err(|e| {
ValueMappingError::InvalidEntityReference {
reason: format!("invalid entity id '{}': {}", entity_ref.entity_id, e),
}
})?;
let uid = EntityUid::from_type_name_and_id(entity_type, entity_id);
Ok(Some(RestrictedExpression::new_entity_uid(uid)))
}
fn convert_extension_marker(
extn: &Value,
) -> Result<Option<RestrictedExpression>, ValueMappingError> {
let extn_obj =
extn.as_object()
.ok_or_else(|| ValueMappingError::InvalidExtensionFormat {
extension_type: "__extn".to_string(),
value: extn.to_string(),
})?;
let fn_name = extn_obj.get("fn").and_then(|v| v.as_str()).ok_or_else(|| {
ValueMappingError::InvalidExtensionFormat {
extension_type: "__extn".to_string(),
value: format!(
"missing or invalid 'fn' field in {}",
serde_json::to_string(extn_obj).unwrap_or_default()
),
}
})?;
let arg = extn_obj
.get("arg")
.and_then(|v| v.as_str())
.ok_or_else(|| ValueMappingError::InvalidExtensionFormat {
extension_type: fn_name.to_string(),
value: format!(
"missing or invalid 'arg' field in {}",
serde_json::to_string(extn_obj).unwrap_or_default()
),
})?;
match fn_name {
"decimal" => Ok(Some(RestrictedExpression::new_decimal(arg))),
"ip" | "ipaddr" => Ok(Some(RestrictedExpression::new_ip(arg))),
"datetime" => Ok(Some(RestrictedExpression::new_datetime(arg))),
"duration" => Ok(Some(RestrictedExpression::new_duration(arg))),
_ => Err(ValueMappingError::InvalidExtensionFormat {
extension_type: fn_name.to_string(),
value: arg.to_string(),
}),
}
}
fn estimate_value_size(value: &Value) -> usize {
match value {
Value::Null => 4,
Value::Bool(_) => 5,
Value::Number(n) => n.to_string().len(),
Value::String(s) => s.len() + 2,
Value::Array(arr) => {
2 + arr
.iter()
.map(|v| Self::estimate_value_size(v) + 1) .sum::<usize>()
},
Value::Object(obj) => {
2 + obj
.iter()
.map(|(k, v)| k.len() + 3 + Self::estimate_value_size(v) + 1) .sum::<usize>()
},
}
}
fn normalize_cedar_json(value: &Value) -> Result<Value, ValueMappingError> {
match value {
Value::Object(obj) => {
if let Some(entity) = obj.get("__entity")
&& let Some(entity_obj) = entity.as_object()
{
return Ok(serde_json::json!({
"type": entity_obj.get("type"),
"id": entity_obj.get("id")
}));
}
if obj.contains_key("__extn") {
return Ok(Value::Object(obj.clone()));
}
let mut normalized = Map::new();
for (key, val) in obj {
normalized.insert(key.clone(), Self::normalize_cedar_json(val)?);
}
Ok(Value::Object(normalized))
},
Value::Array(arr) => {
let normalized: Result<Vec<_>, _> =
arr.iter().map(Self::normalize_cedar_json).collect();
Ok(Value::Array(normalized?))
},
_ => Ok(value.clone()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use test_utils::assert_eq;
#[test]
fn test_json_to_cedar_primitives() {
let mapper = CedarValueMapper::new();
let result = mapper.json_to_cedar(&json!(true));
assert!(result.is_ok());
assert!(result.unwrap().is_some());
let result = mapper.json_to_cedar(&json!(42));
assert!(result.is_ok());
assert!(result.unwrap().is_some());
let result = mapper.json_to_cedar(&json!("hello"));
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
#[test]
fn test_json_to_cedar_null_error() {
let mapper = CedarValueMapper::new();
let result = mapper.json_to_cedar(&json!(null));
assert!(
matches!(result, Err(ValueMappingError::NullNotSupported)),
"expected Err(ValueMappingError::NullNotSupported), got: {result:?}"
);
}
#[test]
fn test_json_to_cedar_collections() {
let mapper = CedarValueMapper::new();
let result = mapper.json_to_cedar(&json!([1, 2, 3]));
assert!(result.is_ok());
assert!(result.unwrap().is_some());
let result = mapper.json_to_cedar(&json!({"name": "Alice", "age": 30}));
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
#[test]
fn test_extension_detection_ipaddr() {
assert!(matches!(
CedarValueMapper::detect_extension("192.168.1.1"),
Some(ExtensionValue::IpAddr(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("::1"),
Some(ExtensionValue::IpAddr(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("10.0.0.0/8"),
Some(ExtensionValue::IpAddr(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("192.168.1.0/24"),
Some(ExtensionValue::IpAddr(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("fe80::/10"),
Some(ExtensionValue::IpAddr(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("2001:db8::/32"),
Some(ExtensionValue::IpAddr(_))
));
assert!(CedarValueMapper::detect_extension("192.168.1.0/33").is_none());
assert!(CedarValueMapper::detect_extension("hello").is_none());
}
#[test]
fn test_extension_detection_decimal() {
assert!(matches!(
CedarValueMapper::detect_extension("3.14"),
Some(ExtensionValue::Decimal(_))
));
assert!(
CedarValueMapper::detect_extension("42").is_none(),
"integer should not be detected as decimal"
);
assert!(
CedarValueMapper::detect_extension("1.2.3.4.5").is_none(),
"multiple dots should not be detected as decimal"
);
assert!(
CedarValueMapper::detect_extension(".5").is_none(),
"decimal without digits before dot should be rejected"
);
assert!(
CedarValueMapper::detect_extension("-.5").is_none(),
"decimal with only sign before dot should be rejected"
);
assert!(
CedarValueMapper::detect_extension("5.").is_none(),
"decimal with trailing dot should be rejected"
);
assert!(
CedarValueMapper::detect_extension("1e5").is_none(),
"scientific notation should be rejected"
);
assert!(
CedarValueMapper::detect_extension("1.2e-3").is_none(),
"scientific notation with decimal should be rejected"
);
}
#[test]
fn test_extension_detection_datetime() {
assert!(matches!(
CedarValueMapper::detect_extension("2024-10-15"),
Some(ExtensionValue::DateTime(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("2024-10-15T11:35:00Z"),
Some(ExtensionValue::DateTime(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("2024-10-15T11:35:00.000Z"),
Some(ExtensionValue::DateTime(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("2024-10-15T11:35:00+01:00"),
Some(ExtensionValue::DateTime(_))
));
assert!(!matches!(
CedarValueMapper::detect_extension("not-a-date"),
Some(ExtensionValue::DateTime(_))
));
}
#[test]
fn test_extension_detection_duration() {
assert!(matches!(
CedarValueMapper::detect_extension("2h30m"),
Some(ExtensionValue::Duration(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("-1d12h"),
Some(ExtensionValue::Duration(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("1h30m45s"),
Some(ExtensionValue::Duration(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("500ms"),
Some(ExtensionValue::Duration(_))
));
assert!(matches!(
CedarValueMapper::detect_extension("1d"),
Some(ExtensionValue::Duration(_))
));
assert!(!matches!(
CedarValueMapper::detect_extension("not-a-duration"),
Some(ExtensionValue::Duration(_))
));
}
#[test]
fn test_json_to_cedar_with_auto_detect() {
let mapper = CedarValueMapper::new();
let result = mapper.json_to_cedar(&json!("192.168.1.1"));
assert!(result.is_ok());
}
#[test]
fn test_json_to_cedar_without_auto_detect() {
let mapper = CedarValueMapper::new_without_auto_detect();
let result = mapper.json_to_cedar(&json!("192.168.1.1"));
assert!(result.is_ok());
}
#[test]
fn test_is_entity_reference() {
assert!(CedarValueMapper::is_entity_reference(&json!({
"type": "User",
"id": "123"
})));
assert!(!CedarValueMapper::is_entity_reference(&json!({
"id": "123"
})));
assert!(!CedarValueMapper::is_entity_reference(&json!({
"type": "User",
"id": "123",
"extra": true
})));
assert!(!CedarValueMapper::is_entity_reference(&json!({
"type": 123,
"id": "123"
})));
}
#[test]
fn test_parse_entity_reference() {
let value = json!({"type": "User", "id": "alice"});
let result = CedarValueMapper::parse_entity_reference(&value);
assert!(result.is_ok());
let entity_ref = result.expect("should parse");
assert_eq!(entity_ref.entity_type, "User");
assert_eq!(entity_ref.entity_id, "alice");
}
#[test]
fn test_dot_notation_access() {
let data = json!({
"user": {
"profile": {
"name": "Alice",
"age": 30
}
}
});
let name = CedarValueMapper::get_nested(&data, "user.profile.name");
assert!(name.is_ok());
assert_eq!(name.unwrap(), &json!("Alice"));
let age = CedarValueMapper::get_nested(&data, "user.profile.age");
assert!(age.is_ok());
assert_eq!(age.unwrap(), &json!(30));
let missing = CedarValueMapper::get_nested(&data, "user.missing.field");
assert!(matches!(
missing,
Err(ValueMappingError::PathNotFound { .. })
));
}
#[test]
fn test_dot_notation_array_access() {
let data = json!({
"items": ["a", "b", "c"]
});
let item = CedarValueMapper::get_nested(&data, "items.1");
assert!(item.is_ok());
assert_eq!(item.unwrap(), &json!("b"));
}
#[test]
fn test_set_nested() {
let mut data = json!({});
CedarValueMapper::set_nested(&mut data, "user.profile.name", json!("Alice"))
.expect("should set nested value");
assert_eq!(data, json!({"user": {"profile": {"name": "Alice"}}}));
}
#[test]
fn test_value_size_limit() {
let mapper = CedarValueMapper::new().with_max_size(10);
let result = mapper.json_to_cedar(&json!("hi"));
assert!(result.is_ok());
let result = mapper.json_to_cedar(&json!("this is a very long string"));
assert!(matches!(
result,
Err(ValueMappingError::ValueTooLarge { .. })
));
}
#[test]
fn test_explicit_extension_marker() {
let mapper = CedarValueMapper::new();
let decimal = json!({"__extn": {"fn": "decimal", "arg": "3.14159"}});
let result = mapper.json_to_cedar(&decimal);
assert!(result.is_ok(), "decimal extension should parse");
let ip = json!({"__extn": {"fn": "ip", "arg": "10.0.0.1"}});
let result = mapper.json_to_cedar(&ip);
assert!(result.is_ok(), "ip extension should parse");
let ip_cidr = json!({"__extn": {"fn": "ip", "arg": "192.168.0.0/16"}});
let result = mapper.json_to_cedar(&ip_cidr);
assert!(result.is_ok(), "ip CIDR extension should parse");
let datetime = json!({"__extn": {"fn": "datetime", "arg": "2024-10-15T11:35:00Z"}});
let result = mapper.json_to_cedar(&datetime);
assert!(result.is_ok(), "datetime extension should parse");
let duration = json!({"__extn": {"fn": "duration", "arg": "2h30m"}});
let result = mapper.json_to_cedar(&duration);
assert!(result.is_ok(), "duration extension should parse");
}
#[test]
fn test_json_to_cedar_with_type() {
let mapper = CedarValueMapper::new();
let result = mapper.json_to_cedar_with_type(&json!("hello"));
assert!(result.is_ok());
let (_, cedar_type) = result.expect("should convert").expect("should have value");
assert_eq!(cedar_type, CedarType::String);
let result = mapper.json_to_cedar_with_type(&json!(42));
assert!(result.is_ok());
let (_, cedar_type) = result.expect("should convert").expect("should have value");
assert_eq!(cedar_type, CedarType::Long);
let result = mapper.json_to_cedar_with_type(&json!({"a": 1}));
assert!(result.is_ok());
let (_, cedar_type) = result.expect("should convert").expect("should have value");
assert_eq!(cedar_type, CedarType::Record);
}
#[test]
fn test_nested_structures() {
let mapper = CedarValueMapper::new();
let complex = json!({
"user": {
"name": "Alice",
"roles": ["admin", "user"],
"profile": {
"age": 30,
"verified": true
}
},
"metadata": {
"version": 1
}
});
let result = mapper.json_to_cedar(&complex);
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
}