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}