wp-connector-api 0.8.2

Connector runtime traits, config helpers, and errors for WarpParse sinks and sources
Documentation
use derive_more::From;
use orion_error::StructError;
use orion_error::{ErrorCode, ToStructError, UvsReason};
use serde::Serialize;
use std::error::Error as StdError;
use std::sync::mpsc::SendError;
use thiserror::Error;

#[derive(Debug, Error, PartialEq, Serialize, From)]
pub enum SinkReason {
    #[error("sink unavailable {0}")]
    Sink(String),
    #[error("set mock error")]
    Mock,
    #[error("stg ctrl error")]
    StgCtrl,
    #[error("{0}")]
    Uvs(UvsReason),
}
impl ErrorCode for SinkReason {
    fn error_code(&self) -> i32 {
        match self {
            // General sink errors
            SinkReason::Sink(_) => 500, // General sink unavailable

            // Testing/mock errors
            SinkReason::Mock => 599, // Mock/test error

            // Storage control errors
            SinkReason::StgCtrl => 510, // Storage control error

            // Delegate to wrapped reason
            SinkReason::Uvs(r) => r.error_code(),
        }
    }
}

pub type SinkError = StructError<SinkReason>;

pub trait ReasonSummary {
    fn summary(&self) -> String;
}

impl<T> From<SendError<T>> for SinkReason
where
    T: ReasonSummary,
{
    fn from(err: SendError<T>) -> Self {
        SinkReason::Sink(format!("send error: {}", err.0.summary()))
    }
}

impl From<anyhow::Error> for SinkReason {
    fn from(e: anyhow::Error) -> Self {
        SinkReason::Sink(format!("{}", e))
    }
}

pub type SinkResult<T> = Result<T, SinkError>;

impl SinkReason {
    pub fn sink<S: Into<String>>(msg: S) -> Self {
        SinkReason::Sink(msg.into())
    }

    pub fn err(self) -> SinkError {
        self.to_err()
    }

    pub fn err_detail<S: Into<String>>(self, detail: S) -> SinkError {
        self.to_err().with_detail(detail.into())
    }

    pub fn err_source<E>(self, source: E) -> SinkError
    where
        E: StdError + Send + Sync + 'static,
    {
        self.to_err().with_source(source)
    }
}

pub trait SinkErrorOwe<T> {
    fn owe_sink<S: Into<String>>(self, msg: S) -> Result<T, StructError<SinkReason>>;
}

impl<T, E> SinkErrorOwe<T> for Result<T, E>
where
    E: std::fmt::Display,
{
    fn owe_sink<S: Into<String>>(self, msg: S) -> Result<T, StructError<SinkReason>> {
        match self {
            Ok(v) => Ok(v),
            Err(e) => {
                Err(StructError::from(SinkReason::Sink(msg.into())).with_detail(e.to_string()))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::mpsc;

    #[derive(Clone)]
    struct Summary(&'static str);

    impl ReasonSummary for Summary {
        fn summary(&self) -> String {
            self.0.into()
        }
    }

    #[test]
    fn sink_reason_from_send_error_uses_inner_summary() {
        let (tx, rx) = mpsc::channel();
        drop(rx);
        let err = tx.send(Summary("queue overflow")).unwrap_err();
        let reason = SinkReason::from(err);
        match reason {
            SinkReason::Sink(msg) => assert!(msg.contains("queue overflow")),
            other => panic!("unexpected reason: {other:?}"),
        }
    }

    #[test]
    fn sink_error_owe_wraps_displayable_error() {
        let failing: Result<(), &str> = Err("io timeout");
        let err = failing.owe_sink("flush failed").unwrap_err();
        match err.reason() {
            SinkReason::Sink(msg) => assert_eq!(msg, "flush failed"),
            other => panic!("unexpected reason: {other:?}"),
        }
        let detail = err.detail();
        assert_eq!(detail.as_ref().map(|s| s.as_str()), Some("io timeout"));
    }

    #[test]
    fn sink_reason_error_codes() {
        assert_eq!(SinkReason::Sink("test".into()).error_code(), 500);
        assert_eq!(SinkReason::Mock.error_code(), 599);
        assert_eq!(SinkReason::StgCtrl.error_code(), 510);
    }

    #[test]
    fn sink_reason_error_codes_are_distinct() {
        let codes = vec![
            SinkReason::Sink("x".into()).error_code(),
            SinkReason::Mock.error_code(),
            SinkReason::StgCtrl.error_code(),
        ];
        // Verify all codes are different
        let mut unique = codes.clone();
        unique.sort();
        unique.dedup();
        assert_eq!(codes.len(), unique.len(), "error codes should be distinct");
    }

    #[test]
    fn sink_reason_err_detail_sets_detail() {
        let err = SinkReason::sink("flush failed").err_detail("io timeout");
        assert_eq!(err.detail().as_deref(), Some("io timeout"));
    }

    #[test]
    fn sink_reason_err_source_preserves_source_message() {
        let err = SinkReason::sink("udp send failed").err_source(std::io::Error::other("no route"));
        assert!(err.to_string().contains("no route"));
    }
}