json-eval-rs 0.0.83

High-performance JSON Logic evaluator with schema validation and dependency tracking. Built on blazing-fast Rust engine.
Documentation
use serde_json::Value;
use std::borrow::Cow;

/// Normalize path to JSON pointer format for efficient native access
///
/// Handles various input formats:
/// - JSON Schema refs: #/$params/constants/DEATH_SA -> /$params/constants/DEATH_SA
/// - Dotted paths: user.name -> /user/name
/// - Already normalized paths (no-op)
/// - Simple field names: field -> /field
///
/// Returns `Cow::Borrowed` for already-normalized paths to avoid heap allocation.
#[inline]
pub fn normalize_to_json_pointer(path: &str) -> Cow<'_, str> {
    if path.is_empty() {
        return Cow::Borrowed("");
    }

    if path.starts_with("#/") {
        let stripped = &path[1..];
        if !stripped.contains("//") {
            return Cow::Borrowed(stripped);
        }
    }

    if path.starts_with('/') && !path.contains("//") {
        return if path == "/" {
            Cow::Borrowed("")
        } else {
            Cow::Borrowed(path)
        };
    }

    let mut normalized = String::with_capacity(path.len() + 1);
    let source = if path.starts_with("#/") {
        &path[1..]
    } else if !path.starts_with('/') {
        normalized.push('/');
        path
    } else {
        path
    };

    let mut prev_slash = normalized.ends_with('/');
    for ch in source.chars() {
        let c = if ch == '.' && !path.starts_with('/') && !path.starts_with('#') {
            '/'
        } else {
            ch
        };
        if c == '/' {
            if !prev_slash {
                normalized.push('/');
            }
            prev_slash = true;
        } else {
            normalized.push(c);
            prev_slash = false;
        }
    }

    if normalized == "/" {
        Cow::Borrowed("")
    } else {
        Cow::Owned(normalized)
    }
}

/// Convert dotted path to JSON Schema pointer format
///
/// This is used for schema paths where properties are nested under `/properties/`
///
/// Examples:
/// - "illustration.insured.name" -> "#/illustration/properties/insured/properties/name"
/// - "header.form_number" -> "#/header/properties/form_number"
/// - "#/already/formatted" -> "#/already/formatted" (no change)
#[inline]
pub fn dot_notation_to_schema_pointer(path: &str) -> String {
    // If already a JSON pointer (starts with # or /), return as-is
    if path.starts_with('#') || path.starts_with('/') {
        return path.to_string();
    }

    // Split by dots and join with /properties/
    let parts: Vec<&str> = path.split('.').collect();
    if parts.is_empty() {
        return "#/".to_string();
    }

    // Build schema path: #/part1/properties/part2/properties/part3
    // First part is root-level field, rest are under /properties/
    // Don't add /properties/ if path starts with $ (direct JSON pointer)
    let mut result = String::from("#");
    for (i, part) in parts.iter().enumerate() {
        if part.eq(&"properties") {
            continue;
        }

        if i > 0 && !path.starts_with('$') {
            result.push_str("/properties");
        }
        result.push_str("/");
        result.push_str(part);
    }

    result
}

/// Convert JSON pointer or schema pointer to dotted notation
///
/// This converts various pointer formats back to dotted notation:
///
/// Examples:
/// - "#/illustration/properties/insured/properties/ins_corrname" -> "illustration.properties.insured.properties.ins_corrname"
/// - "/user/name" -> "user.name"
/// - "person.name" -> "person.name" (already dotted, no change)
#[inline]
pub fn pointer_to_dot_notation(path: &str) -> String {
    if path.is_empty() {
        return String::new();
    }

    // If already dotted notation (no # or / prefix), return as-is
    if !path.starts_with('#') && !path.starts_with('/') {
        return path.to_string();
    }

    // Remove leading # or /
    let clean_path = if path.starts_with("#/") {
        &path[2..]
    } else if path.starts_with('/') {
        &path[1..]
    } else if path.starts_with('#') {
        &path[1..]
    } else {
        path
    };

    // Convert slashes to dots
    clean_path.replace('/', ".")
}

