1use std::fmt;
16
17use crate::field::BoolField;
18use crate::field::ContextVisibility;
19use crate::field::FieldKey;
20use crate::field::FieldSchema;
21use crate::field::I64Field;
22use crate::field::SecretPresenceField;
23use crate::field::TextField;
24use crate::field::U64Field;
25
26pub const REDACTED: &str = "<redacted>";
28
29#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub struct Sensitive<T> {
32 inner: T,
33}
34
35impl<T> Sensitive<T> {
36 #[inline]
37 pub const fn new(inner: T) -> Self {
39 Self { inner }
40 }
41
42 #[inline]
43 pub const fn expose_secret(&self) -> &T {
45 &self.inner
46 }
47
48 #[inline]
49 pub fn into_inner(self) -> T {
51 self.inner
52 }
53}
54
55impl<T> From<T> for Sensitive<T> {
56 #[inline]
57 fn from(value: T) -> Self {
58 Self::new(value)
59 }
60}
61
62impl<T> fmt::Display for Sensitive<T> {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 f.write_str(REDACTED)
65 }
66}
67
68impl<T> fmt::Debug for Sensitive<T> {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.write_str("Sensitive(<redacted>)")
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
76pub enum FieldValueRef<'a> {
77 Text(&'a str),
79 I64(i64),
81 U64(u64),
83 Bool(bool),
85 Presence,
87}
88
89#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
90enum FieldValue {
91 Text(Box<str>),
92 I64(i64),
93 U64(u64),
94 Bool(bool),
95 Presence,
96}
97
98impl FieldValue {
99 fn as_ref(&self) -> FieldValueRef<'_> {
100 match self {
101 Self::Text(value) => FieldValueRef::Text(value),
102 Self::I64(value) => FieldValueRef::I64(*value),
103 Self::U64(value) => FieldValueRef::U64(*value),
104 Self::Bool(value) => FieldValueRef::Bool(*value),
105 Self::Presence => FieldValueRef::Presence,
106 }
107 }
108}
109
110#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
112pub struct ErrorContextField {
113 schema: FieldSchema,
114 value: FieldValue,
115}
116
117impl ErrorContextField {
118 #[inline]
120 pub const fn schema(&self) -> FieldSchema {
121 self.schema
122 }
123
124 #[inline]
126 pub const fn name(&self) -> &'static str {
127 self.schema.name()
128 }
129
130 #[inline]
132 pub const fn visibility(&self) -> ContextVisibility {
133 self.schema.visibility()
134 }
135
136 #[inline]
143 pub fn value(&self) -> FieldValueRef<'_> {
144 self.value.as_ref()
145 }
146}
147
148impl fmt::Debug for ErrorContextField {
149 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150 formatter
151 .debug_struct("ErrorContextField")
152 .field("name", &self.name())
153 .field("visibility", &self.visibility())
154 .field("value", &RedactedValue(self))
155 .finish()
156 }
157}
158
159#[derive(Clone, Default, PartialEq, Eq)]
165pub struct ErrorContext {
166 fields: Vec<ErrorContextField>,
167 truncated: bool,
168}
169
170impl ErrorContext {
171 const MAX_FIELDS: usize = 16;
172
173 #[inline]
174 pub const fn new() -> Self {
176 Self {
177 fields: Vec::new(),
178 truncated: false,
179 }
180 }
181
182 pub fn with_text(mut self, key: FieldKey<TextField>, value: impl AsRef<str>) -> Self {
187 let schema = key.schema();
188 if !self.reserve_name(schema) {
189 return self;
190 }
191 let limit = schema.text_byte_limit().unwrap_or(crate::field::MAX_TEXT_FIELD_BYTES);
192 let (value, truncated) = normalize_text(value.as_ref(), limit);
193 self.truncated |= truncated;
194 self.fields.push(ErrorContextField {
195 schema,
196 value: FieldValue::Text(value),
197 });
198 self
199 }
200
201 pub fn with_i64(mut self, key: FieldKey<I64Field>, value: i64) -> Self {
203 self.insert_scalar(key.schema(), FieldValue::I64(value));
204 self
205 }
206
207 pub fn with_u64(mut self, key: FieldKey<U64Field>, value: u64) -> Self {
209 self.insert_scalar(key.schema(), FieldValue::U64(value));
210 self
211 }
212
213 pub fn with_bool(mut self, key: FieldKey<BoolField>, value: bool) -> Self {
215 self.insert_scalar(key.schema(), FieldValue::Bool(value));
216 self
217 }
218
219 pub fn with_secret_presence(mut self, key: FieldKey<SecretPresenceField>) -> Self {
221 self.insert_scalar(key.schema(), FieldValue::Presence);
222 self
223 }
224
225 pub fn public_fields(&self) -> impl Iterator<Item = &ErrorContextField> {
227 self.fields()
228 .iter()
229 .filter(|field| matches!(field.visibility(), ContextVisibility::Public))
230 }
231
232 pub(crate) fn fields(&self) -> &[ErrorContextField] {
234 &self.fields
235 }
236
237 #[inline]
238 pub fn is_empty(&self) -> bool {
240 self.fields.is_empty()
241 }
242
243 #[inline]
244 pub fn len(&self) -> usize {
246 self.fields.len()
247 }
248
249 #[inline]
251 pub const fn is_truncated(&self) -> bool {
252 self.truncated
253 }
254
255 fn insert_scalar(&mut self, schema: FieldSchema, value: FieldValue) {
256 if self.reserve_name(schema) {
257 self.fields.push(ErrorContextField { schema, value });
258 }
259 }
260
261 fn reserve_name(&mut self, schema: FieldSchema) -> bool {
262 if self.fields.iter().any(|field| field.name() == schema.name()) || self.fields.len() >= Self::MAX_FIELDS {
263 self.truncated = true;
264 return false;
265 }
266 true
267 }
268}
269
270impl fmt::Display for ErrorContext {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 for (index, field) in self.fields().iter().enumerate() {
273 if index > 0 {
274 f.write_str(", ")?;
275 }
276 write!(f, "{}={}", field.name(), DisplayValue(field))?;
277 }
278 Ok(())
279 }
280}
281
282impl fmt::Debug for ErrorContext {
283 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
284 formatter
285 .debug_struct("ErrorContext")
286 .field("fields", &self.fields)
287 .field("truncated", &self.truncated)
288 .finish()
289 }
290}
291
292struct DisplayValue<'a>(&'a ErrorContextField);
293
294impl fmt::Display for DisplayValue<'_> {
295 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
296 if !matches!(self.0.visibility(), ContextVisibility::Public) {
297 return formatter.write_str(REDACTED);
298 }
299 match self.0.value() {
300 FieldValueRef::Text(value) => formatter.write_str(value),
301 FieldValueRef::I64(value) => value.fmt(formatter),
302 FieldValueRef::U64(value) => value.fmt(formatter),
303 FieldValueRef::Bool(value) => value.fmt(formatter),
304 FieldValueRef::Presence => formatter.write_str(REDACTED),
305 }
306 }
307}
308
309struct RedactedValue<'a>(&'a ErrorContextField);
310
311impl fmt::Debug for RedactedValue<'_> {
312 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
313 write!(formatter, "{}", DisplayValue(self.0))
314 }
315}
316
317fn normalize_text(value: &str, limit: usize) -> (Box<str>, bool) {
318 let mut normalized = String::with_capacity(limit);
319 for character in value.chars() {
320 let character = if character.is_control() { ' ' } else { character };
321 if normalized.len() + character.len_utf8() > limit {
322 return (normalized.into_boxed_str(), true);
323 }
324 normalized.push(character);
325 }
326 (normalized.into_boxed_str(), false)
327}