runx-parser 0.6.19

Pure Rust parser parity crate for runx skills, graphs, and tool manifests.
Documentation
use std::collections::BTreeMap;

use runx_contracts::{JsonObject, JsonValue};
use serde::{Deserialize, Serialize};

use crate::ValidationError;

use super::FIELDS;

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct HarnessCallerFixture {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub answers: Option<JsonObject>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub approvals: Option<BTreeMap<String, bool>>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReceiptExpectation {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub kind: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub skill_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub graph_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub owner: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnessExpectation {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub receipt: Option<ReceiptExpectation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub steps: Option<Vec<String>>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RunnerHarnessCase {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub runner: Option<String>,
    pub inputs: JsonObject,
    pub env: BTreeMap<String, String>,
    pub caller: HarnessCallerFixture,
    pub expect: HarnessExpectation,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RunnerHarnessManifest {
    pub cases: Vec<RunnerHarnessCase>,
}

pub(crate) fn validate_harness_manifest(
    value: Option<JsonObject>,
    field: &str,
) -> Result<Option<RunnerHarnessManifest>, ValidationError> {
    let Some(value) = value else {
        return Ok(None);
    };
    let cases = FIELDS
        .required_plain_array(value.get("cases"), &format!("{field}.cases"))?
        .iter()
        .enumerate()
        .map(|(index, entry)| {
            validate_harness_case(
                FIELDS.required_object(Some(entry), &format!("{field}.cases[{index}]"))?,
                &format!("{field}.cases[{index}]"),
            )
        })
        .collect::<Result<Vec<_>, _>>()?;
    Ok(Some(RunnerHarnessManifest { cases }))
}

fn validate_harness_case(
    value: &JsonObject,
    field: &str,
) -> Result<RunnerHarnessCase, ValidationError> {
    Ok(RunnerHarnessCase {
        name: FIELDS.required_string(value.get("name"), &format!("{field}.name"))?,
        runner: FIELDS
            .optional_non_empty_string(value.get("runner"), &format!("{field}.runner"))?,
        inputs: FIELDS
            .optional_object(value.get("inputs"), &format!("{field}.inputs"))?
            .unwrap_or_default(),
        env: validate_string_object(
            FIELDS
                .optional_object(value.get("env"), &format!("{field}.env"))?
                .unwrap_or_default(),
            &format!("{field}.env"),
        )?,
        caller: validate_harness_caller(
            FIELDS
                .optional_object(value.get("caller"), &format!("{field}.caller"))?
                .unwrap_or_default(),
            &format!("{field}.caller"),
        )?,
        expect: validate_harness_expectation(
            FIELDS.required_object(value.get("expect"), &format!("{field}.expect"))?,
            &format!("{field}.expect"),
        )?,
    })
}

fn validate_string_object(
    value: JsonObject,
    field: &str,
) -> Result<BTreeMap<String, String>, ValidationError> {
    value
        .into_iter()
        .map(|(key, value)| match value {
            JsonValue::String(value) => Ok((key, value)),
            _ => Err(FIELDS.validation_error(format!("{field}.{key} must be a string."))),
        })
        .collect()
}

fn validate_harness_caller(
    value: JsonObject,
    field: &str,
) -> Result<HarnessCallerFixture, ValidationError> {
    Ok(HarnessCallerFixture {
        answers: FIELDS.optional_object(value.get("answers"), &format!("{field}.answers"))?,
        approvals: Some(validate_bool_object(
            FIELDS
                .optional_object(value.get("approvals"), &format!("{field}.approvals"))?
                .unwrap_or_default(),
            &format!("{field}.approvals"),
        )?),
    })
}

fn validate_bool_object(
    value: JsonObject,
    field: &str,
) -> Result<BTreeMap<String, bool>, ValidationError> {
    value
        .into_iter()
        .map(|(key, value)| match value {
            JsonValue::Bool(value) => Ok((key, value)),
            _ => Err(FIELDS.validation_error(format!("{field}.{key} must be a boolean."))),
        })
        .collect()
}

fn validate_harness_expectation(
    value: &JsonObject,
    field: &str,
) -> Result<HarnessExpectation, ValidationError> {
    Ok(HarnessExpectation {
        status: optional_harness_status(value.get("status"), &format!("{field}.status"))?,
        receipt: validate_receipt_expectation(
            FIELDS.optional_object(value.get("receipt"), &format!("{field}.receipt"))?,
            &format!("{field}.receipt"),
        )?,
        steps: FIELDS.optional_string_array(value.get("steps"), &format!("{field}.steps"))?,
    })
}

fn validate_receipt_expectation(
    value: Option<JsonObject>,
    field: &str,
) -> Result<Option<ReceiptExpectation>, ValidationError> {
    let Some(value) = value else {
        return Ok(None);
    };
    Ok(Some(ReceiptExpectation {
        kind: optional_receipt_kind(value.get("kind"), &format!("{field}.kind"))?,
        status: optional_receipt_status(value.get("status"), &format!("{field}.status"))?,
        skill_name: FIELDS
            .optional_string(value.get("skill_name"), &format!("{field}.skill_name"))?,
        source_type: FIELDS
            .optional_string(value.get("source_type"), &format!("{field}.source_type"))?,
        graph_name: FIELDS
            .optional_string(value.get("graph_name"), &format!("{field}.graph_name"))?,
        owner: FIELDS.optional_string(value.get("owner"), &format!("{field}.owner"))?,
    }))
}

fn optional_harness_status(
    value: Option<&JsonValue>,
    field: &str,
) -> Result<Option<String>, ValidationError> {
    validate_enum(
        value,
        field,
        &[
            "sealed",
            "failure",
            "needs_agent",
            "policy_denied",
            "escalated",
        ],
    )
}

fn optional_receipt_status(
    value: Option<&JsonValue>,
    field: &str,
) -> Result<Option<String>, ValidationError> {
    validate_enum(value, field, &["sealed", "failure"])
}

fn optional_receipt_kind(
    value: Option<&JsonValue>,
    field: &str,
) -> Result<Option<String>, ValidationError> {
    validate_enum(value, field, &["receipt"])
}

fn validate_enum(
    value: Option<&JsonValue>,
    field: &str,
    allowed: &[&str],
) -> Result<Option<String>, ValidationError> {
    let Some(value) = FIELDS.optional_string(value, field)? else {
        return Ok(None);
    };
    if allowed.iter().any(|allowed| *allowed == value) {
        return Ok(Some(value));
    }
    Err(FIELDS.validation_error(format!("{field} must be {}.", allowed.join(", "))))
}