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	(Config($item:literal, $($args:tt)+)) => {{
98		let mut buf = String::new();
99		$crate::error::Error::Config($item, $crate::err_log!(buf, error, config = %$item, $($args)+))
100	}};
101
102	($variant:ident($level:ident!($($args:tt)+))) => {{
103		let mut buf = String::new();
104		$crate::error::Error::$variant($crate::err_log!(buf, $level, $($args)+))
105	}};
106
107	($variant:ident($($args:ident),+)) => {
108		$crate::error::Error::$variant($($args),+)
109	};
110
111	($variant:ident($($args:tt)+)) => {
112		$crate::error::Error::$variant($crate::format_maybe!($($args)+))
113	};
114
115	($level:ident!($($args:tt)+)) => {{
116		let mut buf = String::new();
117		$crate::error::Error::Err($crate::err_log!(buf, $level, $($args)+))
118	}};
119
120	($($args:tt)+) => {
121		$crate::error::Error::Err($crate::format_maybe!($($args)+))
122	};
123}
124
125/// Renders an error message and sends its fields to tracing and the log bridge.
126///
127/// `visit` passes one `ValueSet` to both dispatch paths, then records its
128/// fields into the caller's output buffer. The surrounding `err!` expansion
129/// uses that buffer to construct the error.
130#[macro_export]
131#[collapse_debuginfo(yes)]
132macro_rules! err_log {
133	($out:ident, $level:ident, $($fields:tt)+) => {{
134		use $crate::tracing::{
135			callsite, callsite2, metadata, valueset, Callsite,
136			Level,
137		};
138
139		const LEVEL: Level = $crate::err_lev!($level);
140		static __CALLSITE: callsite::DefaultCallsite = callsite2! {
141			name: std::concat! {
142				"event ",
143				std::file!(),
144				":",
145				std::line!(),
146			},
147			kind: metadata::Kind::EVENT,
148			target: std::module_path!(),
149			level: LEVEL,
150			fields: $($fields)+,
151		};
152
153		($crate::error::visit)(
154			&mut $out,
155			LEVEL,
156			&__CALLSITE,
157			&mut valueset!(__CALLSITE.metadata().fields(), $($fields)+)
158		);
159
160		($out).into()
161	}}
162}
163
164/// Resolves an error macro level to a tracing level.
165///
166/// Debug-sensitive warning and error levels fall back to `DEBUG` outside debug
167/// logging mode. Fixed warning and error inputs retain their respective levels.
168#[macro_export]
169#[collapse_debuginfo(yes)]
170macro_rules! err_lev {
171	(debug_warn) => {
172		if $crate::debug::logging() {
173			$crate::tracing::Level::WARN
174		} else {
175			$crate::tracing::Level::DEBUG
176		}
177	};
178
179	(debug_error) => {
180		if $crate::debug::logging() {
181			$crate::tracing::Level::ERROR
182		} else {
183			$crate::tracing::Level::DEBUG
184		}
185	};
186
187	(warn) => {
188		$crate::tracing::Level::WARN
189	};
190
191	(error) => {
192		$crate::tracing::Level::ERROR
193	};
194}
195
196use std::{fmt, fmt::Write};
197
198use tracing::{
199	__macro_support, __tracing_log, Callsite, Event, Level,
200	callsite::DefaultCallsite,
201	field::{Field, ValueSet, Visit},
202	level_enabled,
203};
204
205struct Visitor<'a>(&'a mut String);
206
207impl Visit for Visitor<'_> {
208	#[inline]
209	fn record_debug(&mut self, field: &Field, val: &dyn fmt::Debug) {
210		match field.name() {
211			| "message" => write!(self.0, "{val:?}").expect("stream error"),
212			// already named in Error::Config Display; suppress the duplicate field here.
213			| "config" => {},
214			| name => write!(self.0, " {name}={val:?}").expect("stream error"),
215		}
216	}
217}
218
219/// Dispatches structured error fields and records their formatted message.
220///
221/// Enabled tracing subscribers receive an event at the supplied call site, and
222/// the tracing-log bridge receives the same values. The visitor then appends
223/// those values to `out` for construction of the returned error.
224pub fn visit(
225	out: &mut String,
226	level: Level,
227	__callsite: &'static DefaultCallsite,
228	vs: &mut ValueSet<'_>,
229) {
230	let meta = __callsite.metadata();
231	let enabled = level_enabled!(level) && {
232		let interest = __callsite.interest();
233		!interest.is_never() && __macro_support::__is_enabled(meta, interest)
234	};
235
236	if enabled {
237		Event::dispatch(meta, vs);
238	}
239
240	__tracing_log!(level, __callsite, vs);
241	vs.record(&mut Visitor(out));
242}