datalogic-rs 5.0.0

A fast, type-safe Rust implementation of JSONLogic for evaluating logical rules as JSON. Perfect for business rules engines and dynamic filtering in Rust applications.
Documentation
//! Operator-specific compile-time specialisations.
//!
//! These convert the generic `CompiledNode::BuiltinOperator { opcode, args }`
//! form into specialised tree nodes that capture decisions at compile time:
//! - `var` / `val` → `CompiledVar` with pre-parsed segments and reduce hints.
//! - `exists` → `CompiledExists`.

use crate::node::PathSegment;
use crate::node::{CompileCtx, CompiledNode, MetadataHint, ReduceHint};

use super::path_segments::{parse_path_segments, parse_var_path};

/// Try to compile a `var` operator into a `CompiledVar` node.
pub(super) fn try_compile_var(args: &[CompiledNode], ctx: &mut CompileCtx) -> Option<CompiledNode> {
    if args.is_empty() {
        return Some(CompiledNode::Var {
            id: Some(ctx.next_id()),
            scope_level: 0,
            segments: Box::new([]),
            reduce_hint: ReduceHint::None,
            metadata_hint: MetadataHint::None,
            default_value: None,
        });
    }

    let (segments, reduce_hint) = match &args[0] {
        CompiledNode::Value {
            value: datavalue::OwnedDataValue::String(s),
            ..
        } => {
            let (hint, segs) = parse_var_path(s);
            (segs, hint)
        }
        CompiledNode::Value {
            value: datavalue::OwnedDataValue::Number(n),
            ..
        } => {
            let s = n.to_string();
            let segs = parse_path_segments(&s);
            (segs, ReduceHint::None)
        }
        _ => return None,
    };

    let default_value = if args.len() > 1 {
        Some(Box::new(args[1].clone()))
    } else {
        None
    };

    Some(CompiledNode::Var {
        id: Some(ctx.next_id()),
        scope_level: 0,
        segments: segments.into_boxed_slice(),
        reduce_hint,
        metadata_hint: MetadataHint::None,
        default_value,
    })
}

/// Try to compile a `val` operator into a `CompiledVar` node.
pub(super) fn try_compile_val(args: &[CompiledNode], ctx: &mut CompileCtx) -> Option<CompiledNode> {
    if args.is_empty() {
        return Some(CompiledNode::Var {
            id: Some(ctx.next_id()),
            scope_level: 0,
            segments: Box::new([]),
            reduce_hint: ReduceHint::None,
            metadata_hint: MetadataHint::None,
            default_value: None,
        });
    }

    if args.len() == 1 {
        return try_compile_val_single_arg(&args[0], ctx);
    }

    if let CompiledNode::Value {
        value: datavalue::OwnedDataValue::Array(level_arr),
        ..
    } = &args[0]
    {
        if let Some(datavalue::OwnedDataValue::Number(level_num)) = level_arr.first() {
            if let Some(level) = level_num.as_i64() {
                let scope_level = level.unsigned_abs() as u32;
                let metadata_hint = scope_level_metadata_hint(args);
                return try_compile_val_segments(&args[1..], scope_level, metadata_hint, ctx);
            }
        }
    }

    if let Some(first_seg) = val_arg_to_segment(&args[0]) {
        let reduce_hint = match &args[0] {
            CompiledNode::Value {
                value: datavalue::OwnedDataValue::String(s),
                ..
            } if s == "current" => ReduceHint::CurrentPath,
            CompiledNode::Value {
                value: datavalue::OwnedDataValue::String(s),
                ..
            } if s == "accumulator" => ReduceHint::AccumulatorPath,
            _ => ReduceHint::None,
        };

        let mut segments = vec![first_seg];
        if let Some(compiled) =
            try_collect_val_segments(&args[1..], &mut segments, reduce_hint, ctx)
        {
            return Some(compiled);
        }
    }

    None
}

