1use std::fmt;
16use std::iter::FusedIterator;
17use std::slice;
18
19use crate::context::ErrorContextField;
20use crate::context::FieldValueRef;
21use crate::CanonicalCondition;
22use crate::ContextVisibility;
23use crate::ErrorCode;
24use crate::ErrorContext;
25use crate::ErrorDescriptor;
26use crate::ErrorSeverity;
27use crate::Exposure;
28use crate::FieldSchema;
29use crate::ProjectionSpec;
30use crate::RecoveryHint;
31
32#[derive(Clone, Copy, PartialEq, Eq)]
37pub enum ViewContextViolation {
38 UndeclaredField {
40 descriptor: ErrorCode,
42 actual: FieldSchema,
44 },
45 SchemaMismatch {
47 descriptor: ErrorCode,
49 expected: FieldSchema,
51 actual: FieldSchema,
53 },
54}
55
56impl fmt::Debug for ViewContextViolation {
57 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58 match self {
59 Self::UndeclaredField { descriptor, actual } => formatter
60 .debug_struct("UndeclaredField")
61 .field("descriptor", descriptor)
62 .field("actual", actual)
63 .finish(),
64 Self::SchemaMismatch {
65 descriptor,
66 expected,
67 actual,
68 } => formatter
69 .debug_struct("SchemaMismatch")
70 .field("descriptor", descriptor)
71 .field("expected", expected)
72 .field("actual", actual)
73 .finish(),
74 }
75 }
76}
77
78impl fmt::Display for ViewContextViolation {
79 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Self::UndeclaredField { descriptor, actual } => {
82 write!(
83 formatter,
84 "context field `{}` is not declared by {descriptor}",
85 actual.name()
86 )
87 }
88 Self::SchemaMismatch {
89 descriptor,
90 expected,
91 actual,
92 } => write!(
93 formatter,
94 "context field `{}` does not match the schema declared by {descriptor}: expected {:?}, got {:?}",
95 actual.name(),
96 expected,
97 actual,
98 ),
99 }
100 }
101}
102
103pub struct PublicErrorView<'a> {
120 descriptor: &'static ErrorDescriptor,
121 context: &'a ErrorContext,
122}
123
124static EMPTY_ERROR_CONTEXT: ErrorContext = ErrorContext::new();
125
126impl PublicErrorView<'static> {
127 #[inline]
133 pub const fn descriptor_only(descriptor: &'static ErrorDescriptor) -> Self {
134 Self {
135 descriptor,
136 context: &EMPTY_ERROR_CONTEXT,
137 }
138 }
139}
140
141impl<'a> PublicErrorView<'a> {
142 pub fn try_new(
153 descriptor: &'static ErrorDescriptor,
154 context: &'a ErrorContext,
155 ) -> Result<Self, ViewContextViolation> {
156 validate_context(descriptor, context)?;
157 Ok(Self { descriptor, context })
158 }
159
160 #[inline]
162 pub const fn code(&self) -> ErrorCode {
163 self.descriptor.code()
164 }
165
166 #[inline]
168 pub const fn message(&self) -> &'static str {
169 self.descriptor.public_message()
170 }
171
172 #[inline]
176 pub fn fields(&self) -> PublicFields<'a> {
177 PublicFields::new(self.descriptor, self.context)
178 }
179
180 #[inline]
182 pub const fn projection(&self) -> ProjectionSpec {
183 self.descriptor.projection()
184 }
185
186 #[inline]
188 pub const fn is_truncated(&self) -> bool {
189 self.context.is_truncated()
190 }
191}
192
193impl fmt::Debug for PublicErrorView<'_> {
194 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
195 formatter
196 .debug_struct("PublicErrorView")
197 .field("code", &self.code())
198 .field("message", &self.message())
199 .field("fields", &self.fields())
200 .field("projection", &self.projection())
201 .field("truncated", &self.is_truncated())
202 .finish()
203 }
204}
205
206pub struct DiagnosticView<'a> {
215 descriptor: &'static ErrorDescriptor,
216 context: &'a ErrorContext,
217}
218
219impl<'a> DiagnosticView<'a> {
220 pub fn try_new(
227 descriptor: &'static ErrorDescriptor,
228 context: &'a ErrorContext,
229 ) -> Result<Self, ViewContextViolation> {
230 validate_context(descriptor, context)?;
231 Ok(Self { descriptor, context })
232 }
233
234 #[inline]
236 pub const fn code(&self) -> ErrorCode {
237 self.descriptor.code()
238 }
239
240 #[inline]
242 pub const fn message(&self) -> &'static str {
243 self.descriptor.public_message()
244 }
245
246 #[inline]
248 pub const fn condition(&self) -> CanonicalCondition {
249 self.descriptor.condition()
250 }
251
252 #[inline]
254 pub const fn severity(&self) -> ErrorSeverity {
255 self.descriptor.severity()
256 }
257
258 #[inline]
260 pub const fn recovery_hint(&self) -> RecoveryHint {
261 self.descriptor.recovery_hint()
262 }
263
264 #[inline]
266 pub fn fields(&self) -> DiagnosticFields<'a> {
267 DiagnosticFields::new(self.descriptor, self.context)
268 }
269
270 #[inline]
272 pub const fn projection(&self) -> ProjectionSpec {
273 self.descriptor.projection()
274 }
275
276 #[inline]
278 pub const fn is_truncated(&self) -> bool {
279 self.context.is_truncated()
280 }
281}
282
283impl fmt::Debug for DiagnosticView<'_> {
284 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
285 formatter
286 .debug_struct("DiagnosticView")
287 .field("code", &self.code())
288 .field("message", &self.message())
289 .field("condition", &self.condition())
290 .field("severity", &self.severity())
291 .field("recovery_hint", &self.recovery_hint())
292 .field("fields", &self.fields())
293 .field("projection", &self.projection())
294 .field("truncated", &self.is_truncated())
295 .finish()
296 }
297}
298
299#[derive(Clone)]
301pub struct PublicFields<'a> {
302 inner: ViewFields<'a>,
303 enabled: bool,
304}
305
306impl<'a> PublicFields<'a> {
307 fn new(descriptor: &'static ErrorDescriptor, context: &'a ErrorContext) -> Self {
308 Self {
309 inner: ViewFields::new(descriptor, context),
310 enabled: matches!(descriptor.exposure(), Exposure::Public),
311 }
312 }
313}
314
315impl<'a> Iterator for PublicFields<'a> {
316 type Item = ViewFieldRef<'a>;
317
318 fn next(&mut self) -> Option<Self::Item> {
319 if self.enabled {
320 self.inner.next_field(false)
321 } else {
322 None
323 }
324 }
325}
326
327impl FusedIterator for PublicFields<'_> {}
328
329impl fmt::Debug for PublicFields<'_> {
330 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
331 formatter.debug_list().entries(self.clone()).finish()
332 }
333}
334
335#[derive(Clone)]
337pub struct DiagnosticFields<'a> {
338 inner: ViewFields<'a>,
339}
340
341impl<'a> DiagnosticFields<'a> {
342 fn new(descriptor: &'static ErrorDescriptor, context: &'a ErrorContext) -> Self {
343 Self {
344 inner: ViewFields::new(descriptor, context),
345 }
346 }
347}
348
349impl<'a> Iterator for DiagnosticFields<'a> {
350 type Item = ViewFieldRef<'a>;
351
352 fn next(&mut self) -> Option<Self::Item> {
353 self.inner.next_field(true)
354 }
355}
356
357impl FusedIterator for DiagnosticFields<'_> {}
358
359impl fmt::Debug for DiagnosticFields<'_> {
360 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
361 formatter.debug_list().entries(self.clone()).finish()
362 }
363}
364
365#[derive(Clone)]
366struct ViewFields<'a> {
367 descriptor_fields: slice::Iter<'static, FieldSchema>,
368 context: &'a ErrorContext,
369}
370
371impl<'a> ViewFields<'a> {
372 fn new(descriptor: &'static ErrorDescriptor, context: &'a ErrorContext) -> Self {
373 Self {
374 descriptor_fields: descriptor.fields().iter(),
375 context,
376 }
377 }
378
379 fn next_field(&mut self, include_diagnostic: bool) -> Option<ViewFieldRef<'a>> {
380 for schema in self.descriptor_fields.by_ref() {
381 if !include_visibility(schema.visibility(), include_diagnostic) {
382 continue;
383 }
384 let Some(field) = self.context.fields().iter().find(|field| field.schema() == *schema) else {
385 continue;
386 };
387 return Some(ViewFieldRef { field });
388 }
389 None
390 }
391}
392
393fn include_visibility(visibility: ContextVisibility, include_diagnostic: bool) -> bool {
394 match visibility {
395 ContextVisibility::Public => true,
396 ContextVisibility::Diagnostic | ContextVisibility::SecretPresenceOnly => include_diagnostic,
397 }
398}
399
400pub struct ViewFieldRef<'a> {
402 field: &'a ErrorContextField,
403}
404
405impl<'a> ViewFieldRef<'a> {
406 #[inline]
408 pub const fn name(&self) -> &'static str {
409 self.field.name()
410 }
411
412 #[inline]
414 pub const fn visibility(&self) -> ContextVisibility {
415 self.field.visibility()
416 }
417
418 #[inline]
422 pub fn value(&self) -> ViewValueRef<'a> {
423 match self.field.value() {
424 FieldValueRef::Text(value) => ViewValueRef::Text(value),
425 FieldValueRef::I64(value) => ViewValueRef::I64(value),
426 FieldValueRef::U64(value) => ViewValueRef::U64(value),
427 FieldValueRef::Bool(value) => ViewValueRef::Bool(value),
428 FieldValueRef::Presence => ViewValueRef::Redacted,
429 }
430 }
431}
432
433impl fmt::Debug for ViewFieldRef<'_> {
434 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
435 formatter
436 .debug_struct("ViewFieldRef")
437 .field("name", &self.name())
438 .field("visibility", &self.visibility())
439 .field("value", &self.value())
440 .finish()
441 }
442}
443
444#[derive(Clone, Copy, PartialEq, Eq)]
446pub enum ViewValueRef<'a> {
447 Text(&'a str),
449 I64(i64),
451 U64(u64),
453 Bool(bool),
455 Redacted,
457}
458
459impl fmt::Debug for ViewValueRef<'_> {
460 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
461 match self {
462 Self::Text(value) => value.fmt(formatter),
463 Self::I64(value) => value.fmt(formatter),
464 Self::U64(value) => value.fmt(formatter),
465 Self::Bool(value) => value.fmt(formatter),
466 Self::Redacted => formatter.write_str("<redacted>"),
467 }
468 }
469}
470
471fn validate_context(descriptor: &'static ErrorDescriptor, context: &ErrorContext) -> Result<(), ViewContextViolation> {
472 for field in context.fields() {
473 let actual = field.schema();
474 let Some(expected) = descriptor
475 .fields()
476 .iter()
477 .find(|expected| expected.name() == actual.name())
478 else {
479 return Err(ViewContextViolation::UndeclaredField {
480 descriptor: descriptor.code(),
481 actual,
482 });
483 };
484 if *expected != actual {
485 return Err(ViewContextViolation::SchemaMismatch {
486 descriptor: descriptor.code(),
487 expected: *expected,
488 actual,
489 });
490 }
491 }
492 Ok(())
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::fields;
499
500 const BOOLEAN_CODE: ErrorCode = match ErrorCode::try_new("test.view.boolean") {
501 Some(code) => code,
502 None => panic!("synthetic test code must be valid"),
503 };
504 static BOOLEAN_FIELDS: [FieldSchema; 1] = [fields::ATTEMPTED.schema()];
505 static BOOLEAN_DESCRIPTOR: ErrorDescriptor = match ErrorDescriptor::try_new(
506 BOOLEAN_CODE,
507 crate::ErrorClass::INTERNAL,
508 CanonicalCondition::Internal,
509 crate::FaultAttribution::Unknown,
510 crate::ComponentId::CORE,
511 "Synthetic Boolean view test",
512 ErrorSeverity::Info,
513 RecoveryHint::Never,
514 crate::BacktracePolicy::Never,
515 crate::Exposure::Public,
516 crate::ROUTE_TOPIC_NOT_FOUND.projection(),
517 &BOOLEAN_FIELDS,
518 ) {
519 Some(descriptor) => descriptor,
520 None => panic!("synthetic Boolean descriptor must be valid"),
521 };
522
523 const GENERIC_CODE: ErrorCode = match ErrorCode::try_new("test.view.generic") {
524 Some(code) => code,
525 None => panic!("synthetic test code must be valid"),
526 };
527 static GENERIC_FIELDS: [FieldSchema; 1] = [fields::ACTUAL_BYTES.schema()];
528 static GENERIC_DESCRIPTOR: ErrorDescriptor = match ErrorDescriptor::try_new(
529 GENERIC_CODE,
530 crate::ErrorClass::INTERNAL,
531 CanonicalCondition::Internal,
532 crate::FaultAttribution::Unknown,
533 crate::ComponentId::CORE,
534 "Synthetic Generic view test",
535 ErrorSeverity::Info,
536 RecoveryHint::Never,
537 crate::BacktracePolicy::Never,
538 crate::Exposure::Generic,
539 crate::ROUTE_TOPIC_NOT_FOUND.projection(),
540 &GENERIC_FIELDS,
541 ) {
542 Some(descriptor) => descriptor,
543 None => panic!("synthetic Generic descriptor must be valid"),
544 };
545
546 #[test]
547 fn synthetic_boolean_descriptor_exposes_a_borrowed_boolean_value() {
548 let context = ErrorContext::new().with_bool(fields::ATTEMPTED, true);
549 let public = PublicErrorView::try_new(&BOOLEAN_DESCRIPTOR, &context).expect("public view");
550 let diagnostic = DiagnosticView::try_new(&BOOLEAN_DESCRIPTOR, &context).expect("diagnostic view");
551
552 assert!(public.fields().next().is_none());
553 assert_eq!(
554 diagnostic.fields().next().expect("Boolean field").value(),
555 ViewValueRef::Bool(true)
556 );
557 }
558
559 #[test]
560 fn generic_exposure_suppresses_descriptor_declared_public_fields() {
561 let context = ErrorContext::new().with_u64(fields::ACTUAL_BYTES, 42);
562 let public = PublicErrorView::try_new(&GENERIC_DESCRIPTOR, &context).expect("public view");
563 let diagnostic = DiagnosticView::try_new(&GENERIC_DESCRIPTOR, &context).expect("diagnostic view");
564
565 assert!(public.fields().next().is_none());
566 assert_eq!(
567 diagnostic.fields().next().expect("diagnostic field").value(),
568 ViewValueRef::U64(42)
569 );
570 }
571
572 #[test]
573 fn descriptor_only_view_preserves_descriptor_policy_without_context() {
574 let view = PublicErrorView::descriptor_only(&crate::PROTOCOL_BODY_INVALID);
575
576 assert_eq!(view.code(), crate::PROTOCOL_BODY_INVALID.code());
577 assert_eq!(view.message(), crate::PROTOCOL_BODY_INVALID.public_message());
578 assert_eq!(view.projection(), crate::PROTOCOL_BODY_INVALID.projection());
579 assert!(view.fields().next().is_none());
580 assert!(!view.is_truncated());
581 }
582}