shakrs-json-parser 0.1.0

Parser, validator, scaffolder, and canonical formatter for the shakrs.json workspace config. Zero I/O; no policy-registry knowledge.
Documentation
//! Parse + semantic validation of `shakrs.json` bytes.

use garde::Validate;

use crate::types::{ConfigParseError, ShakrsConfig};

impl core::fmt::Display for ConfigParseError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::Schema(detail) => write!(f, "shakrs.json schema error: {detail}"),
            Self::Semantic(detail) => write!(f, "shakrs.json semantic error: {detail}"),
        }
    }
}

impl core::error::Error for ConfigParseError {}

/// Parse `shakrs.json` bytes into a validated [`ShakrsConfig`].
///
/// Two passes: `serde_json` handles syntax and schema (`deny_unknown_fields`
/// rejects unknown keys), then the `garde` semantic pass enforces the
/// cross-field rules. Returns the first failure.
///
/// # Errors
///
/// Returns [`ConfigParseError::Schema`] when the bytes are not valid JSON or
/// violate the schema, and [`ConfigParseError::Semantic`] when a `garde` rule
/// fails (unsupported version, empty waiver reason, ...).
pub fn parse(bytes: &[u8]) -> Result<ShakrsConfig, ConfigParseError> {
    // `serde_json::from_slice` is disallowed by clippy.toml in favour of a
    // `Validated<T>::new` wrapper. That wrapper does not exist in this
    // workspace, and the very next statement runs the `garde` validation the
    // disallowed-method rule requires. This is the deserialize-then-validate
    // contract, not a bypass. A generic wrapper would be single-use here.
    #[expect(
        clippy::disallowed_methods,
        reason = "deserialize-then-garde-validate is the contract the disallowed-method rule enforces; no Validated<T> exists in this workspace and a generic one would be single-use."
    )]
    let config: ShakrsConfig =
        serde_json::from_slice(bytes).map_err(|err| ConfigParseError::Schema(err.to_string()))?;
    config
        .validate()
        .map_err(|err| ConfigParseError::Semantic(err.to_string()))?;
    Ok(config)
}