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#[macro_export]
36#[collapse_debuginfo(yes)]
37macro_rules! Err {
38	($($args:tt)*) => {
39		Err($crate::err!($($args)*))
40	};
41}
42
43#[macro_export]
44#[collapse_debuginfo(yes)]
45macro_rules! err {
46	(HttpJson($statuscode:ident, $($args:tt)+)) => {
47		$crate::error::Error::HttpJson(
48			$crate::http::StatusCode::$statuscode,
49			::axum::Json(::serde_json::json!($($args)+))
50		)
51	};
52
53	(Request(Forbidden($level:ident!($($args:tt)+)))) => {{
54		let mut buf = String::new();
55		$crate::error::Error::Request(
56			$crate::ruma::api::error::ErrorKind::forbidden(),
57			$crate::err_log!(buf, $level, $($args)+),
58			$crate::http::StatusCode::BAD_REQUEST
59		)
60	}};
61
62	(Request(Forbidden($($args:tt)+))) => {
63		$crate::error::Error::Request(
64			$crate::ruma::api::error::ErrorKind::forbidden(),
65			$crate::format_maybe!($($args)+),
66			$crate::http::StatusCode::BAD_REQUEST
67		)
68	};
69
70	(Request($variant:ident($level:ident!($($args:tt)+)))) => {{
71		let mut buf = String::new();
72		$crate::error::Error::Request(
73			$crate::ruma::api::error::ErrorKind::$variant,
74			$crate::err_log!(buf, $level, $($args)+),
75			$crate::http::StatusCode::BAD_REQUEST
76		)
77	}};
78
79	(Request($variant:ident($($args:tt)+))) => {
80		$crate::error::Error::Request(
81			$crate::ruma::api::error::ErrorKind::$variant,
82			$crate::format_maybe!($($args)+),
83			$crate::http::StatusCode::BAD_REQUEST
84		)
85	};
86
87	(Config($item:literal, $($args:tt)+)) => {{
88		let mut buf = String::new();
89		$crate::error::Error::Config($item, $crate::err_log!(buf, error, config = %$item, $($args)+))
90	}};
91
92	($variant:ident($level:ident!($($args:tt)+))) => {{
93		let mut buf = String::new();
94		$crate::error::Error::$variant($crate::err_log!(buf, $level, $($args)+))
95	}};
96
97	($variant:ident($($args:ident),+)) => {
98		$crate::error::Error::$variant($($args),+)
99	};
100
101	($variant:ident($($args:tt)+)) => {
102		$crate::error::Error::$variant($crate::format_maybe!($($args)+))
103	};
104
105	($level:ident!($($args:tt)+)) => {{
106		let mut buf = String::new();
107		$crate::error::Error::Err($crate::err_log!(buf, $level, $($args)+))
108	}};
109
110	($($args:tt)+) => {
111		$crate::error::Error::Err($crate::format_maybe!($($args)+))
112	};
113}
114
115/// A trinity of integration between tracing, logging, and Error. This is a
116/// customization of tracing::event! with the primary purpose of sharing the
117/// error string, fieldset parsing and formatting. An added benefit is that we
118/// can share the same callsite metadata for the source of our Error and the
119/// associated logging and tracing event dispatches.
120#[macro_export]
121#[collapse_debuginfo(yes)]
122macro_rules! err_log {
123	($out:ident, $level:ident, $($fields:tt)+) => {{
124		use $crate::tracing::{
125			callsite, callsite2, metadata, valueset, Callsite,
126			Level,
127		};
128
129		const LEVEL: Level = $crate::err_lev!($level);
130		static __CALLSITE: callsite::DefaultCallsite = callsite2! {
131			name: std::concat! {
132				"event ",
133				std::file!(),
134				":",
135				std::line!(),
136			},
137			kind: metadata::Kind::EVENT,
138			target: std::module_path!(),
139			level: LEVEL,
140			fields: $($fields)+,
141		};
142
143		($crate::error::visit)(
144			&mut $out,
145			LEVEL,
146			&__CALLSITE,
147			&mut valueset!(__CALLSITE.metadata().fields(), $($fields)+)
148		);
149
150		($out).into()
151	}}
152}
153
154#[macro_export]
155#[collapse_debuginfo(yes)]
156macro_rules! err_lev {
157	(debug_warn) => {
158		if $crate::debug::logging() {
159			$crate::tracing::Level::WARN
160		} else {
161			$crate::tracing::Level::DEBUG
162		}
163	};
164
165	(debug_error) => {
166		if $crate::debug::logging() {
167			$crate::tracing::Level::ERROR
168		} else {
169			$crate::tracing::Level::DEBUG
170		}
171	};
172
173	(warn) => {
174		$crate::tracing::Level::WARN
175	};
176
177	(error) => {
178		$crate::tracing::Level::ERROR
179	};
180}
181
182use std::{fmt, fmt::Write};
183
184use tracing::{
185	__macro_support, __tracing_log, Callsite, Event, Level,
186	callsite::DefaultCallsite,
187	field::{Field, ValueSet, Visit},
188	level_enabled,
189};
190
191struct Visitor<'a>(&'a mut String);
192
193impl Visit for Visitor<'_> {
194	#[inline]
195	fn record_debug(&mut self, field: &Field, val: &dyn fmt::Debug) {
196		if field.name() == "message" {
197			write!(self.0, "{val:?}").expect("stream error");
198		} else {
199			write!(self.0, " {}={val:?}", field.name()).expect("stream error");
200		}
201	}
202}
203
204pub fn visit(
205	out: &mut String,
206	level: Level,
207	__callsite: &'static DefaultCallsite,
208	vs: &mut ValueSet<'_>,
209) {
210	let meta = __callsite.metadata();
211	let enabled = level_enabled!(level) && {
212		let interest = __callsite.interest();
213		!interest.is_never() && __macro_support::__is_enabled(meta, interest)
214	};
215
216	if enabled {
217		Event::dispatch(meta, vs);
218	}
219
220	__tracing_log!(level, __callsite, vs);
221	vs.record(&mut Visitor(out));
222}