Skip to main content

shakrs_json_parser/runtime/
parse.rs

1//! Parse + semantic validation of `shakrs.json` bytes.
2
3use garde::Validate;
4
5use crate::types::{ConfigParseError, ShakrsConfig};
6
7impl core::fmt::Display for ConfigParseError {
8    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
9        match self {
10            Self::Schema(detail) => write!(f, "shakrs.json schema error: {detail}"),
11            Self::Semantic(detail) => write!(f, "shakrs.json semantic error: {detail}"),
12        }
13    }
14}
15
16impl core::error::Error for ConfigParseError {}
17
18/// Parse `shakrs.json` bytes into a validated [`ShakrsConfig`].
19///
20/// Two passes: `serde_json` handles syntax and schema (`deny_unknown_fields`
21/// rejects unknown keys), then the `garde` semantic pass enforces the
22/// cross-field rules. Returns the first failure.
23///
24/// # Errors
25///
26/// Returns [`ConfigParseError::Schema`] when the bytes are not valid JSON or
27/// violate the schema, and [`ConfigParseError::Semantic`] when a `garde` rule
28/// fails (unsupported version, empty waiver reason, ...).
29pub fn parse(bytes: &[u8]) -> Result<ShakrsConfig, ConfigParseError> {
30    // `serde_json::from_slice` is disallowed by clippy.toml in favour of a
31    // `Validated<T>::new` wrapper. That wrapper does not exist in this
32    // workspace, and the very next statement runs the `garde` validation the
33    // disallowed-method rule requires. This is the deserialize-then-validate
34    // contract, not a bypass. A generic wrapper would be single-use here.
35    #[expect(
36        clippy::disallowed_methods,
37        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."
38    )]
39    let config: ShakrsConfig =
40        serde_json::from_slice(bytes).map_err(|err| ConfigParseError::Schema(err.to_string()))?;
41    config
42        .validate()
43        .map_err(|err| ConfigParseError::Semantic(err.to_string()))?;
44    Ok(config)
45}