futility_try_catch/lib.rs
1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4 parse::{Parse, ParseStream, Result},
5 parse_macro_input, Block, Ident, Token, Type,
6};
7
8#[proc_macro]
9/// `try_` is a macro to use `try/catch` blocks in Rust until they're
10/// actually implemented in the language
11///
12/// Sometimes you want to scope errors to a given block and handle any error in
13/// there in one specific way. This is particularly useful if you want to
14/// handle failure cases that are recoverable within the function such as
15/// retrying an HTTP request. The macro can be used in two different ways:
16///
17/// ```
18/// # use futility_try_catch::try_;
19/// # fn function_that_might_fail() -> Result<(), Box<dyn std::error::Error>> {
20/// # Ok(())
21/// # }
22/// # fn handle_err() {}
23/// use std::error::Error;
24/// try_!({
25/// function_that_might_fail()?;
26/// } catch Box<dyn Error> as err {
27/// eprintln!("Oh no an error! {err}");
28/// handle_err();
29/// });
30/// ```
31///
32/// In this case we try a set of statements that can use ? without returning to
33/// the top level function scope, but only to the block it's in. This way we can
34/// use it idiomatically like we'd expect. We then state for the catch block
35/// what Error type we expect it to be and then the name of the error so we can
36/// reference it inside of the catch block. This is how it would be used most
37/// often.
38///
39/// The other way is through assignment of the final block value:
40/// ```
41/// # use futility_try_catch::try_;
42/// # fn function_that_might_fail() -> Result<(), Box<dyn std::error::Error>> {
43/// # Ok(())
44/// # }
45/// # fn handle_err() {}
46/// use std::error::Error;
47/// let try_value = try_!({
48/// function_that_might_fail()?;
49/// "This is returned if it does not fail"
50/// } catch Box<dyn Error> as err {
51/// eprintln!("Oh no an error! {err}");
52/// handle_err();
53/// "This is returned if it does fail"
54/// });
55/// ```
56///
57/// In this case you must return the same type in each block, but it does let
58/// you assign a value from the `try/catch` block if you'd like. Simply omit the
59/// semicolon like you would when returning a value in a function.
60///
61/// ### How it works/expands
62/// The macro is actually relatively small in terms of implementation and what
63/// it expands out too. This call:
64/// ```
65/// # use futility_try_catch::try_;
66/// # fn function_that_might_fail() -> Result<(), Box<dyn std::error::Error>> {
67/// # Ok(())
68/// # }
69/// # fn handle_err() {}
70/// use std::error::Error;
71/// try_!({
72/// function_that_might_fail()?;
73/// } catch Box<dyn Error> as err {
74/// eprintln!("Oh no an error! {err}");
75/// handle_err();
76/// });
77/// ```
78///
79/// expands out to:
80/// ```
81/// # use futility_try_catch::try_;
82/// # fn function_that_might_fail() -> Result<(), Box<dyn std::error::Error>> {
83/// # Ok(())
84/// # }
85/// # fn handle_err() {}
86/// use std::error::Error;
87/// match || -> Result<_, Box<dyn Error>> {
88/// Ok({
89/// function_that_might_fail()?;
90/// })
91/// }() {
92/// Ok(val) => val,
93/// Err(err) => {
94/// eprintln!("Oh no an error! {err}");
95/// handle_err();
96/// }
97/// }
98/// ```
99///
100/// This is where the magic is, if we use a closure then we can use `?` inside
101/// of it and scope it to only the block of that function. This means we don't
102/// automatically return all of the way to the top level function where the
103/// macro is invoked and we can handle the error locally! This is however, not
104/// the prettiest to look at and might be considered "unidiomatic" Rust. The
105/// macro therefore abstracts over this and makes it nicer to work with/look at.
106pub fn try_(tokens: TokenStream) -> TokenStream {
107 let TryCatchInput {
108 try_block,
109 catch_block,
110 error_ty,
111 error_ident,
112 } = parse_macro_input!(tokens as TryCatchInput);
113 let expanded = quote! {
114 match || -> ::std::result::Result<_, #error_ty> {
115 ::std::result::Result::Ok(#try_block)
116 }() {
117 ::std::result::Result::Ok(ret) => ret,
118 ::std::result::Result::Err(#error_ident) => #catch_block
119 }
120 };
121 TokenStream::from(expanded)
122}
123
124struct TryCatchInput {
125 try_block: Block,
126 catch_block: Block,
127 error_ty: Type,
128 error_ident: Ident,
129}
130
131impl Parse for TryCatchInput {
132 fn parse(input: ParseStream) -> Result<Self> {
133 let try_block: Block = input.parse()?;
134 let catch: Ident = input.parse()?;
135 assert_eq!(catch, "catch");
136 let error_ty: Type = input.parse()?;
137 let _: Token![as] = input.parse()?;
138 let error_ident: Ident = input.parse()?;
139 let catch_block: Block = input.parse()?;
140
141 Ok(Self {
142 try_block,
143 catch_block,
144 error_ty,
145 error_ident,
146 })
147 }
148}