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}