Skip to main content

skopje_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    Data,
5    DeriveInput,
6    Fields,
7    FieldsNamed,
8    // parse::{Parse, ParseStream},
9    parse_macro_input,
10};
11
12// /// ```rust
13// /// #[derive(Debug, serde::Deserialize)]
14// /// #[skopje::extract(
15// ///     method = HTTP_GET,
16// ///     url = "https://api.binance.com/api/v1/ticker/allBookTickers",
17// /// )]
18// /// #[skopje::load(
19// ///     method = PG_INSERT,
20// ///     client = deadpool_postgres::Pool,
21// ///     obj = self.0
22// /// )]
23// /// pub struct Symbols(pub Vec<Symbol>);
24// /// ```
25// ///
26// /// Above is equivalent to:
27// ///
28// /// ```rust
29// /// #[derive(Debug, serde::Deserialize)]
30// /// pub struct Symbols(pub Vec<Symbol>);
31// ///
32// /// #[skopje::async_trait]
33// /// impl skopje::etl::Extract for Symbols {
34// ///     type Client = skopje::HttpClient;
35// ///     async fn extract(client: Self::Client) -> Result<Self> {
36// ///         let url = "https://api.binance.com/api/v1/ticker/allBookTickers";
37// ///         let data: Self = client.fetch(url).await?;
38// ///         Ok(data)
39// ///     }
40// /// }
41// ///
42// /// #[skopje::async_trait]
43// /// impl skopje::etl::Load for Symbols {
44// ///     type Client = skopje::PgPool;
45// ///     async fn load(&self, client: Self::Client) -> Result<()> {
46// ///         client
47// ///             .insert(super::common_sql::INSERT_SYMBOL, self.0.iter())
48// ///             .await?;
49// ///         Ok(())
50// ///     }
51// /// }
52// /// ```
53// #[proc_macro_attribute]
54// pub fn extract(item: TokenStream, attr: TokenStream) -> TokenStream {
55//     let args = parse_macro_input!(attr as ExtractArgs);
56//     quote! {}.into()
57// }
58
59// struct ExtractArgs<'a> {
60//     method: Option<&'a str>,
61//     url: Option<&'a str>,
62//     path: Option<&'a str>,
63// }
64
65// impl Parse for ExtractArgs<'_> {
66//     fn parse(input: ParseStream) -> syn::Result<Self> {
67//         let mut output = Self {
68//             method: None,
69//             url: None,
70//             path: None,
71//         };
72
73//         Ok(output)
74//     }
75// }
76
77/// Provide a like-for-like implementation of ['crate::load::pg::SqlMap`].
78/// Take the following:
79///
80/// ```rust
81/// #[derive(SqlMap)]
82/// struct MyStruct {
83///     field0: String,
84///     field1: i64,
85/// }
86/// ```
87///
88/// Above is equivalent to below:
89///
90/// ```rust
91/// struct MyStruct {
92///     field0: String,
93///     field1: i64,
94/// }
95///
96/// impl skopje::load::pg::SqlMap for &MyStruct {
97///     fn sql_map(&self) -> std::vec::Vec<&(dyn skopje::ToSql + std::marker::Sync)> {
98///         std::vec::Vec::from([
99///             &self.field0,
100///             &self.field1,
101///         ])
102///     }
103/// }
104/// ```
105#[proc_macro_derive(SqlMap)]
106pub fn derive_sql_map(item: TokenStream) -> TokenStream {
107    let body = parse_macro_input!(item as DeriveInput);
108
109    // Extract the struct name.
110    let struct_name = &body.ident;
111
112    // Extract field names.
113    let fields = match &body.data {
114        Data::Struct(data_struct) => match &data_struct.fields {
115            Fields::Named(FieldsNamed { named, .. }) => named,
116            _ => panic!("SqlMap can only be derived for structs with named fields"),
117        },
118        _ => panic!("SqlMap can only be derived for structs"),
119    };
120
121    // Create an array of references to each field.
122    let field_refs = fields.iter().map(|field| {
123        let field_name = &field.ident;
124        quote! { &self.#field_name }
125    });
126
127    // Return the implementation.
128    quote! {
129        impl skopje::load::pg::SqlMap for &#struct_name {
130            fn sql_map(&self) -> std::vec::Vec<&(dyn skopje::ToSql + std::marker::Sync)> {
131                vec![#(#field_refs),*]
132             }
133        }
134    }
135    .into()
136}