rulemorph 0.3.4

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

pub(super) fn apply_mappings_traced(
    rule: &RuleFile,
    record: &JsonValue,
    context: Option<&JsonValue>,
    warnings: &mut Vec<TransformWarning>,
    limits: EvalLimits,
    base_v2_ctx: &V2EvalContext<'_>,
    collector: &mut TraceCollector,
) -> Result<JsonValue, TransformError> {
    let mut out = JsonValue::Object(Map::new());
    apply_mappings_into_traced(
        rule,
        &rule.mappings,
        record,
        context,
        &mut out,
        warnings,
        rule.version,
        "mappings",
        limits,
        base_v2_ctx,
        collector,
    )?;
    Ok(out)
}

#[allow(clippy::too_many_arguments)]
pub(super) fn apply_mappings_into_traced(
    rule: &RuleFile,
    mappings: &[Mapping],
    record: &JsonValue,
    context: Option<&JsonValue>,
    out: &mut JsonValue,
    warnings: &mut Vec<TransformWarning>,
    rule_version: u8,
    base_path: &str,
    limits: EvalLimits,
    base_v2_ctx: &V2EvalContext<'_>,
    collector: &mut TraceCollector,
) -> Result<(), TransformError> {
    for (index, mapping) in mappings.iter().enumerate() {
        let mapping_path = format!("{}[{}]", base_path, index);
        collector
            .start_span(TraceEventKind::MappingStart, TracePhase::Start)
            .rule_path(&mapping_path)
            .attr_index("mapping_index", index)
            .finish(collector);

        let applied = if mapping.when.is_some() {
            let when_path = format!("{}.when", mapping_path);
            collector
                .start_span(TraceEventKind::MappingWhenStart, TracePhase::Start)
                .rule_path(&when_path)
                .finish(collector);
            let flag = eval_when_traced(
                mapping,
                record,
                context,
                out,
                &mapping_path,
                warnings,
                rule_version,
                limits,
                base_v2_ctx,
                collector,
            );
            collector
                .end_span(TraceEventKind::MappingWhenEnd, TracePhase::End)
                .rule_path(&when_path)
                .finish_with_output(collector, &JsonValue::Bool(flag), None);
            flag
        } else {
            true
        };

        collector
            .emit(TraceEventKind::MappingDecision, TracePhase::Instant)
            .rule_path(&mapping_path)
            .attr_bool("applied", applied)
            .attr_enum("skip_reason", if applied { "none" } else { "when_false" })
            .finish(collector);

        if !applied {
            collector
                .end_span(TraceEventKind::MappingEnd, TracePhase::End)
                .rule_path(&mapping_path)
                .finish(collector);
            continue;
        }

        let value = match eval_mapping_traced(
            rule,
            mapping,
            record,
            context,
            out,
            &mapping_path,
            rule_version,
            limits,
            base_v2_ctx,
            collector,
        ) {
            Ok(value) => value,
            Err(error) => {
                collector
                    .error_span(TraceEventKind::Error, "MAPPING_ERROR", "mapping failed")
                    .rule_path(&mapping_path)
                    .finish(collector);
                return Err(error);
            }
        };

        if let Some(value) = value {
            if let Err(error) = set_path(out, &mapping.target, value.clone(), &mapping_path) {
                collector
                    .error_span(TraceEventKind::Error, "MAPPING_ERROR", "mapping failed")
                    .rule_path(&mapping_path)
                    .finish(collector);
                return Err(error);
            }
            let output_redaction_hint = mapping_output_redaction_hint(mapping);
            collector
                .emit(TraceEventKind::OutputWrite, TracePhase::Instant)
                .rule_path(format!("{}.target", mapping_path))
                .output_path(canonical_output_path(&mapping.target))
                .attr_path("target_path", canonical_output_path(&mapping.target))
                .finish_with_output(collector, &value, Some(&output_redaction_hint));
        }

        collector
            .end_span(TraceEventKind::MappingEnd, TracePhase::End)
            .rule_path(&mapping_path)
            .finish(collector);
    }
    Ok(())
}

fn mapping_output_redaction_hint(mapping: &Mapping) -> String {
    let mut hint = mapping.target.clone();
    if let Some(source) = &mapping.source {
        hint.push(' ');
        hint.push_str(source);
    }
    if let Some(expr) = &mapping.expr {
        collect_expr_redaction_hints(expr, &mut hint);
    }
    hint
}

fn collect_expr_redaction_hints(expr: &Expr, hint: &mut String) {
    match expr {
        Expr::Ref(expr_ref) => {
            hint.push(' ');
            hint.push_str(&expr_ref.ref_path);
        }
        Expr::Op(expr_op) => {
            for arg in &expr_op.args {
                collect_expr_redaction_hints(arg, hint);
            }
        }
        Expr::Chain(expr_chain) => {
            for part in &expr_chain.chain {
                collect_expr_redaction_hints(part, hint);
            }
        }
        Expr::Literal(value) => collect_json_redaction_hints(value, hint),
    }
}

fn collect_json_redaction_hints(value: &JsonValue, hint: &mut String) {
    match value {
        JsonValue::String(value) if value.starts_with('@') => {
            hint.push(' ');
            hint.push_str(value);
        }
        JsonValue::Array(values) => {
            for value in values {
                collect_json_redaction_hints(value, hint);
            }
        }
        JsonValue::Object(values) => {
            for value in values.values() {
                collect_json_redaction_hints(value, hint);
            }
        }
        _ => {}
    }
}