fn try_compile_val_single_arg(arg: &CompiledNode, ctx: &mut CompileCtx) -> Option<CompiledNode> {
    let CompiledNode::Value {
        value: datavalue::OwnedDataValue::String(s),
        ..
    } = arg
    else {
        return None;
    };
    if s.is_empty() {
        return None;
    }
    let reduce_hint = if s == "current" {
        ReduceHint::Current
    } else if s == "accumulator" {
        ReduceHint::Accumulator
    } else {
        ReduceHint::None
    };
    let segment = if let Ok(idx) = s.parse::<usize>() {
        PathSegment::FieldOrIndex(s.as_str().into(), idx)
    } else {
        PathSegment::Field(s.as_str().into())
    };
    Some(CompiledNode::Var {
        id: Some(ctx.next_id()),
        scope_level: 0,
        segments: vec![segment].into_boxed_slice(),
        reduce_hint,
        metadata_hint: MetadataHint::None,
        default_value: None,
    })
}

fn scope_level_metadata_hint(args: &[CompiledNode]) -> MetadataHint {
    if args.len() == 2 {
        if let CompiledNode::Value {
            value: datavalue::OwnedDataValue::String(s),
            ..
        } = &args[1]
        {
            if s == "index" {
                return MetadataHint::Index;
            } else if s == "key" {
                return MetadataHint::Key;
            }
        }
    }
    MetadataHint::None
}

fn val_arg_to_segment(arg: &CompiledNode) -> Option<PathSegment> {
    match arg {
        CompiledNode::Value {
            value: datavalue::OwnedDataValue::String(s),
            ..
        } => {
            if let Ok(idx) = s.parse::<usize>() {
                Some(PathSegment::FieldOrIndex(s.as_str().into(), idx))
            } else {
                Some(PathSegment::Field(s.as_str().into()))
            }
        }
        CompiledNode::Value {
            value: datavalue::OwnedDataValue::Number(n),
            ..
        } => n
            .as_i64()
            .filter(|i| *i >= 0)
            .map(|i| PathSegment::Index(i as usize)),
        _ => None,
    }
}

fn try_compile_val_segments(
    args: &[CompiledNode],
    scope_level: u32,
    metadata_hint: MetadataHint,
    ctx: &mut CompileCtx,
) -> Option<CompiledNode> {
    let mut segments = Vec::new();
    for arg in args {
        segments.push(val_arg_to_segment(arg)?);
    }

    Some(CompiledNode::Var {
        id: Some(ctx.next_id()),
        scope_level,
        segments: segments.into_boxed_slice(),
        reduce_hint: ReduceHint::None,
        metadata_hint,
        default_value: None,
    })
}

fn try_collect_val_segments(
    args: &[CompiledNode],
    segments: &mut Vec<PathSegment>,
    reduce_hint: ReduceHint,
    ctx: &mut CompileCtx,
) -> Option<CompiledNode> {
    for arg in args {
        segments.push(val_arg_to_segment(arg)?);
    }

    Some(CompiledNode::Var {
        id: Some(ctx.next_id()),
        scope_level: 0,
        segments: std::mem::take(segments).into_boxed_slice(),
        reduce_hint,
        metadata_hint: MetadataHint::None,
        default_value: None,
    })
}

/// Try to compile an `exists` operator into a `CompiledExists` node.
#[cfg(feature = "ext-control")]
pub(super) fn try_compile_exists(
    args: &[CompiledNode],
    ctx: &mut CompileCtx,
) -> Option<CompiledNode> {
    if args.is_empty() {
        return Some(CompiledNode::Exists(Box::new(
            crate::node::CompiledExistsData {
                id: Some(ctx.next_id()),
                scope_level: 0,
                segments: Box::new([]),
            },
        )));
    }

    if args.len() == 1 {
        if let CompiledNode::Value {
            value: datavalue::OwnedDataValue::String(s),
            ..
        } = &args[0]
        {
            return Some(CompiledNode::Exists(Box::new(
                crate::node::CompiledExistsData {
                    id: Some(ctx.next_id()),
                    scope_level: 0,
                    segments: vec![PathSegment::Field(s.as_str().into())].into_boxed_slice(),
                },
            )));
        }
        return None;
    }

    let mut segments = Vec::new();
    for arg in args {
        if let CompiledNode::Value {
            value: datavalue::OwnedDataValue::String(s),
            ..
        } = arg
        {
            segments.push(PathSegment::Field(s.as_str().into()));
        } else {
            return None;
        }
    }

    Some(CompiledNode::Exists(Box::new(
        crate::node::CompiledExistsData {
            id: Some(ctx.next_id()),
            scope_level: 0,
            segments: segments.into_boxed_slice(),
        },
    )))
}