helena 0.1.0

Core types and component interfaces for helena, a latent data-to-waveform generation platform.
Documentation
//! Closed-set validation of flattened component `extra` maps.

use crate::error::{Error, Result};
use crate::latent::Metadata;

/// Reject any key in `extra` that no resolver field consumes.
///
/// The component resolvers (`BuiltinEncoder` / `BuiltinCodec` /
/// `BuiltinGenerator`, in the downstream crates) read architecture settings from
/// a flattened `extra` map. A key outside the recognized set is a typo or a
/// setting meant for a different `kind`; left silent it falls back to a default,
/// so the resolved component diverges from the written config without warning, an
/// NFR-010 reproducibility hazard. This fails such a key closed at the config
/// trust boundary instead.
///
/// `component` is the config section (`"data_encoder"`, `"audio_codec"`,
/// `"generator"`) and `kind` the resolved plugin key, so the message points at
/// the exact site. [`Metadata`] is a `BTreeMap`, so the reported keys are sorted
/// and the error is deterministic (REPRO).
pub fn reject_unknown_extra(
    component: &str,
    kind: &str,
    extra: &Metadata,
    recognized: &[&str],
) -> Result<()> {
    let unknown: Vec<&str> = extra
        .keys()
        .map(String::as_str)
        .filter(|key| !recognized.contains(key))
        .collect();
    if unknown.is_empty() {
        return Ok(());
    }
    Err(Error::validation(format!(
        "{component}.type {kind:?}: unrecognized setting(s) {unknown:?} \
         (recognized: {recognized:?})"
    )))
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    fn meta(pairs: &[(&str, serde_json::Value)]) -> Metadata {
        pairs
            .iter()
            .map(|(k, v)| (k.to_string(), v.clone()))
            .collect()
    }

    #[test]
    fn empty_extra_is_accepted() {
        assert!(reject_unknown_extra("audio_codec", "pcm", &meta(&[]), &["frame_size"]).is_ok());
    }

    #[test]
    fn recognized_keys_are_accepted() {
        let extra = meta(&[("frame_size", json!(4)), ("channels", json!(2))]);
        assert!(
            reject_unknown_extra("audio_codec", "pcm", &extra, &["frame_size", "channels"]).is_ok()
        );
    }

    #[test]
    fn unknown_key_is_rejected_and_named() {
        let extra = meta(&[("frame_size", json!(4)), ("frmae_size", json!(8))]);
        let err = reject_unknown_extra("audio_codec", "pcm", &extra, &["frame_size", "channels"])
            .unwrap_err();
        let msg = err.to_string();
        assert!(matches!(err, Error::Validation(_)), "{msg}");
        assert!(msg.contains("frmae_size"), "names the typo: {msg}");
        assert!(msg.contains("pcm"), "names the kind: {msg}");
    }

    #[test]
    fn no_recognized_keys_rejects_any_extra() {
        let extra = meta(&[("hidden_dim", json!(64))]);
        assert!(reject_unknown_extra("data_encoder", "vector", &extra, &[]).is_err());
    }

    #[test]
    fn all_unknown_keys_are_reported_in_sorted_order() {
        // BTreeMap iteration is sorted, so the message is deterministic.
        let extra = meta(&[("zeta", json!(1)), ("alpha", json!(2))]);
        let msg = reject_unknown_extra("generator", "mlp", &extra, &[])
            .unwrap_err()
            .to_string();
        let alpha = msg.find("alpha").expect("alpha reported");
        let zeta = msg.find("zeta").expect("zeta reported");
        assert!(alpha < zeta, "sorted order: {msg}");
    }
}