omena-query 0.3.0

Omena query boundary over CME producer query fragments
Documentation
use crate::{
    OmenaErrorClassV0, OmenaErrorContextV0, OmenaErrorEvidenceReferenceV0,
    OmenaErrorRecoverabilityV0, OmenaErrorSeverityV0, OmenaErrorV0,
};
use omena_evidence_graph::{EvidenceGraphV0, EvidenceNodeKeyV0};
use std::fmt;

pub type OmenaError = OmenaErrorV0;

impl OmenaErrorV0 {
    pub fn new(
        class: OmenaErrorClassV0,
        message: impl Into<String>,
        context: OmenaErrorContextV0,
    ) -> Self {
        Self {
            class,
            message: message.into(),
            context,
        }
    }

    pub fn unknown(message: impl Into<String>, code: impl Into<String>) -> Self {
        Self::new(
            OmenaErrorClassV0::Unknown,
            message,
            OmenaErrorContextV0 {
                code: code.into(),
                severity: OmenaErrorSeverityV0::Error,
                recoverability: OmenaErrorRecoverabilityV0::UserAction,
                evidence: Vec::new(),
            },
        )
    }

    pub fn with_evidence_graph_nodes(
        mut self,
        graph: &EvidenceGraphV0,
        keys: impl IntoIterator<Item = EvidenceNodeKeyV0>,
    ) -> Result<Self, OmenaErrorEvidenceBindingErrorV0> {
        let mut keys = keys.into_iter().collect::<Vec<_>>();
        keys.sort();
        keys.dedup();
        for key in &keys {
            if !graph.nodes.iter().any(|node| &node.key == key) {
                return Err(OmenaErrorEvidenceBindingErrorV0::MissingNode(key.clone()));
            }
        }
        self.context.evidence = keys
            .into_iter()
            .map(|key| OmenaErrorEvidenceReferenceV0 {
                query_identity: key.query_identity,
                input_identity: key.input_identity,
            })
            .collect();
        Ok(self)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OmenaErrorEvidenceBindingErrorV0 {
    MissingNode(EvidenceNodeKeyV0),
}

impl fmt::Display for OmenaErrorEvidenceBindingErrorV0 {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MissingNode(key) => write!(
                formatter,
                "evidence graph does not contain query '{}' input '{}'",
                key.query_identity, key.input_identity
            ),
        }
    }
}

impl std::error::Error for OmenaErrorEvidenceBindingErrorV0 {}

impl fmt::Display for OmenaErrorV0 {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}: {}", self.context.code, self.message)
    }
}

impl std::error::Error for OmenaErrorV0 {}

pub fn omena_error_from_boundary_encoding(
    kind: &str,
    message: impl Into<String>,
    operation: &str,
) -> OmenaError {
    let (class, recoverability) = match kind {
        "parse-error" => (
            OmenaErrorClassV0::Input,
            OmenaErrorRecoverabilityV0::UserAction,
        ),
        "serialize-error" => (
            OmenaErrorClassV0::Internal,
            OmenaErrorRecoverabilityV0::Retry,
        ),
        "unsupported-mode" => (
            OmenaErrorClassV0::Unsupported,
            OmenaErrorRecoverabilityV0::UserAction,
        ),
        _ => (
            OmenaErrorClassV0::Unknown,
            OmenaErrorRecoverabilityV0::UserAction,
        ),
    };
    OmenaErrorV0::new(
        class,
        message,
        OmenaErrorContextV0 {
            code: format!("boundary.{operation}.{kind}"),
            severity: OmenaErrorSeverityV0::Error,
            recoverability,
            evidence: Vec::new(),
        },
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use omena_evidence_graph::{
        EvidenceDemandEdgeV0, EvidenceNodeSeedV0, GuaranteeKindV0,
        build_evidence_graph_from_edges_v0,
    };

    #[test]
    fn boundary_error_encodings_map_to_unified_classes() {
        assert_eq!(
            omena_error_from_boundary_encoding("parse-error", "bad input", "build").class,
            OmenaErrorClassV0::Input
        );
        assert_eq!(
            omena_error_from_boundary_encoding("serialize-error", "encoding", "check").class,
            OmenaErrorClassV0::Internal
        );
        assert_eq!(
            omena_error_from_boundary_encoding("unsupported-mode", "mode", "build").class,
            OmenaErrorClassV0::Unsupported
        );
        assert_eq!(
            omena_error_from_boundary_encoding("future-kind", "unknown", "query").class,
            OmenaErrorClassV0::Unknown
        );
    }

    #[test]
    fn evidence_binding_requires_a_graph_node_and_is_absent_by_default() -> Result<(), String> {
        let key = EvidenceNodeKeyV0::new("sdkDiagnostics", "src/card.css");
        let graph = build_evidence_graph_from_edges_v0(
            [EvidenceNodeSeedV0::new(
                key.clone(),
                vec!["fixture".to_string()],
                GuaranteeKindV0::for_label_less_family(),
            )],
            [EvidenceDemandEdgeV0::new(
                "sdkError",
                key.clone(),
                "supportsErrorContext",
            )],
        )
        .map_err(|error| format!("evidence graph fixture must be valid: {error:?}"))?;
        let error = OmenaError::unknown("analysis failed", "analysis.failed");
        let default_json = serde_json::to_value(&error)
            .map_err(|error| format!("default error must serialize: {error}"))?;
        assert!(default_json["context"].get("evidence").is_none());

        let bound = error
            .with_evidence_graph_nodes(&graph, [key.clone()])
            .map_err(|error| format!("known evidence node must bind: {error}"))?;
        let bound_json = serde_json::to_value(bound)
            .map_err(|error| format!("bound error must serialize: {error}"))?;
        assert_eq!(
            bound_json["context"]["evidence"][0],
            serde_json::json!({
                "queryIdentity": "sdkDiagnostics",
                "inputIdentity": "src/card.css",
            }),
        );

        let missing = OmenaError::unknown("analysis failed", "analysis.failed")
            .with_evidence_graph_nodes(
                &graph,
                [EvidenceNodeKeyV0::new("sdkDiagnostics", "src/missing.css")],
            );
        assert!(matches!(
            missing,
            Err(OmenaErrorEvidenceBindingErrorV0::MissingNode(_))
        ));
        Ok(())
    }
}