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}