Skip to main content

cedarling/context_data_api/
mapper.rs

1// This software is available under the Apache-2.0 license.
2// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text.
3//
4// Copyright (c) 2024, Gluu, Inc.
5
6//! Cedar Value Mapping
7//!
8//! Provides bidirectional conversion between JSON values and Cedar values,
9//! with support for all Cedar data types including extension types.
10
11use 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/// Represents a parsed Cedar entity reference.
21#[derive(Debug, Clone, PartialEq)]
22pub(super) struct EntityReference {
23    /// The entity type (e.g., "User", "`Namespace::Type`")
24    pub entity_type: String,
25    /// The entity identifier
26    pub entity_id: String,
27}
28
29/// Represents a detected extension type with its parsed value.
30#[derive(Debug, Clone, PartialEq)]
31pub enum ExtensionValue {
32    /// An IP address (IPv4 or IPv6) or CIDR range
33    IpAddr(String),
34    /// A fixed-precision decimal number (up to 4 decimal places)
35    Decimal(String),
36    /// An instant of time with millisecond precision (RFC 3339 / ISO 8601)
37    DateTime(String),
38    /// A duration of time with millisecond precision
39    Duration(String),
40}
41
42/// Mapper for bidirectional JSON ↔ Cedar value conversion.
43///
44/// Provides methods to convert between `serde_json::Value` and Cedar's
45/// `RestrictedExpression`, with support for all Cedar data types including
46/// extension types (IP addresses, decimals).
47#[derive(Debug, Clone)]
48pub struct CedarValueMapper {
49    /// Whether to auto-detect extension types from string patterns
50    auto_detect_extensions: bool,
51    /// Maximum allowed value size in bytes (0 = no limit)
52    max_value_size: usize,
53}
54
55impl Default for CedarValueMapper {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl CedarValueMapper {
62    /// Create a new mapper with default settings.
63    #[must_use]
64    pub fn new() -> Self {
65        Self {
66            auto_detect_extensions: true,
67            max_value_size: 0,
68        }
69    }
70
71    /// Create a mapper with auto-detection of extension types disabled.
72    #[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    /// Set the maximum allowed value size in bytes.
81    ///
82    /// A value of 0 means no limit.
83    #[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    /// Convert a JSON value to a Cedar `RestrictedExpression`.
90    ///
91    /// Supports all Cedar primitive types, collections, and extension types.
92    /// Null values are not supported and will return an error.
93    pub fn json_to_cedar(
94        &self,
95        value: &Value,
96    ) -> Result<Option<RestrictedExpression>, ValueMappingError> {
97        // Check size limit
98        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    /// Convert a JSON value to Cedar, returning the inferred Cedar type.
112    ///
113    /// This is useful when you need both the expression and type information.
114    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    /// Convert a Cedar expression back to JSON format.
124    ///
125    /// This is useful for serializing Cedar values for storage or transmission.
126    /// Entity references are converted to `{"type": "...", "id": "..."}` format.
127    /// Extension types are converted to `{"__extn": {"fn": "...", "arg": "..."}}` format.
128    ///
129    /// # Note
130    ///
131    /// This method works with the JSON representation of Cedar values,
132    /// not the evaluated result. For evaluated results, use `eval_result_to_json`.
133    pub fn cedar_to_json(expr_json: &Value) -> Result<Value, ValueMappingError> {
134        // Cedar's JSON format uses special markers for extension types and entities
135        // This method normalizes those to a consistent format
136        Self::normalize_cedar_json(expr_json)
137    }
138
139    /// Access a nested value using dot notation.
140    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                    // Support numeric indexing for arrays
162                    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    /// Set a value at a nested path, creating intermediate objects as needed.
185    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                // Set the value
209                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            // Navigate or create intermediate objects
220            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    /// Detect if a string value represents a Cedar extension type.
235    ///
236    /// Detects the following extension types:
237    /// - `ipaddr`: IP addresses (IPv4/IPv6) and CIDR ranges (e.g., "192.168.1.1", "10.0.0.0/8")
238    /// - `decimal`: Fixed-precision decimals (e.g., "3.14", "-12.345")
239    /// - `datetime`: ISO 8601 / RFC 3339 timestamps (e.g., "2024-10-15T11:35:00Z")
240    /// - `duration`: Duration strings (e.g., "2h30m", "1d12h", "500ms")
241    ///
242    /// See: <https://docs.cedarpolicy.com/policies/syntax-datatypes.html#datatype-extension>
243    #[must_use]
244    pub fn detect_extension(value: &str) -> Option<ExtensionValue> {
245        // Check for plain IP address (IPv4 or IPv6)
246        if IpAddr::from_str(value).is_ok() {
247            return Some(ExtensionValue::IpAddr(value.to_string()));
248        }
249
250        // Check for CIDR notation (e.g., "192.168.1.0/24", "fe80::/10")
251        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            // Validate prefix length: 0-32 for IPv4, 0-128 for IPv6
256            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        // Check for datetime (ISO 8601 / RFC 3339 format)
263        // Examples: "2024-10-15", "2024-10-15T11:35:00Z", "2024-10-15T11:35:00.000+0100"
264        if Self::is_datetime_format(value) {
265            return Some(ExtensionValue::DateTime(value.to_string()));
266        }
267
268        // Check for duration format (e.g., "2h30m", "-1d12h", "500ms")
269        if Self::is_duration_format(value) {
270            return Some(ExtensionValue::Duration(value.to_string()));
271        }
272
273        // Check for decimal (must contain decimal point and be parseable as f64)
274        // Must have exactly one decimal point, not end with it, and reject exponent notation
275        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            // Parse as f64 to validate numeric format, but ensure it's fixed-point
282            if value.parse::<f64>().is_ok() {
283                // Additional check: ensure there's at least one digit before and after the decimal point
284                if let Some(dot_pos) = value.find('.') {
285                    let before_dot = &value[..dot_pos];
286                    let after_dot = &value[dot_pos + 1..];
287                    // Require at least one ASCII digit in before_dot (optionally preceded by a single '+' or '-')
288                    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 // Must have at least one digit, not just sign or empty
292                        } else {
293                            // Must have at least one digit, and all chars are digits or a single leading sign
294                            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                    // Require at least one ASCII digit in after_dot, and all chars are digits
307                    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                    // Both sides must have digits
311                    if before_valid && after_ok {
312                        return Some(ExtensionValue::Decimal(value.to_string()));
313                    }
314                }
315            }
316        }
317
318        None
319    }
320
321    /// Check if a string looks like an ISO 8601 / RFC 3339 datetime.
322    ///
323    /// Uses real parsing to validate semantic correctness, not just byte patterns.
324    /// Supported formats:
325    /// - "2024-10-15" (date only)
326    /// - "2024-10-15T11:35:00Z" (UTC)
327    /// - "2024-10-15T11:35:00.000Z" (UTC with milliseconds)
328    /// - "2024-10-15T11:35:00+0100" (with timezone offset)
329    /// - "2024-10-15T11:35:00.000+0100" (with timezone and milliseconds)
330    fn is_datetime_format(value: &str) -> bool {
331        use chrono::{DateTime, NaiveDate};
332
333        // Try RFC 3339 parsing first (handles full datetime with timezone)
334        if DateTime::parse_from_rfc3339(value).is_ok() {
335            return true;
336        }
337
338        // Try ISO 8601 format with offset without colon (e.g., "+0100")
339        if DateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%z").is_ok() {
340            return true;
341        }
342
343        // Try ISO 8601 format with offset and fractional seconds
344        if DateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%.f%z").is_ok() {
345            return true;
346        }
347
348        // Try date-only format (YYYY-MM-DD)
349        if NaiveDate::parse_from_str(value, "%Y-%m-%d").is_ok() {
350            return true;
351        }
352
353        false
354    }
355
356    /// Check if a string looks like a Cedar duration format.
357    ///
358    /// Enforces unit ordering: d > h > m > s > ms (descending rank order).
359    /// Supported formats:
360    /// - "2h30m" (hours and minutes)
361    /// - "-1d12h" (negative, days and hours)
362    /// - "1h30m45s" (hours, minutes, seconds)
363    /// - "500ms" (milliseconds only)
364    /// - "1d" (days only)
365    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    /// Check if a value represents a Cedar entity reference.
421    #[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    /// Parse an entity reference from JSON.
433    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    /// Get the JSON type name of a value.
461    #[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    // Internal conversion method
474    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    /// Convert a JSON number to a Cedar expression.
491    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            // Convert floating point to decimal extension
496            // Format to 4 decimal places to avoid scientific notation and ensure Cedar compatibility
497            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    /// Convert a JSON string to a Cedar expression.
507    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    /// Convert a JSON array to a Cedar set expression.
522    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    /// Convert a JSON object to a Cedar expression (entity, extension, or record).
538    fn convert_object(
539        &self,
540        value: &Value,
541        obj: &serde_json::Map<String, Value>,
542    ) -> Result<Option<RestrictedExpression>, ValueMappingError> {
543        // Check for entity reference
544        if Self::is_entity_reference(value) {
545            return Self::convert_entity_reference(value);
546        }
547
548        // Check for extension type markers (__extn)
549        if let Some(extn) = obj.get("__extn") {
550            return Self::convert_extension_marker(extn);
551        }
552
553        // Regular record
554        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    /// Convert an entity reference to a Cedar entity UID expression.
568    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    /// Convert an extension marker (__extn) to a Cedar extension expression.
590    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    /// Estimates the JSON-serialized size of a value in bytes.
634    ///
635    /// This provides a rough estimate for size limiting without actual serialization.
636    fn estimate_value_size(value: &Value) -> usize {
637        match value {
638            // "null" = 4 chars
639            Value::Null => 4,
640            // "true" or "false" = 4-5 chars, use max
641            Value::Bool(_) => 5,
642            // Number as string representation
643            Value::Number(n) => n.to_string().len(),
644            // String content + 2 for surrounding quotes
645            Value::String(s) => s.len() + 2,
646            // 2 for brackets [] + elements with commas
647            Value::Array(arr) => {
648                2 + arr
649                    .iter()
650                    .map(|v| Self::estimate_value_size(v) + 1) // +1 for comma separator
651                    .sum::<usize>()
652            },
653            // 2 for braces {} + key-value pairs
654            Value::Object(obj) => {
655                2 + obj
656                    .iter()
657                    .map(|(k, v)| k.len() + 3 + Self::estimate_value_size(v) + 1) // +3 for quotes and colon, +1 for comma
658                    .sum::<usize>()
659            },
660        }
661    }
662
663    // Normalize Cedar JSON format to standard JSON
664    fn normalize_cedar_json(value: &Value) -> Result<Value, ValueMappingError> {
665        match value {
666            Value::Object(obj) => {
667                // Check for __entity marker (Cedar format for entity references)
668                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                // Check for __extn marker (extension types)
678                // Preserve the entire wrapper so json_to_cedar can consume it
679                if obj.contains_key("__extn") {
680                    return Ok(Value::Object(obj.clone()));
681                }
682
683                // Regular object - recursively process
684                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            // Primitives pass through unchanged
696            _ => 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        // Boolean
712        let result = mapper.json_to_cedar(&json!(true));
713        assert!(result.is_ok());
714        assert!(result.unwrap().is_some());
715
716        // Long
717        let result = mapper.json_to_cedar(&json!(42));
718        assert!(result.is_ok());
719        assert!(result.unwrap().is_some());
720
721        // String
722        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        // Set (array)
742        let result = mapper.json_to_cedar(&json!([1, 2, 3]));
743        assert!(result.is_ok());
744        assert!(result.unwrap().is_some());
745
746        // Record (object)
747        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        // IPv4
755        assert!(matches!(
756            CedarValueMapper::detect_extension("192.168.1.1"),
757            Some(ExtensionValue::IpAddr(_))
758        ));
759
760        // IPv6
761        assert!(matches!(
762            CedarValueMapper::detect_extension("::1"),
763            Some(ExtensionValue::IpAddr(_))
764        ));
765
766        // IPv4 CIDR notation
767        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        // IPv6 CIDR notation
777        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        // Invalid CIDR prefix (too large for IPv4)
787        assert!(CedarValueMapper::detect_extension("192.168.1.0/33").is_none());
788
789        // Not an IP
790        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        // Integer is not decimal
801        assert!(
802            CedarValueMapper::detect_extension("42").is_none(),
803            "integer should not be detected as decimal"
804        );
805
806        // Multiple dots is not decimal (would be detected as IP first if valid)
807        assert!(
808            CedarValueMapper::detect_extension("1.2.3.4.5").is_none(),
809            "multiple dots should not be detected as decimal"
810        );
811
812        // Negative cases: should reject invalid decimal formats
813        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        // Date only
838        assert!(matches!(
839            CedarValueMapper::detect_extension("2024-10-15"),
840            Some(ExtensionValue::DateTime(_))
841        ));
842
843        // UTC datetime
844        assert!(matches!(
845            CedarValueMapper::detect_extension("2024-10-15T11:35:00Z"),
846            Some(ExtensionValue::DateTime(_))
847        ));
848
849        // UTC with milliseconds
850        assert!(matches!(
851            CedarValueMapper::detect_extension("2024-10-15T11:35:00.000Z"),
852            Some(ExtensionValue::DateTime(_))
853        ));
854
855        // With timezone offset (RFC3339 requires colon in timezone)
856        assert!(matches!(
857            CedarValueMapper::detect_extension("2024-10-15T11:35:00+01:00"),
858            Some(ExtensionValue::DateTime(_))
859        ));
860
861        // Invalid datetime
862        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        // Hours and minutes
871        assert!(matches!(
872            CedarValueMapper::detect_extension("2h30m"),
873            Some(ExtensionValue::Duration(_))
874        ));
875
876        // Negative duration
877        assert!(matches!(
878            CedarValueMapper::detect_extension("-1d12h"),
879            Some(ExtensionValue::Duration(_))
880        ));
881
882        // Hours, minutes, seconds
883        assert!(matches!(
884            CedarValueMapper::detect_extension("1h30m45s"),
885            Some(ExtensionValue::Duration(_))
886        ));
887
888        // Milliseconds only
889        assert!(matches!(
890            CedarValueMapper::detect_extension("500ms"),
891            Some(ExtensionValue::Duration(_))
892        ));
893
894        // Days only
895        assert!(matches!(
896            CedarValueMapper::detect_extension("1d"),
897            Some(ExtensionValue::Duration(_))
898        ));
899
900        // Invalid duration
901        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        // IP address should be detected
912        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        // IP address should be treated as string
921        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        // Missing type
933        assert!(!CedarValueMapper::is_entity_reference(&json!({
934            "id": "123"
935        })));
936
937        // Extra field
938        assert!(!CedarValueMapper::is_entity_reference(&json!({
939            "type": "User",
940            "id": "123",
941            "extra": true
942        })));
943
944        // Wrong types
945        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        // Valid paths
973        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        // Invalid path
982        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        // Small value should pass
1015        let result = mapper.json_to_cedar(&json!("hi"));
1016        assert!(result.is_ok());
1017
1018        // Large value should fail
1019        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        // Decimal with explicit marker
1031        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        // IP with explicit marker
1036        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        // IP CIDR with explicit marker
1041        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        // Datetime with explicit marker
1046        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        // Duration with explicit marker
1051        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}