Skip to main content

tuwunel_core/error/
err.rs

1//! Error construction macros
2//!
3//! These are specialized macros specific to this project's patterns for
4//! throwing Errors; they make Error construction succinct and reduce clutter.
5//! They are developed from folding existing patterns into the macro while
6//! fixing several anti-patterns in the codebase.
7//!
8//! - The primary macros `Err!` and `err!` are provided. `Err!` simply wraps
9//!   `err!` in the Result variant to reduce `Err(err!(...))` boilerplate, thus
10//!   `err!` can be used in any case.
11//!
12//! 1. The macro makes the general Error construction easy: `return
13//!    Err!("something went wrong")` replaces the prior `return
14//!    Err(Error::Err("something went wrong".to_owned()))`.
15//!
16//! 2. The macro integrates format strings automatically: `return
17//!    Err!("something bad: {msg}")` replaces the prior `return
18//!    Err(Error::Err(format!("something bad: {msg}")))`.
19//!
20//! 3. The macro scopes variants of Error: `return Err!(Database("problem with
21//!    bad database."))` replaces the prior `return Err(Error::Database("problem
22//!    with bad database."))`.
23//!
24//! 4. The macro matches and scopes some special-case sub-variants, for example
25//!    with ruma ErrorKind: `return Err!(Request(MissingToken("you must provide
26//!    an access token")))`.
27//!
28//! 5. The macro fixes the anti-pattern of repeating messages in an error! log
29//!    and then again in an Error construction, often slightly different due to
30//!    the Error variant not supporting a format string. Instead `return
31//!    Err(Database(error!("problem with db: {msg}")))` logs the error at the
32//!    callsite and then returns the error with the same string. Caller has the
33//!    option of replacing `error!` with `debug_error!`.
34
35/// Constructs an error result through `err!`.
36///
37/// The supplied tokens select or format an [`Error`](crate::Error), then wrap
38/// it in [`std::result::Result::Err`]. Every input form accepted by `err!` is
39/// supported.
40#[macro_export]
41#[collapse_debuginfo(yes)]
42macro_rules! Err {
43	($($args:tt)*) => {
44		Err($crate::err!($($args)*))
45	};
46}
47
48/// Constructs a core error from structured or formatted input.
49///
50/// Variant forms preserve typed Matrix, HTTP, and configuration context.
51/// Forms containing a tracing level also emit the formatted fields before
52/// returning the error.
53#[macro_export]
54#[collapse_debuginfo(yes)]
55macro_rules! err {
56	(HttpJson($statuscode:ident, $($args:tt)+)) => {
57		$crate::error::Error::HttpJson(
58			$crate::http::StatusCode::$statuscode,
59			::axum::Json(::serde_json::json!($($args)+))
60		)
61	};
62
63	(Request(Forbidden($level:ident!($($args:tt)+)))) => {{
64		let mut buf = String::new();
65		$crate::error::Error::Request(
66			$crate::ruma::api::error::ErrorKind::forbidden(),
67			$crate::err_log!(buf, $level, $($args)+),
68			$crate::http::StatusCode::BAD_REQUEST
69		)
70	}};
71
72	(Request(Forbidden($($args:tt)+))) => {
73		$crate::error::Error::Request(
74			$crate::ruma::api::error::ErrorKind::forbidden(),
75			$crate::format_maybe!($($args)+),
76			$crate::http::StatusCode::BAD_REQUEST
77		)
78	};
79
80	(Request($variant:ident($level:ident!($($args:tt)+)))) => {{
81		let mut buf = String::new();
82		$crate::error::Error::Request(
83			$crate::ruma::api::error::ErrorKind::$variant,
84			$crate::err_log!(buf, $level, $($args)+),
85			$crate::http::StatusCode::BAD_REQUEST
86		)
87	}};
88
89	(Request($variant:ident($($args:tt)+))) => {
90		$crate::error::Error::Request(
91			$crate::ruma::api::error::ErrorKind::$variant,
92			$crate::format_maybe!($($args)+),
93			$crate::http::StatusCode::BAD_REQUEST
94		)
95	};
96
97	(RequestStatus($status:ident, $variant:ident($($args:tt)+))) => {
98		$crate::error::Error::RequestStatus(
99			$crate::ruma::api::error::ErrorKind::$variant,
100			$crate::format_maybe!($($args)+),
101			$crate::http::StatusCode::$status
102		)
103	};
104
105	(Config($item:literal, $($args:tt)+)) => {{
106		let mut buf = String::new();
107		$crate::error::Error::Config($item, $crate::err_log!(buf, error, config = %$item, $($args)+))
108	}};
109
110	($variant:ident($level:ident!($($args:tt)+))) => {{
111		let mut buf = String::new();
112		$crate::error::Error::$variant($crate::err_log!(buf, $level, $($args)+))
113	}};
114
115	($variant:ident($($args:ident),+)) => {
116		$crate::error::Error::$variant($($args),+)
117	};
118
119	($variant:ident($($args:tt)+)) => {
120		$crate::error::Error::$variant($crate::format_maybe!($($args)+))
121	};
122
123	($level:ident!($($args:tt)+)) => {{
124		let mut buf = String::new();
125		$crate::error::Error::Err($crate::err_log!(buf, $level, $($args)+))
126	}};
127
128	($($args:tt)+) => {
129		$crate::error::Error::Err($crate::format_maybe!($($args)+))
130	};
131}
132
133/// Renders an error message and sends its fields to tracing and the log bridge.
134///
135/// `visit` passes one `ValueSet` to both dispatch paths, then records its
136/// fields into the caller's output buffer. The surrounding `err!` expansion
137/// uses that buffer to construct the error.
138#[macro_export]
139#[collapse_debuginfo(yes)]
140macro_rules! err_log {
141	($out:ident, $level:ident, $($fields:tt)+) => {{
142		use $crate::tracing::{
143			callsite, callsite2, metadata, valueset, Callsite,
144			Level,
145		};
146
147		const LEVEL: Level = $crate::err_lev!($level);
148		static __CALLSITE: callsite::DefaultCallsite = callsite2! {
149			name: std::concat! {
150				"event ",
151				std::file!(),
152				":",
153				std::line!(),
154			},
155			kind: metadata::Kind::EVENT,
156			target: std::module_path!(),
157			level: LEVEL,
158			fields: $($fields)+,
159		};
160
161		($crate::error::visit)(
162			&mut $out,
163			LEVEL,
164			&__CALLSITE,
165			&mut valueset!(__CALLSITE.metadata().fields(), $($fields)+)
166		);
167
168		($out).into()
169	}}
170}
171
172/// Resolves an error macro level to a tracing level.
173///
174/// Debug-sensitive warning and error levels fall back to `DEBUG` outside debug
175/// logging mode. Fixed warning and error inputs retain their respective levels.
176#[macro_export]
177#[collapse_debuginfo(yes)]
178macro_rules! err_lev {
179	(debug_warn) => {
180		if $crate::debug::logging() {
181			$crate::tracing::Level::WARN
182		} else {
183			$crate::tracing::Level::DEBUG
184		}
185	};
186
187	(debug_error) => {
188		if $crate::debug::logging() {
189			$crate::tracing::Level::ERROR
190		} else {
191			$crate::tracing::Level::DEBUG
192		}
193	};
194
195	(warn) => {
196		$crate::tracing::Level::WARN
197	};
198
199	(error) => {
200		$crate::tracing::Level::ERROR
201	};
202}
203
204use std::{fmt, fmt::Write};
205
206use tracing::{
207	__macro_support, __tracing_log, Callsite, Event, Level,
208	callsite::DefaultCallsite,
209	field::{Field, ValueSet, Visit},
210	level_enabled,
211};
212
213struct Visitor<'a>(&'a mut String);
214
215impl Visit for Visitor<'_> {
216	#[inline]
217	fn record_debug(&mut self, field: &Field, val: &dyn fmt::Debug) {
218		match field.name() {
219			| "message" => write!(self.0, "{val:?}").expect("stream error"),
220			// already named in Error::Config Display; suppress the duplicate field here.
221			| "config" => {},
222			| name => write!(self.0, " {name}={val:?}").expect("stream error"),
223		}
224	}
225}
226
227/// Dispatches structured error fields and records their formatted message.
228///
229/// Enabled tracing subscribers receive an event at the supplied call site, and
230/// the tracing-log bridge receives the same values. The visitor then appends
231/// those values to `out` for construction of the returned error.
232pub fn visit(
233	out: &mut String,
234	level: Level,
235	__callsite: &'static DefaultCallsite,
236	vs: &mut ValueSet<'_>,
237) {
238	let meta = __callsite.metadata();
239	let enabled = level_enabled!(level) && {
240		let interest = __callsite.interest();
241		!interest.is_never() && __macro_support::__is_enabled(meta, interest)
242	};
243
244	if enabled {
245		Event::dispatch(meta, vs);
246	}
247
248	__tracing_log!(level, __callsite, vs);
249	vs.record(&mut Visitor(out));
250}