macros/
lib.rs

1extern crate proc_macro;
2
3use make_path_parts_proc::PathPartsInput;
4use proc_macro::TokenStream;
5use syn::{parse_macro_input, DeriveInput, ExprArray, Lit, MetaList, NestedMeta, Result};
6
7mod make_path_parts_proc;
8mod path_part_derive;
9
10use quote::{format_ident, quote};
11
12#[proc_macro]
13pub fn make_path_parts(input: TokenStream) -> TokenStream {
14    let input = proc_macro2::TokenStream::from(input);
15
16    let parts = PathPartsInput::try_from(input).unwrap();
17
18    // let output: proc_macro2::TokenStream = parts.into();
19    let output: proc_macro2::TokenStream = parts.try_into().unwrap();
20
21    output.into()
22}
23
24/// Derive macro that to implements PathPart for IDs.
25/// Only works on tuple structs with a single value value that implements
26/// ToString like Uuid or u32.
27/// If you wanna implement PathPart on types that don'w follow this exact
28/// structure do it manually
29#[proc_macro_derive(PathPart, attributes(my_trait))]
30pub fn path_part(input: TokenStream) -> TokenStream {
31    let ast = parse_macro_input!(input as DeriveInput);
32
33    path_part_derive::impl_path_part(&ast)
34}
35
36fn parse_list(list: &MetaList) -> Result<ExprArray> {
37    let mut items = vec![];
38    for nested in &list.nested {
39        match nested {
40            syn::NestedMeta::Lit(syn::Lit::Str(value)) => {
41                items.push(value.value());
42            }
43            other => {
44                return Err(syn::Error::new_spanned(other, "expected string literal"));
45            }
46        }
47    }
48
49    let mut concatenated = items
50        .into_iter()
51        .reduce(|acc, value| acc + "," + &value)
52        .unwrap_or_default();
53    concatenated = format!("[{concatenated}]");
54
55    let concatenated = match concatenated.parse::<TokenStream>() {
56        Err(error) => {
57            return Err(syn::Error::new_spanned(list, error));
58        }
59        Ok(concatenated) => concatenated,
60    };
61
62    Ok(syn::parse::<syn::ExprArray>(concatenated)?)
63}
64
65type SetupFn = syn::Ident;
66type Fixtures = ExprArray;
67type Services = ExprArray;
68
69/// Argument macro which wraps the SQLx `sqlx::test` macro to bootstrap and setup our backend services
70/// for testing, and allows for safe shutdown of the Actix server.
71///
72/// Note: Only uses the `PgPoolOptions, PgConnectOptions` arguments.
73///
74/// Note: Any drop implementation of the server/application should not include handling the shutdown
75/// of the server. This is handled by the macro so that if there is a panic, it can still be shutdown
76/// correctly, and the panic can continue.
77///
78/// Example setup function:
79///
80/// ```no_run
81/// async fn setup_service(
82/// 	fixtures: &[Fixture],
83/// 	services: &[Service],
84/// 	pool_opts: PgPoolOptions,
85/// 	conn_opts: PgConnectOptions
86/// ) -> (ServerHandle, u16) {
87///     let app = initialize_server(fixtures, services, pool_opts, conn_opts).await;
88///
89///     let handle = app.handle();
90///     let port = app.port();
91///
92///     let _join_handle = tokio::spawn(app.run_until_stopped());
93///
94///     (handle, port)
95/// }
96/// ```
97///
98/// The macro takes three arguments:
99///
100/// - `setup` **Required**: The setup function, for example `setup = "my_service_setup"`;
101/// - `fixtures` **Optional**: A list of `T` - Passed as the `fixtures` argument to the setup fn;
102/// - `services` **Optional**: A list of `T` - Passed as the `services` argument to the setup fn.
103///
104/// ```no_run
105/// #[test_service(
106/// 	setup = "setup_service",
107/// 	fixtures("Fixture::User", "Fixture::Jig"),
108/// 	services("Service::S3", "Service::Email"),
109/// )
110/// ```
111///
112/// Example test case:
113///
114/// ```no_run
115/// use macros::test_service;
116/// use crate::{fixture::Fixture, service::Service};
117///
118/// //...
119///
120/// async fn setup_service(
121///     fixtures: &[Fixture],
122///     services: &[Service],
123///     pool_opts: PgPoolOptions,
124///     conn_opts: PgConnectOptions
125/// ) -> (ServerHandle, u16) {
126///     // ... do setup
127///
128///     (handle, port)
129/// }
130///
131/// #[test_service(setup = "setup_service", fixtures("Fixture::User"))]
132/// async fn create_default(
133///     port: u16, // <-- NB
134/// ) -> anyhow::Result<()> {
135///     let settings = insta::Settings::clone_current();
136///
137///     let client = reqwest::Client::new();
138///
139///     let resp = client
140///         .post(&format!("http://0.0.0.0:{}/v1/resource", port))
141///         .login()
142///         .send()
143///         .await?
144///         .error_for_status()?;
145///
146///     assert_eq!(resp.status(), StatusCode::CREATED);
147///
148///     let body: CreateResponse<ResourceId> = resp.json().await?;
149///
150///     settings
151///         .bind_async(async {
152///             assert_json_snapshot!(body, {".id" => "[id]"});
153///         })
154///         .await;
155///
156///     let resource_id = body.id.0;
157///
158///     let resp = client
159///         .get(&format!(
160///             "http://0.0.0.0:{}/v1/resource/{}/draft",
161///             port, resource_id
162///         ))
163///         .login()
164///         .send()
165///         .await?
166///         .error_for_status()?;
167///
168///     let body: serde_json::Value = resp.json().await?;
169///
170///     insta::assert_json_snapshot!(
171///         body, {
172///             ".**.id" => "[id]",
173///             ".**.createdAt" => "[created_at]",
174///             ".**.lastEdited" => "[last_edited]"});
175///
176///     Ok(())
177/// }
178/// ```
179#[proc_macro_attribute]
180pub fn test_service(args: TokenStream, input: TokenStream) -> TokenStream {
181    let args = syn::parse_macro_input!(args as syn::AttributeArgs);
182
183    let input = syn::parse_macro_input!(input as syn::ItemFn);
184
185    let ret = &input.sig.output;
186    let name = &input.sig.ident;
187    let body = &input.block;
188
189    match parse_args(args) {
190        Ok((setup_fn, fixtures, services)) => {
191            quote! {
192                #[sqlx::test]
193                pub async fn #name(
194                    pool_opts: PgPoolOptions,
195                    conn_opts: PgConnectOptions,
196                ) #ret {
197                    async fn wrapped(port: u16) #ret {
198                        #body
199                    }
200
201                    let (server_handle, port) = #setup_fn(&#fixtures, &#services, pool_opts, conn_opts).await;
202
203                    use ::futures::FutureExt;
204                    use ::std::panic::AssertUnwindSafe;
205                    let result = AssertUnwindSafe(wrapped(port)).catch_unwind().await;
206
207                    server_handle.stop(true).await;
208
209                    match result {
210                        Ok(result) => result,
211                        Err(error) => ::std::panic::resume_unwind(error),
212                    }
213                }
214            }
215            .into()
216        }
217        Err(error) => {
218            let error = error.to_compile_error();
219            return quote!(#error).into();
220        }
221    }
222}
223
224fn parse_args(args: Vec<NestedMeta>) -> Result<(SetupFn, Fixtures, Services)> {
225    let mut setup_fn = None;
226    let mut fixtures = None;
227    let mut services = None;
228
229    for arg in args.iter() {
230        match arg {
231            NestedMeta::Meta(syn::Meta::List(list)) => {
232                if list.path.is_ident("fixtures") {
233                    fixtures = Some(parse_list(list)?);
234                } else if list.path.is_ident("services") {
235                    services = Some(parse_list(list)?);
236                }
237            }
238            NestedMeta::Meta(syn::Meta::NameValue(namevalue)) => {
239                if namevalue.path.is_ident("setup") {
240                    if let Lit::Str(value) = &namevalue.lit {
241                        setup_fn = Some(format_ident!("{}", value.value()));
242                    } else {
243                        return Err(syn::Error::new_spanned(
244                            namevalue,
245                            "expected string literal",
246                        ));
247                    }
248                }
249            }
250            other => {
251                return Err(syn::Error::new_spanned(other, "unexpected argument"));
252            }
253        }
254    }
255
256    let setup_fn = match setup_fn {
257        None => {
258            let args = args.iter();
259            return Err(syn::Error::new_spanned(quote!({#(#args)*}), "foo"));
260        }
261        Some(setup_fn) => setup_fn,
262    };
263
264    let default_expr_array =
265        || syn::parse::<syn::ExprArray>("[]".parse::<TokenStream>().unwrap()).unwrap();
266
267    Ok((
268        setup_fn,
269        fixtures.unwrap_or_else(default_expr_array),
270        services.unwrap_or_else(default_expr_array),
271    ))
272}