Skip to main content

rocketmq_error/
view.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// A descriptor/context contract violation discovered while creating a safe view.
33///
34/// Violations carry only catalog schema metadata. They never include a runtime
35/// context value, source error, or backtrace.
36#[derive(Clone, Copy, PartialEq, Eq)]
37pub enum ViewContextViolation {
38    /// A retained context field is not declared by the selected descriptor.
39    UndeclaredField {
40        /// The selected descriptor's stable code.
41        descriptor: ErrorCode,
42        /// The schema retained by the context.
43        actual: FieldSchema,
44    },
45    /// A retained context field has the same name as a descriptor field but a different schema.
46    SchemaMismatch {
47        /// The selected descriptor's stable code.
48        descriptor: ErrorCode,
49        /// The schema required by the descriptor.
50        expected: FieldSchema,
51        /// The schema retained by the context.
52        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
103/// A borrowed public projection of one descriptor-validated error context.
104///
105/// This view exposes only the descriptor's stable identity, fixed public
106/// message, and descriptor-owned protocol projections. Descriptors with
107/// [`Exposure::Public`] may additionally expose declared public context fields;
108/// [`Exposure::Generic`] descriptors expose no dynamic fields. The view borrows
109/// its inputs and performs no allocation after the [`ErrorContext`] has been
110/// constructed.
111///
112/// ```compile_fail,E0616
113/// use rocketmq_error::{ErrorContext, PublicErrorView, ROUTE_TOPIC_NOT_FOUND};
114///
115/// let context = ErrorContext::new();
116/// let view = PublicErrorView::try_new(&ROUTE_TOPIC_NOT_FOUND, &context).unwrap();
117/// let _ = view.context;
118/// ```
119pub 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    /// Creates a public view containing descriptor-owned data only.
128    ///
129    /// This is the safe fallback when retained context fails descriptor
130    /// validation. It preserves the original descriptor and performs no
131    /// allocation.
132    #[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    /// Validates `context` against `descriptor` and creates a public safe view.
143    ///
144    /// Every retained context field must exactly match a schema declared by
145    /// `descriptor`. Missing fields are valid because descriptor schemas do
146    /// not currently define required values.
147    ///
148    /// # Errors
149    ///
150    /// Returns [`ViewContextViolation`] for the first retained context field
151    /// that is undeclared or has a different complete schema.
152    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    /// Returns the stable catalog code.
161    #[inline]
162    pub const fn code(&self) -> ErrorCode {
163        self.descriptor.code()
164    }
165
166    /// Returns the descriptor's fixed public message.
167    #[inline]
168    pub const fn message(&self) -> &'static str {
169        self.descriptor.public_message()
170    }
171
172    /// Iterates permitted public fields in descriptor declaration order.
173    ///
174    /// Generic-exposure descriptors always return an empty iterator.
175    #[inline]
176    pub fn fields(&self) -> PublicFields<'a> {
177        PublicFields::new(self.descriptor, self.context)
178    }
179
180    /// Returns descriptor-owned boundary projections.
181    #[inline]
182    pub const fn projection(&self) -> ProjectionSpec {
183        self.descriptor.projection()
184    }
185
186    /// Returns whether context construction normalized, discarded, or truncated a value.
187    #[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
206/// A borrowed controlled-diagnostics projection of one descriptor-validated context.
207///
208/// `DiagnosticView` exposes descriptor-approved diagnostic fields and
209/// key-only secret-presence markers for operational diagnostics. It never
210/// exposes source errors, caller locations, or backtraces and must not be
211/// serialized into public protocol responses. Like [`PublicErrorView`], it
212/// borrows its inputs and performs no allocation after [`ErrorContext`] has
213/// been constructed.
214pub struct DiagnosticView<'a> {
215    descriptor: &'static ErrorDescriptor,
216    context: &'a ErrorContext,
217}
218
219impl<'a> DiagnosticView<'a> {
220    /// Validates `context` against `descriptor` and creates a controlled diagnostic view.
221    ///
222    /// # Errors
223    ///
224    /// Returns [`ViewContextViolation`] for the first retained context field
225    /// that is undeclared or has a different complete schema.
226    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    /// Returns the stable catalog code.
235    #[inline]
236    pub const fn code(&self) -> ErrorCode {
237        self.descriptor.code()
238    }
239
240    /// Returns the descriptor's fixed public message.
241    #[inline]
242    pub const fn message(&self) -> &'static str {
243        self.descriptor.public_message()
244    }
245
246    /// Returns the descriptor's protocol-independent canonical condition.
247    #[inline]
248    pub const fn condition(&self) -> CanonicalCondition {
249        self.descriptor.condition()
250    }
251
252    /// Returns the descriptor's operational severity.
253    #[inline]
254    pub const fn severity(&self) -> ErrorSeverity {
255        self.descriptor.severity()
256    }
257
258    /// Returns the descriptor's recovery advice.
259    #[inline]
260    pub const fn recovery_hint(&self) -> RecoveryHint {
261        self.descriptor.recovery_hint()
262    }
263
264    /// Iterates present public, diagnostic, and redacted secret-presence fields.
265    #[inline]
266    pub fn fields(&self) -> DiagnosticFields<'a> {
267        DiagnosticFields::new(self.descriptor, self.context)
268    }
269
270    /// Returns descriptor-owned boundary projections.
271    #[inline]
272    pub const fn projection(&self) -> ProjectionSpec {
273        self.descriptor.projection()
274    }
275
276    /// Returns whether context construction normalized, discarded, or truncated a value.
277    #[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/// A borrowed descriptor-ordered iterator over public context fields.
300#[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/// A borrowed descriptor-ordered iterator over controlled diagnostic fields.
336#[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
400/// A borrowed field exposed by a descriptor-validated view.
401pub struct ViewFieldRef<'a> {
402    field: &'a ErrorContextField,
403}
404
405impl<'a> ViewFieldRef<'a> {
406    /// Returns the descriptor-approved external field name.
407    #[inline]
408    pub const fn name(&self) -> &'static str {
409        self.field.name()
410    }
411
412    /// Returns the descriptor-approved field visibility.
413    #[inline]
414    pub const fn visibility(&self) -> ContextVisibility {
415        self.field.visibility()
416    }
417
418    /// Returns the safe borrowed value for this field.
419    ///
420    /// Secret-presence fields always return [`ViewValueRef::Redacted`].
421    #[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/// A safe borrowed value exposed by a descriptor-validated view.
445#[derive(Clone, Copy, PartialEq, Eq)]
446pub enum ViewValueRef<'a> {
447    /// Bounded normalized text.
448    Text(&'a str),
449    /// A signed integer.
450    I64(i64),
451    /// An unsigned integer.
452    U64(u64),
453    /// A Boolean value.
454    Bool(bool),
455    /// A key-only marker for secret-bearing input.
456    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}