runx-parser 0.6.19

Pure Rust parser parity crate for runx skills, graphs, and tool manifests.
Documentation
use runx_contracts::{JsonObject, JsonValue};

use crate::ValidationError;

#[derive(Clone, Copy)]
pub(crate) struct JsonFieldReader {
    owner: &'static str,
}

impl JsonFieldReader {
    pub(crate) const fn new(owner: &'static str) -> Self {
        Self { owner }
    }

    pub(crate) fn validation_error(&self, message: impl Into<String>) -> ValidationError {
        ValidationError::InvalidField {
            field: self.owner.to_owned(),
            message: message.into(),
        }
    }

    pub(crate) fn required_string(
        &self,
        value: Option<&JsonValue>,
        field: &str,
    ) -> Result<String, ValidationError> {
        match self.optional_string(value, field)? {
            Some(value) if !value.is_empty() => Ok(value),
            _ => Err(ValidationError::MissingField {
                field: field.to_owned(),
            }),
        }
    }

    pub(crate) fn optional_string(
        &self,
        value: Option<&JsonValue>,
        field: &str,
    ) -> Result<Option<String>, ValidationError> {
        match value {
            None | Some(JsonValue::Null) => Ok(None),
            Some(JsonValue::String(value)) => Ok(Some(value.clone())),
            Some(_) => Err(self.validation_error(format!("{field} must be a string."))),
        }
    }

    pub(crate) fn optional_non_empty_string(
        &self,
        value: Option<&JsonValue>,
        field: &str,
    ) -> Result<Option<String>, ValidationError> {
        let Some(value) = self.optional_string(value, field)? else {
            return Ok(None);
        };
        if value.trim().is_empty() {
            return Err(self.validation_error(format!("{field} must not be empty.")));
        }
        Ok(Some(value))
    }

    pub(crate) fn required_object<'a>(
        &self,
        value: Option<&'a JsonValue>,
        field: &str,
    ) -> Result<&'a JsonObject, ValidationError> {
        match value {
            Some(JsonValue::Object(value)) => Ok(value),
            None | Some(JsonValue::Null) => {
                Err(self.validation_error(format!("{field} is required.")))
            }
            Some(_) => Err(self.validation_error(format!("{field} must be an object."))),
        }
    }

    pub(crate) fn optional_object(
        &self,
        value: Option<&JsonValue>,
        field: &str,
    ) -> Result<Option<JsonObject>, ValidationError> {
        match value {
            None | Some(JsonValue::Null) => Ok(None),
            Some(JsonValue::Object(value)) => Ok(Some(value.clone())),
            Some(_) => Err(self.validation_error(format!("{field} must be an object."))),
        }
    }

    pub(crate) fn required_plain_array<'a>(
        &self,
        value: Option<&'a JsonValue>,
        field: &str,
    ) -> Result<&'a [JsonValue], ValidationError> {
        match value {
            Some(JsonValue::Array(values)) => Ok(values),
            None | Some(JsonValue::Null) => {
                Err(self.validation_error(format!("{field} is required.")))
            }
            Some(_) => Err(self.validation_error(format!("{field} must be an array."))),
        }
    }

    pub(crate) fn optional_string_array(
        &self,
        value: Option<&JsonValue>,
        field: &str,
    ) -> Result<Option<Vec<String>>, ValidationError> {
        match value {
            None | Some(JsonValue::Null) => Ok(None),
            Some(JsonValue::Array(values)) => values
                .iter()
                .map(|value| match value {
                    JsonValue::String(value) => Ok(value.clone()),
                    _ => {
                        Err(self.validation_error(format!("{field} must be an array of strings.")))
                    }
                })
                .collect::<Result<Vec<_>, _>>()
                .map(Some),
            Some(_) => Err(self.validation_error(format!("{field} must be an array of strings."))),
        }
    }

    pub(crate) fn optional_bool(
        &self,
        value: Option<&JsonValue>,
        field: &str,
    ) -> Result<Option<bool>, ValidationError> {
        match value {
            None | Some(JsonValue::Null) => Ok(None),
            Some(JsonValue::Bool(value)) => Ok(Some(*value)),
            Some(_) => Err(self.validation_error(format!("{field} must be a boolean."))),
        }
    }

    pub(crate) fn optional_u64(
        &self,
        value: Option<&JsonValue>,
        field: &str,
    ) -> Result<Option<u64>, ValidationError> {
        match value {
            None | Some(JsonValue::Null) => Ok(None),
            Some(JsonValue::Number(number)) => {
                let Some(value) = number.as_f64() else {
                    return Err(self.validation_error(format!("{field} must be a finite number.")));
                };
                if value.fract() == 0.0 && value >= 0.0 && value <= u64::MAX as f64 {
                    Ok(Some(value as u64))
                } else {
                    Err(self.validation_error(format!("{field} must be a positive integer.")))
                }
            }
            Some(_) => Err(self.validation_error(format!("{field} must be a finite number."))),
        }
    }

    pub(crate) fn reject_unknown_fields(
        &self,
        object: &JsonObject,
        field: &str,
        allowed: &[&str],
    ) -> Result<(), ValidationError> {
        for key in object.keys() {
            if !allowed.contains(&key.as_str()) {
                return Err(self.validation_error(format!(
                    "{field}.{key} is not supported; allowed fields: {}.",
                    allowed.join(", ")
                )));
            }
        }
        Ok(())
    }
}

pub(crate) fn first_value<'a>(
    left: Option<&'a JsonValue>,
    right: Option<&'a JsonValue>,
) -> Option<&'a JsonValue> {
    match left {
        None | Some(JsonValue::Null) => right,
        Some(value) => Some(value),
    }
}

pub(crate) fn field_value<'a>(
    object: Option<&'a JsonObject>,
    field: &str,
) -> Option<&'a JsonValue> {
    object.and_then(|object| object.get(field))
}

pub(crate) fn nested_value<'a>(value: Option<&'a JsonValue>, field: &str) -> Option<&'a JsonValue> {
    match value {
        Some(JsonValue::Object(object)) => object.get(field),
        _ => None,
    }
}