tinc 0.2.3

GRPc to REST transcoding library
Documentation
use axum::response::IntoResponse;

use super::{
    HttpErrorResponse, HttpErrorResponseCode, HttpErrorResponseDetails,
    HttpErrorResponseRequestViolation, TrackerFor, TrackerSharedState, TrackerWrapper,
};

#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
    #[error("error evaluating expression `{expression}` on field `{field}`: {error}")]
    Expression {
        field: &'static str,
        error: Box<str>,
        expression: &'static str,
    },
    #[error("{0}")]
    FailFast(Box<str>),
}

impl serde::de::Error for ValidationError {
    fn custom<T>(msg: T) -> Self
    where
        T: std::fmt::Display,
    {
        Self::FailFast(msg.to_string().into_boxed_str())
    }
}

#[cfg(feature = "tonic")]
impl From<ValidationError> for tonic::Status {
    fn from(value: ValidationError) -> Self {
        tonic::Status::internal(value.to_string())
    }
}

impl IntoResponse for ValidationError {
    fn into_response(self) -> axum::response::Response {
        let message = self.to_string();
        HttpErrorResponse {
            code: HttpErrorResponseCode::Internal,
            message: &message,
            details: HttpErrorResponseDetails::default(),
        }
        .into_response()
    }
}

impl From<ValidationError> for axum::response::Response {
    fn from(value: ValidationError) -> Self {
        value.into_response()
    }
}

pub trait TincValidate
where
    Self: TrackerFor,
    Self::Tracker: TrackerWrapper,
{
    fn validate(&self, tracker: Option<&Self::Tracker>) -> Result<(), ValidationError>;

    #[allow(clippy::result_large_err)]
    fn validate_http(
        &self,
        mut state: TrackerSharedState,
        tracker: &Self::Tracker,
    ) -> Result<(), axum::response::Response> {
        tinc_cel::CelMode::Serde.set();

        state.in_scope(|| self.validate(Some(tracker)))?;

        if state.errors.is_empty() {
            Ok(())
        } else {
            let mut details = HttpErrorResponseDetails::default();

            for error in &state.errors {
                details
                    .request
                    .violations
                    .push(HttpErrorResponseRequestViolation {
                        field: error.path.as_ref(),
                        description: error.message(),
                    })
            }

            Err(HttpErrorResponse {
                code: HttpErrorResponseCode::InvalidArgument,
                message: "bad request",
                details,
            }
            .into_response())
        }
    }

    #[cfg(feature = "tonic")]
    #[allow(clippy::result_large_err)]
    fn validate_tonic(&self) -> Result<(), tonic::Status> {
        tinc_cel::CelMode::Proto.set();

        use tonic_types::{ErrorDetails, StatusExt};

        use crate::__private::TrackerSharedState;

        let mut state = TrackerSharedState::default();

        state.in_scope(|| self.validate(None))?;

        if !state.errors.is_empty() {
            let mut details = ErrorDetails::new();

            for error in state.errors {
                details.add_bad_request_violation(error.path.as_ref(), error.message());
            }

            Err(tonic::Status::with_error_details(
                tonic::Code::InvalidArgument,
                "bad request",
                details,
            ))
        } else {
            Ok(())
        }
    }
}

impl<V> TincValidate for Box<V>
where
    V: TincValidate,
    V::Tracker: TrackerWrapper,
{
    fn validate(&self, tracker: Option<&Self::Tracker>) -> Result<(), ValidationError> {
        self.as_ref().validate(tracker.map(|t| t.as_ref()))
    }
}