use std::fmt;
use crate::field::BoolField;
use crate::field::ContextVisibility;
use crate::field::FieldKey;
use crate::field::FieldSchema;
use crate::field::I64Field;
use crate::field::SecretPresenceField;
use crate::field::TextField;
use crate::field::U64Field;
pub const REDACTED: &str = "<redacted>";
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Sensitive<T> {
inner: T,
}
impl<T> Sensitive<T> {
#[inline]
pub const fn new(inner: T) -> Self {
Self { inner }
}
#[inline]
pub const fn expose_secret(&self) -> &T {
&self.inner
}
#[inline]
pub fn into_inner(self) -> T {
self.inner
}
}
impl<T> From<T> for Sensitive<T> {
#[inline]
fn from(value: T) -> Self {
Self::new(value)
}
}
impl<T> fmt::Display for Sensitive<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(REDACTED)
}
}
impl<T> fmt::Debug for Sensitive<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Sensitive(<redacted>)")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FieldValueRef<'a> {
Text(&'a str),
I64(i64),
U64(u64),
Bool(bool),
Presence,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum FieldValue {
Text(Box<str>),
I64(i64),
U64(u64),
Bool(bool),
Presence,
}
impl FieldValue {
fn as_ref(&self) -> FieldValueRef<'_> {
match self {
Self::Text(value) => FieldValueRef::Text(value),
Self::I64(value) => FieldValueRef::I64(*value),
Self::U64(value) => FieldValueRef::U64(*value),
Self::Bool(value) => FieldValueRef::Bool(*value),
Self::Presence => FieldValueRef::Presence,
}
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ErrorContextField {
schema: FieldSchema,
value: FieldValue,
}
impl ErrorContextField {
#[inline]
pub const fn schema(&self) -> FieldSchema {
self.schema
}
#[inline]
pub const fn name(&self) -> &'static str {
self.schema.name()
}
#[inline]
pub const fn visibility(&self) -> ContextVisibility {
self.schema.visibility()
}
#[inline]
pub fn value(&self) -> FieldValueRef<'_> {
self.value.as_ref()
}
}
impl fmt::Debug for ErrorContextField {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ErrorContextField")
.field("name", &self.name())
.field("visibility", &self.visibility())
.field("value", &RedactedValue(self))
.finish()
}
}
#[derive(Clone, Default, PartialEq, Eq)]
pub struct ErrorContext {
fields: Vec<ErrorContextField>,
truncated: bool,
}
impl ErrorContext {
const MAX_FIELDS: usize = 16;
#[inline]
pub const fn new() -> Self {
Self {
fields: Vec::new(),
truncated: false,
}
}
pub fn with_text(mut self, key: FieldKey<TextField>, value: impl AsRef<str>) -> Self {
let schema = key.schema();
if !self.reserve_name(schema) {
return self;
}
let limit = schema.text_byte_limit().unwrap_or(crate::field::MAX_TEXT_FIELD_BYTES);
let (value, truncated) = normalize_text(value.as_ref(), limit);
self.truncated |= truncated;
self.fields.push(ErrorContextField {
schema,
value: FieldValue::Text(value),
});
self
}
pub fn with_i64(mut self, key: FieldKey<I64Field>, value: i64) -> Self {
self.insert_scalar(key.schema(), FieldValue::I64(value));
self
}
pub fn with_u64(mut self, key: FieldKey<U64Field>, value: u64) -> Self {
self.insert_scalar(key.schema(), FieldValue::U64(value));
self
}
pub fn with_bool(mut self, key: FieldKey<BoolField>, value: bool) -> Self {
self.insert_scalar(key.schema(), FieldValue::Bool(value));
self
}
pub fn with_secret_presence(mut self, key: FieldKey<SecretPresenceField>) -> Self {
self.insert_scalar(key.schema(), FieldValue::Presence);
self
}
pub fn public_fields(&self) -> impl Iterator<Item = &ErrorContextField> {
self.fields()
.iter()
.filter(|field| matches!(field.visibility(), ContextVisibility::Public))
}
pub(crate) fn fields(&self) -> &[ErrorContextField] {
&self.fields
}
#[inline]
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
#[inline]
pub fn len(&self) -> usize {
self.fields.len()
}
#[inline]
pub const fn is_truncated(&self) -> bool {
self.truncated
}
fn insert_scalar(&mut self, schema: FieldSchema, value: FieldValue) {
if self.reserve_name(schema) {
self.fields.push(ErrorContextField { schema, value });
}
}
fn reserve_name(&mut self, schema: FieldSchema) -> bool {
if self.fields.iter().any(|field| field.name() == schema.name()) || self.fields.len() >= Self::MAX_FIELDS {
self.truncated = true;
return false;
}
true
}
}
impl fmt::Display for ErrorContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, field) in self.fields().iter().enumerate() {
if index > 0 {
f.write_str(", ")?;
}
write!(f, "{}={}", field.name(), DisplayValue(field))?;
}
Ok(())
}
}
impl fmt::Debug for ErrorContext {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ErrorContext")
.field("fields", &self.fields)
.field("truncated", &self.truncated)
.finish()
}
}
struct DisplayValue<'a>(&'a ErrorContextField);
impl fmt::Display for DisplayValue<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if !matches!(self.0.visibility(), ContextVisibility::Public) {
return formatter.write_str(REDACTED);
}
match self.0.value() {
FieldValueRef::Text(value) => formatter.write_str(value),
FieldValueRef::I64(value) => value.fmt(formatter),
FieldValueRef::U64(value) => value.fmt(formatter),
FieldValueRef::Bool(value) => value.fmt(formatter),
FieldValueRef::Presence => formatter.write_str(REDACTED),
}
}
}
struct RedactedValue<'a>(&'a ErrorContextField);
impl fmt::Debug for RedactedValue<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", DisplayValue(self.0))
}
}
fn normalize_text(value: &str, limit: usize) -> (Box<str>, bool) {
let mut normalized = String::with_capacity(limit);
for character in value.chars() {
let character = if character.is_control() { ' ' } else { character };
if normalized.len() + character.len_utf8() > limit {
return (normalized.into_boxed_str(), true);
}
normalized.push(character);
}
(normalized.into_boxed_str(), false)
}