rulemorph 0.3.4

YAML-based declarative data transformation engine for CSV/JSON to JSON
Documentation
use super::*;

pub(in crate::transform) fn eval_lookup(
    args: &[Expr],
    injected: Option<&EvalValue>,
    record: &JsonValue,
    context: Option<&JsonValue>,
    out: &JsonValue,
    base_path: &str,
    first_only: bool,
    locals: Option<&EvalLocals<'_>>,
    compiled_lookup: Option<&CompiledLookup>,
) -> Result<EvalValue, TransformError> {
    let total_len = args_len(args, injected);
    if !(3..=4).contains(&total_len) {
        return Err(TransformError::new(
            TransformErrorKind::ExprError,
            "lookup args must be [collection, key_path, match_value, output_path?]",
        )
        .with_path(format!("{}.args", base_path)));
    }

    let collection_path = format!("{}.args[0]", base_path);
    let collection_storage;
    let collection_array =
        if let (None, Some(compiled), Some(context)) = (injected, compiled_lookup, context) {
            if let Some(tokens) = compiled.context_collection_tokens(args) {
                match get_path(context, tokens) {
                    Some(JsonValue::Array(items)) => items.as_slice(),
                    Some(JsonValue::Null) => {
                        return Err(TransformError::new(
                            TransformErrorKind::ExprError,
                            "lookup collection must be an array",
                        )
                        .with_path(collection_path));
                    }
                    Some(_) => {
                        return Err(TransformError::new(
                            TransformErrorKind::ExprError,
                            "lookup collection must be an array",
                        )
                        .with_path(collection_path));
                    }
                    None => return Ok(EvalValue::Missing),
                }
            } else {
                collection_storage = eval_lookup_collection(
                    0,
                    args,
                    injected,
                    record,
                    Some(context),
                    out,
                    base_path,
                    locals,
                    &collection_path,
                )?;
                let Some(items) = collection_storage.as_ref() else {
                    return Ok(EvalValue::Missing);
                };
                items.as_slice()
            }
        } else {
            collection_storage = eval_lookup_collection(
                0,
                args,
                injected,
                record,
                context,
                out,
                base_path,
                locals,
                &collection_path,
            )?;
            let Some(items) = collection_storage.as_ref() else {
                return Ok(EvalValue::Missing);
            };
            items.as_slice()
        };

    let key_expr = arg_expr_at(1, args, injected).ok_or_else(|| {
        TransformError::new(
            TransformErrorKind::ExprError,
            "lookup key_path must be a non-empty string literal",
        )
        .with_path(format!("{}.args[1]", base_path))
    })?;
    let key_path = literal_string(key_expr).ok_or_else(|| {
        TransformError::new(
            TransformErrorKind::ExprError,
            "lookup key_path must be a non-empty string literal",
        )
        .with_path(format!("{}.args[1]", base_path))
    })?;
    if key_path.is_empty() {
        return Err(TransformError::new(
            TransformErrorKind::ExprError,
            "lookup key_path must be a non-empty string literal",
        )
        .with_path(format!("{}.args[1]", base_path)));
    }
    let key_tokens_storage;
    let key_tokens = if let (None, Some(compiled)) = (injected, compiled_lookup) {
        compiled.key_tokens(args)?
    } else {
        key_tokens_storage = parse_path(key_path).map_err(|_| {
            TransformError::new(TransformErrorKind::ExprError, "lookup key_path is invalid")
                .with_path(format!("{}.args[1]", base_path))
        })?;
        &key_tokens_storage
    };

    let output_tokens_storage;
    let output_tokens = if total_len == 4 {
        let output_expr = arg_expr_at(3, args, injected).ok_or_else(|| {
            TransformError::new(
                TransformErrorKind::ExprError,
                "lookup output_path must be a non-empty string literal",
            )
            .with_path(format!("{}.args[3]", base_path))
        })?;
        let value = literal_string(output_expr).ok_or_else(|| {
            TransformError::new(
                TransformErrorKind::ExprError,
                "lookup output_path must be a non-empty string literal",
            )
            .with_path(format!("{}.args[3]", base_path))
        })?;
        if value.is_empty() {
            return Err(TransformError::new(
                TransformErrorKind::ExprError,
                "lookup output_path must be a non-empty string literal",
            )
            .with_path(format!("{}.args[3]", base_path)));
        }
        if let (None, Some(compiled)) = (injected, compiled_lookup) {
            compiled.output_tokens(args).transpose()?
        } else {
            let tokens = parse_path(value).map_err(|_| {
                TransformError::new(
                    TransformErrorKind::ExprError,
                    "lookup output_path is invalid",
                )
                .with_path(format!("{}.args[3]", base_path))
            })?;
            output_tokens_storage = tokens;
            Some(output_tokens_storage.as_slice())
        }
    } else {
        None
    };

    let match_path = format!("{}.args[2]", base_path);
    let match_value =
        match eval_expr_at_index(2, args, injected, record, context, out, base_path, locals)? {
            EvalValue::Missing => return Ok(EvalValue::Missing),
            EvalValue::Value(value) => value,
        };
    if match_value.is_null() {
        return Err(TransformError::new(
            TransformErrorKind::ExprError,
            "lookup match_value must not be null",
        )
        .with_path(match_path));
    }
    let match_key = value_to_string(&match_value, &match_path)?;

    if let (None, Some(compiled)) = (injected, compiled_lookup) {
        if let Some(index) = compiled.index(args, collection_array, key_tokens, output_tokens) {
            let Some(matches) = index.get(&match_key) else {
                return Ok(EvalValue::Missing);
            };
            match matches {
                LookupMatches::ClonedValues(values) => {
                    if first_only {
                        return Ok(EvalValue::Value(values[0].clone()));
                    }
                    return Ok(EvalValue::Value(JsonValue::Array(values.to_vec())));
                }
                LookupMatches::ItemIndices(indices) => {
                    let mut results = Vec::new();
                    for &index in indices {
                        let Some(item) = collection_array.get(index) else {
                            continue;
                        };
                        let selected = match output_tokens {
                            Some(tokens) => get_path(item, tokens),
                            None => Some(item),
                        };
                        if let Some(value) = selected {
                            if first_only {
                                return Ok(EvalValue::Value(value.clone()));
                            }
                            results.push(value.clone());
                        }
                    }
                    if results.is_empty() {
                        return Ok(EvalValue::Missing);
                    }
                    return Ok(EvalValue::Value(JsonValue::Array(results)));
                }
            }
        }
    }

    let mut results = Vec::new();
    for item in collection_array {
        let key_value = match get_path(item, key_tokens) {
            Some(value) => value,
            None => continue,
        };
        if !value_matches_string_key(key_value, &match_key) {
            continue;
        }

        let selected = match output_tokens {
            Some(tokens) => get_path(item, tokens),
            None => Some(item),
        };

        if let Some(value) = selected {
            if first_only {
                return Ok(EvalValue::Value(value.clone()));
            }
            results.push(value.clone());
        }
    }

    if results.is_empty() {
        Ok(EvalValue::Missing)
    } else {
        Ok(EvalValue::Value(JsonValue::Array(results)))
    }
}

#[allow(clippy::too_many_arguments)]
fn eval_lookup_collection(
    index: usize,
    args: &[Expr],
    injected: Option<&EvalValue>,
    record: &JsonValue,
    context: Option<&JsonValue>,
    out: &JsonValue,
    base_path: &str,
    locals: Option<&EvalLocals<'_>>,
    collection_path: &str,
) -> Result<Option<Vec<JsonValue>>, TransformError> {
    let collection = match eval_expr_at_index(
        index, args, injected, record, context, out, base_path, locals,
    )? {
        EvalValue::Missing => return Ok(None),
        EvalValue::Value(value) => value,
    };
    match collection {
        JsonValue::Array(items) => Ok(Some(items)),
        JsonValue::Null => Err(TransformError::new(
            TransformErrorKind::ExprError,
            "lookup collection must be an array",
        )
        .with_path(collection_path)),
        _ => Err(TransformError::new(
            TransformErrorKind::ExprError,
            "lookup collection must be an array",
        )
        .with_path(collection_path)),
    }
}