solders_macros/
lib.rs

1//! A collection of attribute macros to reduce boilerplate in the
2//! [solders](https://github.com/kevinheavey/solders) project.
3//!
4//! These macros make some very specific assumptions about the structs
5//! they're applied to, so they're unlikely to be useful for other projects.
6use proc_macro::TokenStream;
7use proc_macro2::Ident;
8use quote::{quote, ToTokens};
9use syn::{parse_macro_input, ImplItem, ItemEnum, ItemImpl};
10
11/// Add a `__hash__` to the impl using the `PyHash` trait.
12///
13/// # Example
14///
15/// ```rust
16/// use solders_macros::pyhash;
17///
18/// #[derive(Debug)]
19/// struct Foo(u8);
20///
21/// #[pyhash]
22/// impl Foo {
23///   pub fn pyhash(&self) -> u64 {  // Fake implementation in place of `PyHash`.
24///      self.0.into()
25///   }
26/// }
27///
28/// let foo = Foo(3);
29/// assert_eq!(3, foo.__hash__());
30///
31/// ```
32#[proc_macro_attribute]
33pub fn pyhash(_: TokenStream, item: TokenStream) -> TokenStream {
34    let mut ast = parse_macro_input!(item as ItemImpl);
35    let to_add = quote! {pub fn __hash__(&self) -> u64 {
36        solders_traits_core::PyHash::pyhash(self)
37    }};
38    ast.items.push(ImplItem::Verbatim(to_add));
39    TokenStream::from(ast.to_token_stream())
40}
41
42/// Add a `__richcmp__` to the impl using the `RichcmpFull` trait.
43///
44/// # Example
45///
46/// ```rust
47/// use solders_macros::richcmp_full;
48///
49///
50/// #[derive(Debug)]
51/// struct Foo(u8);
52///
53/// #[richcmp_full]
54/// impl Foo {
55///   pub fn richcmp(&self, other: &Self, op: CompareOp) -> bool {  // Fake implementation in place of `RichcmpFull`.
56///      true
57///   }
58/// }
59///
60/// let foo = Foo(3);
61/// assert_eq!(true, foo.__richcmp__(&foo, CompareOp::Eq));
62///
63/// ```
64#[proc_macro_attribute]
65pub fn richcmp_full(_: TokenStream, item: TokenStream) -> TokenStream {
66    let mut ast = parse_macro_input!(item as ItemImpl);
67    let to_add = quote! {pub fn __richcmp__(&self, other: &Self, op: pyo3::basic::CompareOp) -> bool {
68        solders_traits_core::RichcmpFull::richcmp(self, other, op)
69    }};
70    ast.items.push(ImplItem::Verbatim(to_add));
71    TokenStream::from(ast.to_token_stream())
72}
73
74/// Add a `__richcmp__` to the impl using the `RichcmpEqualityOnly` trait.
75#[proc_macro_attribute]
76pub fn richcmp_eq_only(_: TokenStream, item: TokenStream) -> TokenStream {
77    let mut ast = parse_macro_input!(item as ItemImpl);
78    let to_add = quote! {pub fn __richcmp__(&self, other: &Self, op: pyo3::basic::CompareOp) -> pyo3::prelude::PyResult<bool> {
79        solders_traits_core::RichcmpEqualityOnly::richcmp(self, other, op)
80    }};
81    ast.items.push(ImplItem::Verbatim(to_add));
82    TokenStream::from(ast.to_token_stream())
83}
84
85/// Add a `__richcmp__` to the impl using the `RichcmpSigner` trait.
86#[proc_macro_attribute]
87pub fn richcmp_signer(_: TokenStream, item: TokenStream) -> TokenStream {
88    let mut ast = parse_macro_input!(item as ItemImpl);
89    let to_add = quote! {pub fn __richcmp__(&self, other: crate::signer::Signer, op: pyo3::basic::CompareOp) -> pyo3::prelude::PyResult<bool> {
90        solders_traits::RichcmpSigner::richcmp(self, other, op)
91    }};
92    ast.items.push(ImplItem::Verbatim(to_add));
93    TokenStream::from(ast.to_token_stream())
94}
95
96fn add_core_methods(ast: &mut ItemImpl) {
97    let mut methods = vec![
98        ImplItem::Verbatim(
99            quote! {pub fn __bytes__<'a>(&self, py: pyo3::prelude::Python<'a>) -> &'a pyo3::types::PyBytes  {
100                solders_traits_core::CommonMethodsCore::pybytes(self, py)
101            }},
102        ),
103        ImplItem::Verbatim(quote! { pub fn __str__(&self) -> String {
104            solders_traits_core::CommonMethodsCore::pystr(self)
105        } }),
106        ImplItem::Verbatim(quote! { pub fn __repr__(&self) -> String {
107            solders_traits_core::CommonMethodsCore::pyrepr(self)
108        } }),
109        ImplItem::Verbatim(
110            quote! { pub fn __reduce__(&self) -> pyo3::prelude::PyResult<(pyo3::prelude::PyObject, pyo3::prelude::PyObject)> {
111                solders_traits_core::CommonMethodsCore::pyreduce(self)
112            } },
113        ),
114    ];
115    if !ast.items.iter().any(|item| match item {
116        ImplItem::Method(m) => m.sig.ident == "from_bytes",
117        _ => false,
118    }) {
119        let from_bytes = ImplItem::Verbatim(quote! {
120            /// Deserialize from bytes.
121            ///
122            /// Args:
123            ///     data (bytes): the serialized object.
124            ///
125            /// Returns: the deserialized object.
126            ///
127            #[staticmethod]
128            pub fn from_bytes(data: &[u8]) -> PyResult<Self> {
129                <Self as solders_traits_core::CommonMethodsCore>::py_from_bytes(data)
130            }
131        });
132        methods.push(from_bytes);
133    };
134    ast.items.extend_from_slice(&methods);
135}
136
137/// Add `__bytes__`, `__str__`, `__repr__` and `__reduce__` using the `CommonMethodsCore` trait.
138///
139/// Also add `from_bytes` if not already defined.
140#[proc_macro_attribute]
141pub fn common_methods_core(_: TokenStream, item: TokenStream) -> TokenStream {
142    let mut ast = parse_macro_input!(item as ItemImpl);
143    add_core_methods(&mut ast);
144    TokenStream::from(ast.to_token_stream())
145}
146
147/// Add `__bytes__`, `__str__`, `__repr__` and `__reduce__`, `to_json` and `from_json` using the `CommonMethods` trait.
148///
149/// Also add `from_bytes` if not already defined.
150#[proc_macro_attribute]
151pub fn common_methods(_: TokenStream, item: TokenStream) -> TokenStream {
152    let mut ast = parse_macro_input!(item as ItemImpl);
153    add_core_methods(&mut ast);
154    let methods = vec![
155        ImplItem::Verbatim(quote! {
156        /// Convert to a JSON string.
157        pub fn to_json(&self) -> String {
158            solders_traits_core::CommonMethods::py_to_json(self)
159        } }),
160        ImplItem::Verbatim(quote! {
161        /// Build from a JSON string.
162        #[staticmethod] pub fn from_json(raw: &str) -> PyResult<Self> {
163            <Self as solders_traits_core::CommonMethods>::py_from_json(raw)
164        } }),
165    ];
166    ast.items.extend_from_slice(&methods);
167    TokenStream::from(ast.to_token_stream())
168}
169
170/// Add `__bytes__`, `__str__`, `__repr__`, `__reduce__`, `to_json`, `from_json`, `from_bytes` and `__richcmp__` using the `CommonMethodsRpcResp` trait.
171#[proc_macro_attribute]
172pub fn common_methods_rpc_resp(_: TokenStream, item: TokenStream) -> TokenStream {
173    let mut ast = parse_macro_input!(item as ItemImpl);
174    let methods = vec![
175        ImplItem::Verbatim(
176            quote! {pub fn __bytes__<'a>(&self, py: pyo3::prelude::Python<'a>) -> &'a pyo3::types::PyBytes  {
177                CommonMethodsRpcResp::pybytes(self, py)
178            }},
179        ),
180        ImplItem::Verbatim(quote! { pub fn __str__(&self) -> String {
181            CommonMethodsRpcResp::pystr(self)
182        } }),
183        ImplItem::Verbatim(quote! { pub fn __repr__(&self) -> String {
184            CommonMethodsRpcResp::pyrepr(self)
185        } }),
186        ImplItem::Verbatim(
187            quote! { pub fn __reduce__(&self) -> pyo3::prelude::PyResult<(pyo3::prelude::PyObject, pyo3::prelude::PyObject)> {
188                CommonMethodsRpcResp::pyreduce(self)
189            } },
190        ),
191        ImplItem::Verbatim(quote! {
192        /// Convert to a JSON string.
193        pub fn to_json(&self) -> String {
194            CommonMethodsRpcResp::py_to_json(self)
195        } }),
196        ImplItem::Verbatim(quote! {
197        /// Build from a JSON string.
198        ///
199        /// Args:
200        ///     raw (str): The RPC JSON response (can be an error response).
201        ///
202        /// Returns:
203        ///     Either the deserialized object or ``RPCError``.
204        ///
205        #[staticmethod]
206        pub fn from_json(raw: &str) -> PyResult<Resp<Self>> {
207            <Self as CommonMethodsRpcResp>::py_from_json(raw)
208        } }),
209        ImplItem::Verbatim(quote! {
210            /// Deserialize from bytes.
211            ///
212            /// Args:
213            ///     data (bytes): the serialized object.
214            ///
215            /// Returns: the deserialized object.
216            ///
217            #[staticmethod]
218            pub fn from_bytes(data: &[u8]) -> PyResult<Self> {
219                <Self as CommonMethodsRpcResp>::py_from_bytes(data)
220            }
221        }),
222        ImplItem::Verbatim(
223            quote! {pub fn __richcmp__(&self, other: &Self, op: pyo3::basic::CompareOp) -> pyo3::prelude::PyResult<bool> {
224                solders_traits_core::RichcmpEqualityOnly::richcmp(self, other, op)
225            }},
226        ),
227    ];
228    ast.items.extend_from_slice(&methods);
229    TokenStream::from(ast.to_token_stream())
230}
231
232/// Add an `id` getter to an RPC request object.
233///
234/// By convention, assumes the `id` lives at `self.base.id`.
235#[proc_macro_attribute]
236pub fn rpc_id_getter(_: TokenStream, item: TokenStream) -> TokenStream {
237    let mut ast = parse_macro_input!(item as ItemImpl);
238    let to_add = quote! {
239    /// int: The ID of the RPC request.
240    #[getter]
241    pub fn id(&self) -> u64 {
242        self.base.id
243    }};
244    ast.items.push(ImplItem::Verbatim(to_add));
245    TokenStream::from(ast.to_token_stream())
246}
247
248/// Add mappings to and from another enum that has the exact same fields.
249///
250/// # Example
251///
252/// ```rust
253/// use solders_macros::enum_original_mapping;
254///
255/// #[derive(PartialEq, Debug)]
256/// pub enum Foo {
257///   A,
258///   B
259/// }
260/// #[enum_original_mapping(Foo)]
261/// #[derive(PartialEq, Debug)]
262/// pub enum Bar {
263///   A,
264///   B,
265/// }
266///
267/// let a = Bar::A;
268/// let b = Foo::B;
269/// assert_eq!(Foo::from(a), Foo::A);
270/// assert_eq!(Bar::from(b), Bar::B);
271///
272#[proc_macro_attribute]
273pub fn enum_original_mapping(original: TokenStream, item: TokenStream) -> TokenStream {
274    let mut new_stream = proc_macro2::TokenStream::from(item.clone());
275    let ast = parse_macro_input!(item as ItemEnum);
276    let enum_name = ast.ident;
277    let orig = parse_macro_input!(original as Ident);
278    let variant_names: Vec<Ident> = ast.variants.into_iter().map(|v| v.ident).collect();
279    let from_impl = quote! {
280        impl From<#orig> for #enum_name {
281            fn from(left: #orig) -> Self {
282                match left {
283                    #(#orig::#variant_names => Self::#variant_names),*,
284                    _ => panic!("Unrecognized variant: {:?}", left)
285                }
286            }
287        }
288
289        impl From<#enum_name> for #orig {
290            fn from(left: #enum_name) -> Self {
291                match left {
292                    #(#enum_name::#variant_names => Self::#variant_names),*
293                }
294            }
295        }
296    };
297    new_stream.extend(from_impl);
298    TokenStream::from(new_stream)
299}
300
301/// Impl IntoPy<PyObject> for an ADT where each variant is a newtype.
302///
303/// # Example
304///
305/// ```rust
306/// use solders_macros::EnumIntoPy;
307///
308/// #[derive(PartialEq, Debug, EnumIntoPy)]
309/// pub enum Foo {
310///   A(u8),
311///   B(u8)
312/// }
313///
314#[proc_macro_derive(EnumIntoPy)]
315pub fn enum_into_py(item: TokenStream) -> TokenStream {
316    let ast = parse_macro_input!(item as ItemEnum);
317    let enum_name = ast.ident;
318    let variant_names: Vec<Ident> = ast.variants.into_iter().map(|v| v.ident).collect();
319    let into_py_impl = quote! {
320        impl IntoPy<PyObject> for #enum_name {
321            fn into_py(self, py: Python<'_>) -> PyObject {
322                match self {
323                    #(Self::#variant_names(x) => x.into_py(py)),*,
324                }
325            }
326        }
327    };
328    into_py_impl.to_token_stream().into()
329}