arminius_lib_macros/lib.rs
1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Crate for procedural macros used in arminius
6
7#![allow(
8 clippy::expect_used,
9 reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
10)]
11
12mod matrix_api_error;
13mod test_macros;
14use matrix_api_error::{
15 MatrixApiErrorArgs, matrix_api_error_derive_for_enum, matrix_api_error_derive_for_struct,
16};
17use proc_macro::TokenStream;
18use syn::{DeriveInput, parse_macro_input};
19
20/// Marks the following async function as an Arminius test.
21///
22/// Currently, this sets up the function with an SQL database, runs migrations,
23/// starts NATS, and provides a ComponentState object and NatsConfiguration.
24/// Depends on sqlx being available.
25#[proc_macro_attribute]
26pub fn test(_: TokenStream, input: TokenStream) -> TokenStream {
27 test_macros::test(input)
28}
29
30/// A test macro for e2e tests, which sets up logging, and ensures that if the
31/// roomserver panics, that the rest also panics
32///
33/// Exports the variable TEST_NAME, which is a String consisting of the module
34/// path, and the name of the test function
35///
36/// Tests whose body mentions a blueprint starting with `TwoHs` get `_2hs`
37/// appended to their function name, so that test filters such as nextest's
38/// `test(/_2hs$/)` can filter for tests that start two homeservers.
39/// `TEST_NAME` keeps the name without the suffix.
40///
41/// ## Panics
42///
43/// This macro panics if it is applied to anything other than a function.
44#[proc_macro_attribute]
45pub fn e2etest(_: TokenStream, input: TokenStream) -> TokenStream {
46 test_macros::e2etest(input)
47}
48
49/// Implements the traits `arminius_lib_component::error::MatrixApiError` and
50/// [`axum::response::IntoResponse`] for the struct or enum.
51/// Errors can be defined on structs or on enums
52///
53/// ## Structs
54/// To implement simple individual errors, we implement them as structs. This
55/// requires that the type on which we're deriving `MatrixApiError` implements
56/// [`ToString`], which can conveniently be done through the `thiserror` crate.
57/// To implement it, you give the attribute `#[matrix_api_error]` with two
58/// arguments, `status_code` which is a [`axum::http::StatusCode`] and
59/// `error_kind` which is an [`ruma::api::error::ErrorKind`] value.
60///
61/// ### Example
62/// To implement an `InvalidUserId` error, which can also be printed as a
63/// matrix error, we use the proc macro to derive
64/// `arminius_lib_component::error::MatrixApiError`:
65/// ```
66/// # use axum::http::StatusCode;
67/// # use arminius_lib_component::error::MatrixApiError;
68/// # use ruma::{UserId, api::error::ErrorKind};
69/// #[derive(thiserror::Error, MatrixApiError, Debug)]
70/// #[error("Invalid user id: {0}")]
71/// #[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::BadJson)]
72/// struct InvalidUserId(String);
73/// ```
74///
75/// This will cause `InvalidUserId` to implement `MatrixApiError` in such a way
76/// that `InvalidUserId("jens".to_owned)::create_matrix_error()` will result in:
77/// ```json
78/// {
79/// "errcode": "M_BAD_JSON",
80/// "error": "Invalid user id: jens"
81/// }
82/// ```
83/// And converting it into a response with
84/// [`axum::response::IntoResponse::into_response`] will give a return code 400,
85/// with the above body
86///
87///
88/// ## Enums
89/// For cases where we want to describe several possible errors we use an enum
90/// which can collect several other errors, but also define their own errors
91/// where applicable. Each variant of the enum must either have exactly one
92/// field which implements `MatrixApiError`, usually with the help of this
93/// macro, or have the attribute `#[matrix_api_error]` to define the matrix api
94/// error inline.
95///
96/// ### Example
97/// ```
98/// # use axum::http::StatusCode;
99/// # use ruma::{UserId, api::error::ErrorKind};
100/// # use arminius_lib_component::error::MatrixApiError;
101/// #[derive(thiserror::Error, MatrixApiError, Debug)]
102/// #[error("Invalid user id: {0}")]
103/// #[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::BadJson)]
104/// struct InvalidUserId(String);
105///
106/// #[derive(thiserror::Error, Debug, MatrixApiError)]
107/// enum LoginError {
108/// #[error(transparent)]
109/// InvalidUsername(#[from] InvalidUserId),
110///
111/// #[error("Incorrect password for user. The actual password is {0}")]
112/// #[matrix_api_error(status_code = StatusCode::UNAUTHORIZED, error_kind =
113/// ErrorKind::Forbidden)]
114/// IncorrectPassword(String),
115/// }
116/// ```
117/// This will implement `MatrixApiError` and `IntoResponse` in such a way that
118/// `LoginError::InvalidUsername` will use the implementation of `MatrixApiError`
119/// implemented by the `InvalidUserId` type, whereas `IncorrectPassword` will
120/// create a matrix api error with the status code `UNAUTHORIZED` and error kind
121/// `FORBIDDEN`.
122///
123/// Note that this derive macro is re-exported from `arminius_lib_component`, so
124/// that it is sufficient to just depend on `arminius_lib_component` instead
125/// of depending on `arminius-lib-macros` directly. This is also the reason for the
126/// funny name with the two dashes.
127#[proc_macro_derive(__MatrixApiError, attributes(matrix_api_error))]
128pub fn matrix_api_error_derive(item: TokenStream) -> TokenStream {
129 let derive_input = parse_macro_input!(item as DeriveInput);
130 let args = MatrixApiErrorArgs::parse(&derive_input.attrs);
131
132 if let Some(proxying) = &args.proxying {
133 matrix_api_error_derive_for_struct(
134 derive_input,
135 &proxying.status_code,
136 &proxying.error_kind,
137 &args.component_crate_root,
138 )
139 } else {
140 matrix_api_error_derive_for_enum(derive_input, &args.component_crate_root)
141 }
142}
143
144/// Implements `From<arminius_lib_db::DatabaseError>` for a given error type,
145/// which already implements `From<InternalServerError>`
146///
147/// All database errors will print a trace
148#[proc_macro_derive(FromDbError)]
149pub fn impl_from_db_error_derive(item: TokenStream) -> TokenStream {
150 let derive_input = parse_macro_input!(item as DeriveInput);
151 let DeriveInput { ident, generics, .. } = derive_input;
152 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
153
154 quote::quote! {
155 impl #impl_generics From<arminius_lib_db::DatabaseError> for #ident #ty_generics #where_clause {
156 #[track_caller]
157 fn from(error: arminius_lib_db::DatabaseError) -> Self {
158 let location = std::panic::Location::caller();
159 tracing::error!(?error, ?location, "Error communicating with database");
160 Self::from(InternalServerError::untraced_internal_server_error())
161 }
162 }
163 }
164 .into()
165}
166
167/// Implements `From<arminius_lib_nats::NatsError>` for a given error type,
168/// which already implements `From<InternalServerError>`
169///
170/// All database errors will print a trace
171#[proc_macro_derive(FromNatsError)]
172pub fn impl_from_nats_error_derive(item: TokenStream) -> TokenStream {
173 let derive_input = parse_macro_input!(item as DeriveInput);
174 let DeriveInput { ident, generics, .. } = derive_input;
175 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
176
177 quote::quote! {
178 impl #impl_generics From<arminius_lib_nats::NatsError> for #ident #ty_generics #where_clause {
179 #[track_caller]
180 fn from(error: arminius_lib_nats::NatsError) -> Self {
181 let location = std::panic::Location::caller();
182 tracing::error!(?error, ?location, "Error performing NATS call");
183 Self::from(InternalServerError::untraced_internal_server_error())
184 }
185 }
186 }
187 .into()
188}