/// Canonicalize a path for schema lookups.
///
/// This performs a single-pass conversion that:
/// 1. Normalizes the path to a JSON pointer (starts with /).
/// 2. Injects `/properties/` segments for data paths (e.g., `a.b.c` -> `/a/properties/b/properties/c`).
/// 3. Preserves system paths starting with `$` (e.g., `/$params` -> `/$params`).
/// 4. Handles existing JSON pointers/schema refs by re-canonicalizing them.
///
/// Returns `Cow::Borrowed` if the path is already canonical.
pub fn canonicalize_schema_path(path: &str) -> Cow<'_, str> {
    if path.is_empty() {
        return Cow::Borrowed("");
    }

    // Fast check for already normalized system paths
    if path.starts_with("/$") && !path.contains('.') && !path.contains("//") {
        return Cow::Borrowed(path);
    }

    // Identify system paths early
    let is_system = path.starts_with('$') || path.starts_with("/$") || path.starts_with("#/$");

    // Clean prefix and detect if we need to do work
    let clean_path = if path.starts_with("#/") {
        &path[2..]
    } else if path.starts_with('/') {
        &path[1..]
    } else if path.starts_with('#') {
        &path[1..]
    } else {
        path
    };

    // If it's a simple top-level field with no dots/slashes, and not system,
    // we can just prepend / and return borrowed if it was already /field
    if !is_system
        && !clean_path.contains('.')
        && !clean_path.contains('/')
        && !clean_path.is_empty()
    {
        if path.starts_with('/') && path.len() == clean_path.len() + 1 {
            return Cow::Borrowed(path);
        }
        let mut s = String::with_capacity(clean_path.len() + 1);
        s.push('/');
        s.push_str(clean_path);
        return Cow::Owned(s);
    }

    // Full decomposition and reconstruction
    let mut result = String::with_capacity(path.len() * 2);
    result.push('/');

    let parts = clean_path.split(|c| c == '/' || c == '.');
    let mut first = true;

    for part in parts {
        if part.is_empty() || part == "properties" {
            continue;
        }

        if !first && !is_system {
            result.push_str("properties/");
        }
        result.push_str(part);
        result.push('/');
        first = false;
    }

    if result.len() > 1 {
        result.pop(); // Remove trailing slash
    }

    // If result matches original exactly, return borrowed
    if result == path {
        Cow::Borrowed(path)
    } else {
        Cow::Owned(result)
    }
}

/// Fast JSON pointer-based value access using serde's native implementation
///
/// This is significantly faster than manual path traversal for deeply nested objects
#[inline]
pub fn get_value_by_pointer<'a>(data: &'a Value, pointer: &str) -> Option<&'a Value> {
    if pointer.is_empty() {
        Some(data)
    } else {
        data.pointer(pointer)
    }
}

#[inline]
pub fn get_value_by_pointer_without_properties<'a>(
    data: &'a Value,
    pointer: &str,
) -> Option<&'a Value> {
    if pointer.is_empty() {
        Some(data)
    } else {
        data.pointer(&pointer.replace("properties/", ""))
    }
}

/// Batch pointer resolution for multiple paths
pub fn get_values_by_pointers<'a>(data: &'a Value, pointers: &[String]) -> Vec<Option<&'a Value>> {
    pointers
        .iter()
        .map(|pointer| get_value_by_pointer(data, pointer))
        .collect()
}

/// Fast array indexing helper for JSON arrays
///
/// Returns None if not an array or index out of bounds
#[inline]
pub fn get_array_element<'a>(data: &'a Value, index: usize) -> Option<&'a Value> {
    data.as_array()?.get(index)
}

/// Fast array indexing with JSON pointer path
///
/// Example: get_array_element_by_pointer(data, "/$params/tables", 0)
#[inline]
pub fn get_array_element_by_pointer<'a>(
    data: &'a Value,
    pointer: &str,
    index: usize,
) -> Option<&'a Value> {
    get_value_by_pointer(data, pointer)?.as_array()?.get(index)
}

/// Extract table metadata for fast array operations during schema parsing
#[derive(Debug, Clone)]
pub struct ArrayMetadata {
    /// Pointer to the array location
    pub pointer: String,
    /// Array length (cached for fast bounds checking)
    pub length: usize,
    /// Column names for object arrays (cached for fast field access)
    pub column_names: Vec<String>,
    /// Whether this is a uniform object array (all elements have same structure)
    pub is_uniform: bool,
}

impl ArrayMetadata {
    /// Build metadata for an array at the given pointer
    pub fn build(data: &Value, pointer: &str) -> Option<Self> {
        let array = get_value_by_pointer(data, pointer)?.as_array()?;

        let length = array.len();
        if length == 0 {
            return Some(ArrayMetadata {
                pointer: pointer.to_string(),
                length: 0,
                column_names: Vec::new(),
                is_uniform: true,
            });
        }

        // Analyze first element to determine structure
        let first_element = &array[0];
        let column_names = if let Value::Object(obj) = first_element {
            obj.keys().cloned().collect()
        } else {
            Vec::new()
        };

        // Check if all elements have the same structure (uniform array)
        let is_uniform = if !column_names.is_empty() {
            array.iter().all(|elem| {
                if let Value::Object(obj) = elem {
                    obj.keys().len() == column_names.len()
                        && column_names.iter().all(|col| obj.contains_key(col))
                } else {
                    false
                }
            })
        } else {
            // Non-object arrays are considered uniform if all elements have same type
            let first_type = std::mem::discriminant(first_element);
            array
                .iter()
                .all(|elem| std::mem::discriminant(elem) == first_type)
        };

        Some(ArrayMetadata {
            pointer: pointer.to_string(),
            length,
            column_names,
            is_uniform,
        })
    }

    /// Fast column access for uniform object arrays
    #[inline]
    pub fn get_column_value<'a>(
        &self,
        data: &'a Value,
        row_index: usize,
        column: &str,
    ) -> Option<&'a Value> {
        if !self.is_uniform || row_index >= self.length {
            return None;
        }

        get_array_element_by_pointer(data, &self.pointer, row_index)?
            .as_object()?
            .get(column)
    }

    /// Fast bounds checking
    #[inline]
    pub fn is_valid_index(&self, index: usize) -> bool {
        index < self.length
    }
}