hen 0.17.0

Run protocol-aware API request collections from the command line or through MCP.
Documentation
use serde_json::{json, Value};

use crate::request::{
    AssertionDiff, AssertionMismatch, AssertionMismatchValue, AssertionOutcome,
    ExecutionTraceEntry,
};

use super::redaction::OutputRedactor;

pub(crate) fn assertion_results_json(
    assertions: &[AssertionOutcome],
    redactor: &OutputRedactor,
) -> Vec<Value> {
    assertions
        .iter()
        .map(|assertion| {
            json!({
                "assertion": assertion.assertion,
                "status": assertion.status.as_str(),
                "message": redactor.redact_optional_text(assertion.message.as_deref()),
                "mismatch": assertion.mismatch.as_ref().map(|mismatch| assertion_mismatch_json(mismatch, redactor)),
                "diff": assertion.diff.as_ref().map(|diff| assertion_diff_json(diff, redactor)),
            })
        })
        .collect()
}

pub(crate) fn assertion_diff_json(diff: &AssertionDiff, redactor: &OutputRedactor) -> Value {
    json!({
        "format": diff.format.as_str(),
        "path": diff.path,
        "text": redactor.redact_text(&diff.rendered),
    })
}

pub(crate) fn assertion_mismatch_json(
    mismatch: &AssertionMismatch,
    redactor: &OutputRedactor,
) -> Value {
    json!({
        "kind": mismatch.kind.as_str(),
        "reason": mismatch.reason.as_str(),
        "target": mismatch.target,
        "path": mismatch.path,
        "actualPath": mismatch.actual_path,
        "comparedPath": mismatch.compared_path,
        "operator": mismatch.operator,
        "actual": assertion_mismatch_value_json(&mismatch.actual, redactor),
        "expected": assertion_mismatch_value_json(&mismatch.expected, redactor),
    })
}

pub(crate) fn assertion_mismatch_value_json(
    value: &AssertionMismatchValue,
    redactor: &OutputRedactor,
) -> Value {
    json!({
        "type": value.value_type,
        "value": redactor.redact_json_value(&value.value),
    })
}

pub(crate) fn execution_trace_json(entry: &ExecutionTraceEntry) -> Value {
    let redactor = OutputRedactor::with_header_names(
        &entry.sensitive_values,
        &entry.sensitive_header_names,
    );

    json!({
        "seq": entry.seq,
        "kind": entry.kind.as_str(),
        "index": entry.request_index,
        "request": entry.request.as_deref().map(|request| redactor.redact_text(request)),
        "dependencies": entry.dependencies.iter().map(|dependency| redactor.redact_text(dependency)).collect::<Vec<_>>(),
        "waitingOn": entry.waiting_on.iter().map(|request| redactor.redact_text(request)).collect::<Vec<_>>(),
        "protocol": entry.protocol.map(|protocol| protocol.as_str()),
        "protocolContext": entry.protocol_context.as_ref().map(|value| redactor.redact_json_value(value)),
        "durationMs": entry.duration.map(|duration| duration.as_millis() as u64),
        "reason": entry.reason,
        "relatedRequest": entry.related_request.as_deref().map(|request| redactor.redact_text(request)),
        "group": entry.group.as_deref().map(|group| redactor.redact_text(group)),
        "cause": entry.cause.as_deref().map(|cause| redactor.redact_text(cause)),
        "message": entry.message.as_deref().map(|message| redactor.redact_text(message)),
        "signal": entry.signal.map(|signal| signal.as_str()),
    })
}

pub(crate) fn with_type(mut value: Value, kind: &str) -> Value {
    if let Value::Object(ref mut map) = value {
        map.insert("type".to_string(), Value::String(kind.to_string()));
    }
    value
}