Skip to main content

rocketmq_error/
descriptor.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;
16
17use crate::field::FieldSchema;
18use crate::projection::ProjectionSpec;
19use crate::CanonicalCondition;
20use crate::RecoveryHint;
21
22/// Broad classification used for stable diagnostic policy.
23///
24/// Values are closed to the associated constants exposed by this crate. The
25/// catalog owns the class for every descriptor; error instances and domain
26/// facades cannot override it.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct ErrorClass(&'static str);
29
30impl ErrorClass {
31    /// Deterministic input or contract validation failure.
32    pub const VALIDATION: Self = Self("validation");
33    /// Authentication failure.
34    pub const AUTHENTICATION: Self = Self("authentication");
35    /// Authorization failure.
36    pub const AUTHORIZATION: Self = Self("authorization");
37    /// Routing or leadership failure.
38    pub const ROUTING: Self = Self("routing");
39    /// Resource-capacity failure.
40    pub const CAPACITY: Self = Self("capacity");
41    /// Deadline or timeout failure.
42    pub const TIMEOUT: Self = Self("timeout");
43    /// Temporarily unavailable service or resource.
44    pub const UNAVAILABLE: Self = Self("unavailable");
45    /// Input/output failure.
46    pub const IO: Self = Self("io");
47    /// Corrupted or irrecoverable data.
48    pub const DATA_CORRUPTION: Self = Self("data_corruption");
49    /// Unsupported operation or capability.
50    pub const UNSUPPORTED: Self = Self("unsupported");
51    /// Internal operational failure.
52    pub const INTERNAL: Self = Self("internal");
53    /// Violation of an implementation invariant.
54    pub const BUG: Self = Self("bug");
55
56    /// Returns the stable class name.
57    #[inline]
58    pub const fn as_str(self) -> &'static str {
59        self.0
60    }
61}
62
63impl fmt::Display for ErrorClass {
64    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65        formatter.write_str(self.0)
66    }
67}
68
69/// Catalog owner for a canonical descriptor.
70///
71/// Domain-specific operation and subcomponent values remain typed context and
72/// do not replace this catalog-level owner.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
74pub struct ComponentId(&'static str);
75
76impl ComponentId {
77    /// Canonical error core.
78    pub const CORE: Self = Self("core");
79    /// Protocol encoding and validation.
80    pub const PROTOCOL: Self = Self("protocol");
81    /// Route discovery and leadership.
82    pub const ROUTE: Self = Self("route");
83    /// Authentication and authorization.
84    pub const AUTH: Self = Self("auth");
85    /// Controller services.
86    pub const CONTROLLER: Self = Self("controller");
87    /// Storage services.
88    pub const STORAGE: Self = Self("storage");
89    /// Runtime services.
90    pub const RUNTIME: Self = Self("runtime");
91    /// Transport services.
92    pub const TRANSPORT: Self = Self("transport");
93    /// Broker services.
94    pub const BROKER: Self = Self("broker");
95    /// Client services.
96    pub const CLIENT: Self = Self("client");
97    /// Proxy services.
98    pub const PROXY: Self = Self("proxy");
99    /// Administrative tools.
100    pub const TOOLS: Self = Self("tools");
101    /// Observability services.
102    pub const OBSERVABILITY: Self = Self("observability");
103
104    /// Returns the stable component name.
105    #[inline]
106    pub const fn as_str(self) -> &'static str {
107        self.0
108    }
109}
110
111impl fmt::Display for ComponentId {
112    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113        formatter.write_str(self.0)
114    }
115}
116
117/// Party or resource attributed with a canonical failure.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
119pub enum FaultAttribution {
120    /// The caller supplied invalid input or violated a contract.
121    Caller,
122    /// A remote peer failed or rejected an operation.
123    RemotePeer,
124    /// A local resource failed or became exhausted.
125    LocalResource,
126    /// A required dependency failed.
127    Dependency,
128    /// Configuration caused the failure.
129    Configuration,
130    /// An implementation invariant was violated.
131    Bug,
132    /// The failure cannot be attributed safely.
133    Unknown,
134}
135
136/// Public-context exposure policy for a descriptor.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
138pub enum Exposure {
139    /// The fixed message and descriptor-approved public fields may be exposed.
140    Public,
141    /// Only the fixed descriptor message may be exposed.
142    Generic,
143}
144
145/// Catalog-controlled backtrace capture policy.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
147pub enum BacktracePolicy {
148    /// Never capture a backtrace.
149    Never,
150    /// Capture only when the standard library's backtrace environment enables it.
151    OnDemand,
152}
153
154/// Default severity for logs, metrics, traces, and alert routing.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
156pub enum ErrorSeverity {
157    /// Diagnostic-only failure.
158    Debug,
159    /// Informational failure.
160    Info,
161    /// Warning-level failure.
162    Warn,
163    /// Error-level failure.
164    Error,
165    /// Critical failure requiring immediate attention.
166    Critical,
167}
168
169/// Stable machine-readable error code.
170///
171/// `ErrorCode` values are intentionally separate from display messages.
172/// Protocol mapping, retry policy, and observability should use the code, not
173/// formatted error text.
174///
175/// [`Self::try_new`] accepts only reviewed catalog identity using the lowercase
176/// dotted grammar. Source display text and runtime values must not become
177/// catalog identity.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
179pub struct ErrorCode(&'static str);
180
181impl ErrorCode {
182    /// Attempts to create a canonical catalog code.
183    ///
184    /// Canonical codes have at least three dot-separated segments. Every
185    /// segment starts with a lowercase ASCII letter and continues with
186    /// lowercase ASCII letters, ASCII digits, or underscores.
187    ///
188    /// Returns [`None`] when `value` does not satisfy that grammar. The
189    /// `'static` input prevents arbitrary runtime strings from becoming catalog
190    /// identity; catalog entries must use reviewed static descriptors.
191    #[inline]
192    pub const fn try_new(value: &'static str) -> Option<Self> {
193        if is_valid_catalog_code(value) {
194            Some(Self(value))
195        } else {
196            None
197        }
198    }
199
200    /// Returns the stable code string.
201    #[inline]
202    pub const fn as_str(self) -> &'static str {
203        self.0
204    }
205}
206
207impl fmt::Display for ErrorCode {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        f.write_str(self.0)
210    }
211}
212
213/// Immutable catalog metadata for one canonical error identity.
214///
215/// A descriptor owns stable identity, classification, fault attribution,
216/// component ownership, the fixed public message, operational policy, public
217/// exposure, backtrace capture, and explicit boundary projections. Its ordered
218/// field schemas define the only context accepted by descriptor-aware views.
219/// Catalog consumers can inspect this metadata but cannot construct or modify
220/// descriptors outside this crate.
221///
222/// ```compile_fail
223/// use rocketmq_error::{
224///     BacktracePolicy, CanonicalCondition, ComponentId, ErrorClass, ErrorCode,
225///     ErrorDescriptor, ErrorSeverity, Exposure, FaultAttribution, RecoveryHint,
226/// };
227///
228/// let descriptor = ErrorDescriptor {
229///     code: ErrorCode::try_new("example.operation.failed").unwrap(),
230///     class: ErrorClass::INTERNAL,
231///     condition: CanonicalCondition::Internal,
232///     fault: FaultAttribution::Unknown,
233///     component: ComponentId::CORE,
234///     public_message: "The operation failed",
235///     severity: ErrorSeverity::Error,
236///     recovery_hint: RecoveryHint::OperatorAction,
237///     backtrace: BacktracePolicy::OnDemand,
238///     exposure: Exposure::Generic,
239///     projection: todo!(),
240///     fields: &[],
241/// };
242/// ```
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244pub struct ErrorDescriptor {
245    code: ErrorCode,
246    class: ErrorClass,
247    condition: CanonicalCondition,
248    fault: FaultAttribution,
249    component: ComponentId,
250    public_message: &'static str,
251    severity: ErrorSeverity,
252    recovery_hint: RecoveryHint,
253    backtrace: BacktracePolicy,
254    exposure: Exposure,
255    projection: ProjectionSpec,
256    fields: &'static [FieldSchema],
257}
258
259impl ErrorDescriptor {
260    #[inline]
261    pub(crate) const fn try_new(
262        code: ErrorCode,
263        class: ErrorClass,
264        condition: CanonicalCondition,
265        fault: FaultAttribution,
266        component: ComponentId,
267        public_message: &'static str,
268        severity: ErrorSeverity,
269        recovery_hint: RecoveryHint,
270        backtrace: BacktracePolicy,
271        exposure: Exposure,
272        projection: ProjectionSpec,
273        fields: &'static [FieldSchema],
274    ) -> Option<Self> {
275        if !valid_descriptor_fields(fields) {
276            return None;
277        }
278        Some(Self {
279            code,
280            class,
281            condition,
282            fault,
283            component,
284            public_message,
285            severity,
286            recovery_hint,
287            backtrace,
288            exposure,
289            projection,
290            fields,
291        })
292    }
293
294    /// Returns the stable dotted catalog code.
295    #[inline]
296    pub const fn code(&self) -> ErrorCode {
297        self.code
298    }
299
300    /// Returns the broad catalog-owned class.
301    #[inline]
302    pub const fn class(&self) -> ErrorClass {
303        self.class
304    }
305
306    /// Returns the protocol-independent canonical condition.
307    #[inline]
308    pub const fn condition(&self) -> CanonicalCondition {
309        self.condition
310    }
311
312    /// Returns the catalog-owned fault attribution.
313    #[inline]
314    pub const fn fault(&self) -> FaultAttribution {
315        self.fault
316    }
317
318    /// Returns the catalog component that owns this descriptor.
319    #[inline]
320    pub const fn component(&self) -> ComponentId {
321        self.component
322    }
323
324    /// Returns the fixed, boundary-safe public message.
325    #[inline]
326    pub const fn public_message(&self) -> &'static str {
327        self.public_message
328    }
329
330    /// Returns the operational severity.
331    #[inline]
332    pub const fn severity(&self) -> ErrorSeverity {
333        self.severity
334    }
335
336    /// Returns the catalog-owned recovery advice.
337    #[inline]
338    pub const fn recovery_hint(&self) -> RecoveryHint {
339        self.recovery_hint
340    }
341
342    /// Returns the catalog-controlled backtrace policy.
343    #[inline]
344    pub const fn backtrace_policy(&self) -> BacktracePolicy {
345        self.backtrace
346    }
347
348    /// Returns the public-context exposure policy.
349    #[inline]
350    pub const fn exposure(&self) -> Exposure {
351        self.exposure
352    }
353
354    /// Returns the explicit boundary projections.
355    #[inline]
356    pub const fn projection(&self) -> ProjectionSpec {
357        self.projection
358    }
359
360    /// Returns allowed context schemas in catalog declaration order.
361    #[inline]
362    pub const fn fields(&self) -> &'static [FieldSchema] {
363        self.fields
364    }
365}
366
367const fn valid_descriptor_fields(fields: &[FieldSchema]) -> bool {
368    if fields.len() > 16 {
369        return false;
370    }
371    let mut index = 0;
372    while index < fields.len() {
373        let mut other = index + 1;
374        while other < fields.len() {
375            if const_str_eq(fields[index].name(), fields[other].name()) {
376                return false;
377            }
378            other += 1;
379        }
380        index += 1;
381    }
382    true
383}
384
385const fn const_str_eq(left: &str, right: &str) -> bool {
386    let left = left.as_bytes();
387    let right = right.as_bytes();
388    if left.len() != right.len() {
389        return false;
390    }
391    let mut index = 0;
392    while index < left.len() {
393        if left[index] != right[index] {
394            return false;
395        }
396        index += 1;
397    }
398    true
399}
400
401const fn is_valid_catalog_code(value: &str) -> bool {
402    let bytes = value.as_bytes();
403    let mut index = 0;
404    let mut segment_count = 0;
405    let mut starts_segment = true;
406
407    while index < bytes.len() {
408        let byte = bytes[index];
409
410        if starts_segment {
411            if !byte.is_ascii_lowercase() {
412                return false;
413            }
414            starts_segment = false;
415            segment_count += 1;
416        } else if byte == b'.' {
417            starts_segment = true;
418        } else if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() && byte != b'_' {
419            return false;
420        }
421
422        index += 1;
423    }
424
425    !starts_segment && segment_count >= 3
426}
427
428#[cfg(test)]
429mod tests {
430    use crate::fields;
431
432    use super::*;
433
434    #[test]
435    fn descriptor_field_lists_reject_duplicate_names_and_capacity_overflow() {
436        let duplicate = [fields::OPERATION.schema(), fields::OPERATION_DIAGNOSTIC.schema()];
437        assert!(!valid_descriptor_fields(&duplicate));
438
439        let too_many = [fields::TOPIC.schema(); 17];
440        assert!(!valid_descriptor_fields(&too_many));
441    }
442}