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}