use std::{fmt::Display, str::FromStr};
use crate::{
attachment, iter::Frames, AttachExt, AttachmentKind, Context, ConversionError, FrameKind,
OptionReport, ParseError, Report, ReportAs,
};
use tonic::{Code, Status};
use tonic_types::{ErrorDetails, StatusExt};
pub const DEBUG_INFO: &str = "details:";
pub const GRPC_ERROR: &str = "GrpcError";
pub trait ErrorStatus<T> {
fn map_err_to_status(self, op: impl FnOnce(String) -> Status) -> Result<T, Status>;
}
impl<T, E> ErrorStatus<T> for Result<T, E>
where
E: Display,
{
fn map_err_to_status(self, op: impl FnOnce(String) -> Status) -> Result<T, Status> {
match self {
Ok(t) => Ok(t),
Err(e) => Err(op(format!("{e}"))),
}
}
}
#[macro_export]
macro_rules! required_field {
($($body:tt)+) => {
bigerror::__field!(
$crate::grpc::missing_field |
$($body)+
)
};
}
#[macro_export]
macro_rules! try_field {
(Report<$to:ty>, $($body:tt)+) => {
bigerror::__field!(
$crate::grpc::try_report_field::<_, $to> |
$($body)+
)
};
($to:ty: $($body:tt)+) => {
bigerror::__field!(
$crate::grpc::try_report_field::<_, $to> |
$($body)+
)
};
($to:ty, $($body:tt)+) => {
bigerror::__field!(
$crate::grpc::try_field::<_, $to> |
$($body)+
)
};
}
#[macro_export]
macro_rules! dyn_field {
($($body:tt)+) => {
{
use $crate::ResultIntoContext;
$crate::expect_field!($($body)+).and_then_ctx(|v| v.try_into())
}
};
}
#[macro_export]
macro_rules! dyn_field_status {
($($body:tt)+) => {
{
use $crate::grpc::ReportStatus;
$crate::dyn_field!($($body)+).to_status(tonic::Code::InvalidArgument)
}
};
}
#[inline]
#[track_caller]
pub fn parse_field<U: FromStr>(value: &str, name: &'static str) -> Result<U, Status>
where
U::Err: Context,
{
U::from_str(value)
.report_as::<ParseError>()
.attach_field_status(name, attachment::Invalid)
.to_status(Code::InvalidArgument)
}
#[inline]
#[track_caller]
pub fn parse_report_field<U: FromStr>(value: &str, name: &'static str) -> Result<U, Status>
where
U::Err: AttachExt + ReportStatus,
{
match U::from_str(value) {
Err(e) => Err(e
.attach_field_status(name, attachment::Invalid)
.to_status(Code::InvalidArgument)),
Ok(v) => Ok(v),
}
}
#[inline]
#[track_caller]
pub fn try_field<T, U>(value: T, name: &'static str) -> Result<U, Status>
where
U: Send + Sync + TryFrom<T>,
U::Error: Context,
{
match U::try_from(value) {
Err(e) => Err(ConversionError::from::<T, U>(e)
.attach_field_status(name, attachment::Invalid)
.to_status(Code::InvalidArgument)),
Ok(v) => Ok(v),
}
}
#[inline]
#[track_caller]
pub fn try_report_field<T, U>(value: T, name: &'static str) -> Result<U, Status>
where
U: Send + Sync + TryFrom<T>,
U::Error: AttachExt + ReportStatus,
{
match U::try_from(value) {
Err(e) => Err(e
.attach_field_status(name, attachment::Invalid)
.to_status(Code::InvalidArgument)),
Ok(v) => Ok(v),
}
}
#[inline]
#[track_caller]
pub fn missing_field<T>(value: Option<T>, name: &'static str) -> Result<T, Status> {
value.expect_field(name).to_status(Code::InvalidArgument)
}
pub trait ReportStatus {
fn to_status(self, code: Code) -> Status;
fn to_msg_status(self, code: Code, msg: impl Into<String>) -> Status;
}
impl<C> ReportStatus for Report<C> {
#[inline]
#[track_caller]
fn to_status(self, code: Code) -> Status {
let msg = self.to_string();
self.to_msg_status(code, msg)
}
#[inline]
#[track_caller]
fn to_msg_status(self, code: Code, msg: impl Into<String>) -> Status {
tracing::error!(err = ?self);
status_with_details(self, code, msg)
}
}
pub trait ResultReportStatus<T, C> {
fn to_status(self, code: Code) -> Result<T, Status>;
fn to_msg_status(self, code: Code, msg: impl Into<String>) -> Result<T, Status>;
}
impl<T, C> ResultReportStatus<T, C> for Result<T, Report<C>> {
#[inline]
#[track_caller]
fn to_status(self, code: tonic::Code) -> Result<T, Status> {
self.map_err(|report| report.to_status(code))
}
#[inline]
#[track_caller]
fn to_msg_status(self, code: tonic::Code, msg: impl Into<String>) -> Result<T, Status> {
self.map_err(|report| report.to_msg_status(code, msg))
}
}
fn status_with_details<C>(report: Report<C>, code: Code, msg: impl Into<String>) -> Status {
let ctx = std::any::type_name::<C>();
let entries = extract_entries(&report);
let error_details = ErrorDetails::with_debug_info(entries, ctx);
Status::with_error_details(code, msg, error_details)
}
fn extract_entries<C>(report: &Report<C>) -> Vec<String> {
frames_to_entries(report.frames())
}
fn status_to_debug_entries(status: &Status) -> Vec<String> {
let mut entries = Vec::new();
entries.push(GRPC_ERROR.to_string());
entries.push(format!("code: {:?}", status.code()));
entries.push(format!("message: {}", status.message()));
if let Some(debug_info) = status.get_details_debug_info() {
entries.push(DEBUG_INFO.to_string());
for entry in debug_info.stack_entries {
entries.push(entry);
}
if debug_info.detail.is_empty() {
entries.push(format!("debug_detail: {}", debug_info.detail));
}
}
entries
}
fn frames_to_entries(frames: Frames) -> Vec<String> {
let mut attachment_idx = 0;
let mut entries = Vec::new();
for frame in frames {
if let Some(status) = frame.downcast_ref::<Status>() {
let status_entries = status_to_debug_entries(status);
for entry in status_entries {
entries.push(entry);
}
continue;
}
match frame.kind() {
FrameKind::Context(context) if frame.sources().is_empty() => {
let type_name: String = format!("{context:?}")
.chars()
.take_while(|c| c.is_alphabetic())
.collect();
let context = format!("{context}");
if type_name == context {
entries.push(type_name.to_string());
} else {
entries.push(format!("{type_name}: {context}"));
}
}
FrameKind::Context(context) => {
entries.push(format!("{context}"));
}
FrameKind::Attachment(AttachmentKind::Printable(attachment)) => {
entries.push(format!("{attachment_idx}: {attachment}"));
if let Some(FrameKind::Context(_)) = frame.sources().first().map(|f| f.kind()) {
attachment_idx = 0;
} else {
attachment_idx += 1;
}
}
FrameKind::Attachment(_) => (),
}
}
entries
}