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}