rulemorph 0.3.3

YAML-based declarative data transformation engine for CSV/JSON to JSON
Documentation
use crate::error::ErrorCode;
use crate::model::Expr;
use crate::path::{PathToken, parse_path};

use super::super::ValidationCtx;

pub(in crate::validator) fn validate_path_array_arg(
    expr: &Expr,
    base_path: &str,
    allow_terminal_index: bool,
    ctx: &mut ValidationCtx<'_>,
) {
    let value = match expr {
        Expr::Literal(value) => value,
        _ => return,
    };

    let mut items: Vec<(String, String)> = Vec::new();
    if let Some(path) = value.as_str() {
        items.push((base_path.to_string(), path.to_string()));
    } else if let Some(array) = value.as_array() {
        for (index, item) in array.iter().enumerate() {
            let item_path = format!("{}[{}]", base_path, index);
            let path = match item.as_str() {
                Some(path) => path,
                None => {
                    ctx.push(
                        ErrorCode::InvalidArgs,
                        "paths must be a string or array of strings",
                        item_path,
                    );
                    continue;
                }
            };
            items.push((item_path, path.to_string()));
        }
    } else {
        ctx.push(
            ErrorCode::InvalidArgs,
            "paths must be a string or array of strings",
            base_path,
        );
        return;
    }

    let mut paths: Vec<Vec<PathToken>> = Vec::new();
    for (item_path, path) in items {
        let tokens = match parse_path(&path) {
            Ok(tokens) => tokens,
            Err(_) => {
                ctx.push(
                    ErrorCode::InvalidArgs,
                    "paths must be valid path strings",
                    item_path,
                );
                continue;
            }
        };

        if !allow_terminal_index && matches!(tokens.last(), Some(PathToken::Index(_))) {
            ctx.push(
                ErrorCode::InvalidArgs,
                "path must not end with array index",
                item_path,
            );
            continue;
        }

        if paths.iter().any(|existing| existing == &tokens) {
            continue;
        }
        if has_path_conflict(&paths, &tokens) {
            ctx.push(
                ErrorCode::InvalidArgs,
                "path conflicts with another path",
                item_path,
            );
            continue;
        }
        paths.push(tokens);
    }
}

pub(in crate::validator) fn validate_path_arg(
    expr: &Expr,
    base_path: &str,
    ctx: &mut ValidationCtx<'_>,
) {
    let value = match expr {
        Expr::Literal(value) => value,
        _ => return,
    };

    let path = match value.as_str() {
        Some(path) => path,
        None => {
            ctx.push(ErrorCode::InvalidArgs, "path must be a string", base_path);
            return;
        }
    };

    if path.is_empty() {
        ctx.push(
            ErrorCode::InvalidArgs,
            "path must be a non-empty string",
            base_path,
        );
        return;
    }

    if parse_path(path).is_err() {
        ctx.push(
            ErrorCode::InvalidArgs,
            "path must be a valid path string",
            base_path,
        );
    }
}

fn has_path_conflict(paths: &[Vec<PathToken>], tokens: &[PathToken]) -> bool {
    paths
        .iter()
        .any(|existing| is_path_prefix(existing, tokens) || is_path_prefix(tokens, existing))
}

fn is_path_prefix(prefix: &[PathToken], tokens: &[PathToken]) -> bool {
    if prefix.len() > tokens.len() {
        return false;
    }
    prefix.iter().zip(tokens).all(|(left, right)| left == right)
}