blueprint_macros/
lib.rs

1//! Macros for [`blueprint_sdk`].
2//!
3//! [`blueprint_sdk`]: https://crates.io/crates/blueprint_sdk
4#![doc(
5    html_logo_url = "https://cdn.prod.website-files.com/6494562b44a28080aafcbad4/65aaf8b0818b1d504cbdf81b_Tnt%20Logo.png"
6)]
7#![cfg_attr(test, allow(clippy::float_cmp))]
8#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))]
9
10use debug_job::FunctionKind;
11use proc_macro::TokenStream;
12use quote::{ToTokens, quote};
13use syn::{Type, parse::Parse};
14
15mod attr_parsing;
16mod debug_job;
17#[cfg(feature = "evm")]
18mod evm;
19mod from_ref;
20mod with_position;
21
22#[cfg(feature = "evm")]
23/// A procedural macro that outputs the [JSON ABI] for the given file path.
24///
25/// [JSON ABI]: https://docs.ethers.org/v5/api/utils/abi/formats/#abi-formats--solidity
26#[proc_macro]
27pub fn load_abi(input: TokenStream) -> TokenStream {
28    evm::load_abi(input)
29}
30
31/// Generates better error messages when applied to job functions.
32///
33/// While using [`blueprint_sdk`], you can get long error messages for simple mistakes. For example:
34///
35/// ```compile_fail
36/// use blueprint_sdk::Router;
37///
38/// #[tokio::main]
39/// async fn main() {
40///   Router::<()>::new().route(0, job);
41/// }
42///
43/// fn job() -> &'static str {
44///     "Hello, world"
45/// }
46/// ```
47///
48/// You will get a long error message about function not implementing [`Job`] trait. But why
49/// does this function not implement it? To figure it out, the [`debug_job`] macro can be used.
50///
51/// ```compile_fail
52/// # use blueprint_sdk::Router;
53/// # use blueprint_sdk::macros::debug_job;
54/// #
55/// # #[tokio::main]
56/// # async fn main() {
57/// #   Router::new().route(0, job);
58/// # }
59/// #
60/// #[debug_job]
61/// fn job() -> &'static str {
62///     "Hello, world"
63/// }
64/// ```
65///
66/// ```text
67/// error: jobs must be async functions
68///   --> main.rs:xx:1
69///    |
70/// xx | fn job() -> &'static str {
71///    | ^^
72/// ```
73///
74/// As the error message says, job function needs to be async.
75///
76/// ```no_run
77/// use blueprint_sdk::{Router, macros::debug_job};
78///
79/// #[tokio::main]
80/// async fn main() {
81///     Router::<()>::new().route(0, job);
82/// }
83///
84/// #[debug_job]
85/// async fn job() -> &'static str {
86///     "Hello, world"
87/// }
88/// ```
89///
90/// # Changing context type
91///
92/// By default `#[debug_job]` assumes your context type is `()` unless your job has a
93/// [`blueprint_sdk::Context`] argument:
94///
95/// ```
96/// use blueprint_sdk::extract::Context;
97/// use blueprint_sdk::macros::debug_job;
98///
99/// #[debug_job]
100/// async fn job(
101///     // this makes `#[debug_job]` use `AppContext`
102///     Context(context): Context<AppContext>,
103/// ) {
104/// }
105///
106/// #[derive(Clone)]
107/// struct AppContext {}
108/// ```
109///
110/// If your job takes multiple [`blueprint_sdk::Context`] arguments or you need to otherwise
111/// customize the context type you can set it with `#[debug_job(context = ...)]`:
112///
113/// ```
114/// use blueprint_sdk::extract::Context;
115/// use blueprint_sdk::{extract::FromRef, macros::debug_job};
116///
117/// #[debug_job(context = AppContext)]
118/// async fn job(Context(app_ctx): Context<AppContext>, Context(inner_ctx): Context<InnerContext>) {
119/// }
120///
121/// #[derive(Clone)]
122/// struct AppContext {
123///     inner: InnerContext,
124/// }
125///
126/// #[derive(Clone)]
127/// struct InnerContext {}
128///
129/// impl FromRef<AppContext> for InnerContext {
130///     fn from_ref(context: &AppContext) -> Self {
131///         context.inner.clone()
132///     }
133/// }
134/// ```
135///
136/// # Limitations
137///
138/// This macro does not work for functions in an `impl` block that don't have a `self` parameter:
139///
140/// ```compile_fail
141/// use blueprint_sdk::macros::debug_job;
142/// use blueprint_tangle_extra::extract::TangleArg;
143///
144/// struct App {}
145///
146/// impl App {
147///     #[debug_job]
148///     async fn my_job(TangleArg(_): TangleArg<u64>) {}
149/// }
150/// ```
151///
152/// This will yield an error similar to this:
153///
154/// ```text
155/// error[E0425]: cannot find function `__blueprint_macros_check_job_0_from_job_call_check` in this scope
156//    --> src/main.rs:xx:xx
157//     |
158//  xx |     pub async fn my_job(TangleArg(_): TangleArg<u64>)  {}
159//     |                                   ^^^^ not found in this scope
160/// ```
161/// 
162/// # Performance
163///
164/// This macro has no effect when compiled with the release profile. (eg. `cargo build --release`)
165///
166/// [`blueprint_sdk`]: https://docs.rs/blueprint_sdk/0.1
167/// [`Job`]: https://docs.rs/blueprint_sdk/0.1/blueprint_sdk/job/trait.Job.html
168/// [`blueprint_sdk::Context`]: https://docs.rs/blueprint_sdk/0.1/blueprint_sdk/context/struct.Context.html
169/// [`debug_job`]: macro@debug_job
170#[proc_macro_attribute]
171pub fn debug_job(_attr: TokenStream, input: TokenStream) -> TokenStream {
172    #[cfg(not(debug_assertions))]
173    return input;
174
175    #[cfg(debug_assertions)]
176    #[allow(clippy::used_underscore_binding)]
177    return expand_attr_with(_attr, input, |attrs, item_fn| {
178        debug_job::expand(attrs, &item_fn, FunctionKind::Job)
179    });
180}
181
182/// Derive an implementation of [`FromRef`] for each field in a struct.
183///
184/// # Example
185///
186/// ```
187/// use blueprint_sdk::{
188///     Router,
189///     extract::{Context, FromRef},
190/// };
191///
192/// #
193/// # type Keystore = String;
194/// # type DatabasePool = ();
195/// #
196/// // This will implement `FromRef` for each field in the struct.
197/// #[derive(FromRef, Clone)]
198/// struct AppContext {
199///     keystore: Keystore,
200///     database_pool: DatabasePool,
201///     // fields can also be skipped
202///     #[from_ref(skip)]
203///     openai_api_token: String,
204/// }
205///
206/// // So those types can be extracted via `Context`
207/// async fn my_job(Context(keystore): Context<Keystore>) {}
208///
209/// async fn other_job(Context(database_pool): Context<DatabasePool>) {}
210///
211/// # let keystore = Default::default();
212/// # let database_pool = Default::default();
213/// let ctx = AppContext {
214///     keystore,
215///     database_pool,
216///     openai_api_token: "sk-secret".to_owned(),
217/// };
218///
219/// let router = Router::new()
220///     .route(0, my_job)
221///     .route(1, other_job)
222///     .with_context(ctx);
223/// # let _: Router = router;
224/// ```
225///
226/// [`FromRef`]: https://docs.rs/blueprint-sdk/0.1/blueprint_sdk/extract/trait.FromRef.html
227#[proc_macro_derive(FromRef, attributes(from_ref))]
228pub fn derive_from_ref(item: TokenStream) -> TokenStream {
229    expand_with(item, from_ref::expand)
230}
231
232fn expand_with<F, I, K>(input: TokenStream, f: F) -> TokenStream
233where
234    F: FnOnce(I) -> syn::Result<K>,
235    I: Parse,
236    K: ToTokens,
237{
238    expand(syn::parse(input).and_then(f))
239}
240
241fn expand_attr_with<F, A, I, K>(attr: TokenStream, input: TokenStream, f: F) -> TokenStream
242where
243    F: FnOnce(A, I) -> K,
244    A: Parse,
245    I: Parse,
246    K: ToTokens,
247{
248    let expand_result = (|| {
249        let attr = syn::parse(attr)?;
250        let input = syn::parse(input)?;
251        Ok(f(attr, input))
252    })();
253    expand(expand_result)
254}
255
256fn expand<T>(result: syn::Result<T>) -> TokenStream
257where
258    T: ToTokens,
259{
260    match result {
261        Ok(tokens) => {
262            let tokens = (quote! { #tokens }).into();
263            if std::env::var_os("BLUEPRINT_MACROS_DEBUG").is_some() {
264                eprintln!("{tokens}");
265            }
266            tokens
267        }
268        Err(err) => err.into_compile_error().into(),
269    }
270}
271
272fn infer_context_types<'a, I>(types: I) -> impl Iterator<Item = Type> + 'a
273where
274    I: Iterator<Item = &'a Type> + 'a,
275{
276    types
277        .filter_map(|ty| {
278            if let Type::Path(path) = ty {
279                Some(&path.path)
280            } else {
281                None
282            }
283        })
284        .filter_map(|path| {
285            if let Some(last_segment) = path.segments.last() {
286                if last_segment.ident != "Context" {
287                    return None;
288                }
289
290                match &last_segment.arguments {
291                    syn::PathArguments::AngleBracketed(args) if args.args.len() == 1 => {
292                        Some(args.args.first().unwrap())
293                    }
294                    _ => None,
295                }
296            } else {
297                None
298            }
299        })
300        .filter_map(|generic_arg| {
301            if let syn::GenericArgument::Type(ty) = generic_arg {
302                Some(ty)
303            } else {
304                None
305            }
306        })
307        .cloned()
308}
309
310#[cfg(test)]
311fn run_ui_tests(directory: &str) {
312    #[rustversion::nightly]
313    fn go(directory: &str) {
314        let t = trybuild::TestCases::new();
315
316        if let Ok(mut path) = std::env::var("BLUEPRINT_TEST_ONLY") {
317            if let Some(path_without_prefix) = path.strip_prefix("macros/") {
318                path = path_without_prefix.to_owned();
319            }
320
321            if !path.contains(&format!("/{directory}/")) {
322                return;
323            }
324
325            if path.contains("/fail/") {
326                t.compile_fail(path);
327            } else if path.contains("/pass/") {
328                t.pass(path);
329            } else {
330                panic!()
331            }
332        } else {
333            t.compile_fail(format!("tests/{directory}/fail/*.rs"));
334            t.pass(format!("tests/{directory}/pass/*.rs"));
335        }
336    }
337
338    #[rustversion::not(nightly)]
339    fn go(_directory: &str) {}
340
341    go(directory);
342}