use std::fmt;
use std::iter::FusedIterator;
use std::slice;
use crate::context::ErrorContextField;
use crate::context::FieldValueRef;
use crate::CanonicalCondition;
use crate::ContextVisibility;
use crate::ErrorCode;
use crate::ErrorContext;
use crate::ErrorDescriptor;
use crate::ErrorSeverity;
use crate::Exposure;
use crate::FieldSchema;
use crate::ProjectionSpec;
use crate::RecoveryHint;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ViewContextViolation {
UndeclaredField {
descriptor: ErrorCode,
actual: FieldSchema,
},
SchemaMismatch {
descriptor: ErrorCode,
expected: FieldSchema,
actual: FieldSchema,
},
}
impl fmt::Debug for ViewContextViolation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UndeclaredField { descriptor, actual } => formatter
.debug_struct("UndeclaredField")
.field("descriptor", descriptor)
.field("actual", actual)
.finish(),
Self::SchemaMismatch {
descriptor,
expected,
actual,
} => formatter
.debug_struct("SchemaMismatch")
.field("descriptor", descriptor)
.field("expected", expected)
.field("actual", actual)
.finish(),
}
}
}
impl fmt::Display for ViewContextViolation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UndeclaredField { descriptor, actual } => {
write!(
formatter,
"context field `{}` is not declared by {descriptor}",
actual.name()
)
}
Self::SchemaMismatch {
descriptor,
expected,
actual,
} => write!(
formatter,
"context field `{}` does not match the schema declared by {descriptor}: expected {:?}, got {:?}",
actual.name(),
expected,
actual,
),
}
}
}
pub struct PublicErrorView<'a> {
descriptor: &'static ErrorDescriptor,
context: &'a ErrorContext,
}
static EMPTY_ERROR_CONTEXT: ErrorContext = ErrorContext::new();
impl PublicErrorView<'static> {
#[inline]
pub const fn descriptor_only(descriptor: &'static ErrorDescriptor) -> Self {
Self {
descriptor,
context: &EMPTY_ERROR_CONTEXT,
}
}
}
impl<'a> PublicErrorView<'a> {
pub fn try_new(
descriptor: &'static ErrorDescriptor,
context: &'a ErrorContext,
) -> Result<Self, ViewContextViolation> {
validate_context(descriptor, context)?;
Ok(Self { descriptor, context })
}
#[inline]
pub const fn code(&self) -> ErrorCode {
self.descriptor.code()
}
#[inline]
pub const fn message(&self) -> &'static str {
self.descriptor.public_message()
}
#[inline]
pub fn fields(&self) -> PublicFields<'a> {
PublicFields::new(self.descriptor, self.context)
}
#[inline]
pub const fn projection(&self) -> ProjectionSpec {
self.descriptor.projection()
}
#[inline]
pub const fn is_truncated(&self) -> bool {
self.context.is_truncated()
}
}
impl fmt::Debug for PublicErrorView<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PublicErrorView")
.field("code", &self.code())
.field("message", &self.message())
.field("fields", &self.fields())
.field("projection", &self.projection())
.field("truncated", &self.is_truncated())
.finish()
}
}
pub struct DiagnosticView<'a> {
descriptor: &'static ErrorDescriptor,
context: &'a ErrorContext,
}
impl<'a> DiagnosticView<'a> {
pub fn try_new(
descriptor: &'static ErrorDescriptor,
context: &'a ErrorContext,
) -> Result<Self, ViewContextViolation> {
validate_context(descriptor, context)?;
Ok(Self { descriptor, context })
}
#[inline]
pub const fn code(&self) -> ErrorCode {
self.descriptor.code()
}
#[inline]
pub const fn message(&self) -> &'static str {
self.descriptor.public_message()
}
#[inline]
pub const fn condition(&self) -> CanonicalCondition {
self.descriptor.condition()
}
#[inline]
pub const fn severity(&self) -> ErrorSeverity {
self.descriptor.severity()
}
#[inline]
pub const fn recovery_hint(&self) -> RecoveryHint {
self.descriptor.recovery_hint()
}
#[inline]
pub fn fields(&self) -> DiagnosticFields<'a> {
DiagnosticFields::new(self.descriptor, self.context)
}
#[inline]
pub const fn projection(&self) -> ProjectionSpec {
self.descriptor.projection()
}
#[inline]
pub const fn is_truncated(&self) -> bool {
self.context.is_truncated()
}
}
impl fmt::Debug for DiagnosticView<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DiagnosticView")
.field("code", &self.code())
.field("message", &self.message())
.field("condition", &self.condition())
.field("severity", &self.severity())
.field("recovery_hint", &self.recovery_hint())
.field("fields", &self.fields())
.field("projection", &self.projection())
.field("truncated", &self.is_truncated())
.finish()
}
}
#[derive(Clone)]
pub struct PublicFields<'a> {
inner: ViewFields<'a>,
enabled: bool,
}
impl<'a> PublicFields<'a> {
fn new(descriptor: &'static ErrorDescriptor, context: &'a ErrorContext) -> Self {
Self {
inner: ViewFields::new(descriptor, context),
enabled: matches!(descriptor.exposure(), Exposure::Public),
}
}
}
impl<'a> Iterator for PublicFields<'a> {
type Item = ViewFieldRef<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.enabled {
self.inner.next_field(false)
} else {
None
}
}
}
impl FusedIterator for PublicFields<'_> {}
impl fmt::Debug for PublicFields<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_list().entries(self.clone()).finish()
}
}
#[derive(Clone)]
pub struct DiagnosticFields<'a> {
inner: ViewFields<'a>,
}
impl<'a> DiagnosticFields<'a> {
fn new(descriptor: &'static ErrorDescriptor, context: &'a ErrorContext) -> Self {
Self {
inner: ViewFields::new(descriptor, context),
}
}
}
impl<'a> Iterator for DiagnosticFields<'a> {
type Item = ViewFieldRef<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next_field(true)
}
}
impl FusedIterator for DiagnosticFields<'_> {}
impl fmt::Debug for DiagnosticFields<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_list().entries(self.clone()).finish()
}
}
#[derive(Clone)]
struct ViewFields<'a> {
descriptor_fields: slice::Iter<'static, FieldSchema>,
context: &'a ErrorContext,
}
impl<'a> ViewFields<'a> {
fn new(descriptor: &'static ErrorDescriptor, context: &'a ErrorContext) -> Self {
Self {
descriptor_fields: descriptor.fields().iter(),
context,
}
}
fn next_field(&mut self, include_diagnostic: bool) -> Option<ViewFieldRef<'a>> {
for schema in self.descriptor_fields.by_ref() {
if !include_visibility(schema.visibility(), include_diagnostic) {
continue;
}
let Some(field) = self.context.fields().iter().find(|field| field.schema() == *schema) else {
continue;
};
return Some(ViewFieldRef { field });
}
None
}
}
fn include_visibility(visibility: ContextVisibility, include_diagnostic: bool) -> bool {
match visibility {
ContextVisibility::Public => true,
ContextVisibility::Diagnostic | ContextVisibility::SecretPresenceOnly => include_diagnostic,
}
}
pub struct ViewFieldRef<'a> {
field: &'a ErrorContextField,
}
impl<'a> ViewFieldRef<'a> {
#[inline]
pub const fn name(&self) -> &'static str {
self.field.name()
}
#[inline]
pub const fn visibility(&self) -> ContextVisibility {
self.field.visibility()
}
#[inline]
pub fn value(&self) -> ViewValueRef<'a> {
match self.field.value() {
FieldValueRef::Text(value) => ViewValueRef::Text(value),
FieldValueRef::I64(value) => ViewValueRef::I64(value),
FieldValueRef::U64(value) => ViewValueRef::U64(value),
FieldValueRef::Bool(value) => ViewValueRef::Bool(value),
FieldValueRef::Presence => ViewValueRef::Redacted,
}
}
}
impl fmt::Debug for ViewFieldRef<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ViewFieldRef")
.field("name", &self.name())
.field("visibility", &self.visibility())
.field("value", &self.value())
.finish()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ViewValueRef<'a> {
Text(&'a str),
I64(i64),
U64(u64),
Bool(bool),
Redacted,
}
impl fmt::Debug for ViewValueRef<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Text(value) => value.fmt(formatter),
Self::I64(value) => value.fmt(formatter),
Self::U64(value) => value.fmt(formatter),
Self::Bool(value) => value.fmt(formatter),
Self::Redacted => formatter.write_str("<redacted>"),
}
}
}
fn validate_context(descriptor: &'static ErrorDescriptor, context: &ErrorContext) -> Result<(), ViewContextViolation> {
for field in context.fields() {
let actual = field.schema();
let Some(expected) = descriptor
.fields()
.iter()
.find(|expected| expected.name() == actual.name())
else {
return Err(ViewContextViolation::UndeclaredField {
descriptor: descriptor.code(),
actual,
});
};
if *expected != actual {
return Err(ViewContextViolation::SchemaMismatch {
descriptor: descriptor.code(),
expected: *expected,
actual,
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fields;
const BOOLEAN_CODE: ErrorCode = match ErrorCode::try_new("test.view.boolean") {
Some(code) => code,
None => panic!("synthetic test code must be valid"),
};
static BOOLEAN_FIELDS: [FieldSchema; 1] = [fields::ATTEMPTED.schema()];
static BOOLEAN_DESCRIPTOR: ErrorDescriptor = match ErrorDescriptor::try_new(
BOOLEAN_CODE,
crate::ErrorClass::INTERNAL,
CanonicalCondition::Internal,
crate::FaultAttribution::Unknown,
crate::ComponentId::CORE,
"Synthetic Boolean view test",
ErrorSeverity::Info,
RecoveryHint::Never,
crate::BacktracePolicy::Never,
crate::Exposure::Public,
crate::ROUTE_TOPIC_NOT_FOUND.projection(),
&BOOLEAN_FIELDS,
) {
Some(descriptor) => descriptor,
None => panic!("synthetic Boolean descriptor must be valid"),
};
const GENERIC_CODE: ErrorCode = match ErrorCode::try_new("test.view.generic") {
Some(code) => code,
None => panic!("synthetic test code must be valid"),
};
static GENERIC_FIELDS: [FieldSchema; 1] = [fields::ACTUAL_BYTES.schema()];
static GENERIC_DESCRIPTOR: ErrorDescriptor = match ErrorDescriptor::try_new(
GENERIC_CODE,
crate::ErrorClass::INTERNAL,
CanonicalCondition::Internal,
crate::FaultAttribution::Unknown,
crate::ComponentId::CORE,
"Synthetic Generic view test",
ErrorSeverity::Info,
RecoveryHint::Never,
crate::BacktracePolicy::Never,
crate::Exposure::Generic,
crate::ROUTE_TOPIC_NOT_FOUND.projection(),
&GENERIC_FIELDS,
) {
Some(descriptor) => descriptor,
None => panic!("synthetic Generic descriptor must be valid"),
};
#[test]
fn synthetic_boolean_descriptor_exposes_a_borrowed_boolean_value() {
let context = ErrorContext::new().with_bool(fields::ATTEMPTED, true);
let public = PublicErrorView::try_new(&BOOLEAN_DESCRIPTOR, &context).expect("public view");
let diagnostic = DiagnosticView::try_new(&BOOLEAN_DESCRIPTOR, &context).expect("diagnostic view");
assert!(public.fields().next().is_none());
assert_eq!(
diagnostic.fields().next().expect("Boolean field").value(),
ViewValueRef::Bool(true)
);
}
#[test]
fn generic_exposure_suppresses_descriptor_declared_public_fields() {
let context = ErrorContext::new().with_u64(fields::ACTUAL_BYTES, 42);
let public = PublicErrorView::try_new(&GENERIC_DESCRIPTOR, &context).expect("public view");
let diagnostic = DiagnosticView::try_new(&GENERIC_DESCRIPTOR, &context).expect("diagnostic view");
assert!(public.fields().next().is_none());
assert_eq!(
diagnostic.fields().next().expect("diagnostic field").value(),
ViewValueRef::U64(42)
);
}
#[test]
fn descriptor_only_view_preserves_descriptor_policy_without_context() {
let view = PublicErrorView::descriptor_only(&crate::PROTOCOL_BODY_INVALID);
assert_eq!(view.code(), crate::PROTOCOL_BODY_INVALID.code());
assert_eq!(view.message(), crate::PROTOCOL_BODY_INVALID.public_message());
assert_eq!(view.projection(), crate::PROTOCOL_BODY_INVALID.projection());
assert!(view.fields().next().is_none());
assert!(!view.is_truncated());
}
}