tusk_rs_derive/
lib.rs

1//! Procedural macros used by the [`tusk-rs`](https://crates.io/crates/tusk-rs) library.
2#[macro_use]
3extern crate quote;
4extern crate proc_macro;
5
6use proc_macro::TokenStream;
7use quote::{format_ident, ToTokens};
8use syn::{parse_macro_input, Data, DeriveInput, ItemEnum, ItemStruct};
9
10/// Derive the [`tusk_rs::FromPostgres`] trait for a struct.
11#[proc_macro_derive(FromPostgres)]
12pub fn derive_from_postgres(item: TokenStream) -> TokenStream {
13    let input = parse_macro_input!(item as ItemStruct);
14    let struct_name = input.ident;
15
16    let from_postgres_fields = input
17        .fields
18        .iter()
19        .map(|field| {
20            let field_name = field.ident.as_ref().unwrap();
21            let field_name_string = field.ident.as_ref().unwrap().to_string();
22            quote! {
23                #field_name: row.get(#field_name_string)
24            }
25        })
26        .collect::<Vec<_>>();
27
28    let try_from_postgres_fields = input.fields.iter().map(|field| {
29        let field_name = field.ident.as_ref().unwrap();
30        let field_name_string = field.ident.as_ref().unwrap().to_string();
31        quote! {
32            #field_name: row.try_get(#field_name_string).map_err(|_| tusk_rs::FromPostgresError::MissingColumn(#field_name_string))?
33        }
34    }).collect::<Vec<_>>();
35
36    quote! {
37        impl tusk_rs::FromPostgres for #struct_name {
38            fn from_postgres(row: &tusk_rs::Row) -> #struct_name {
39                #struct_name {
40                    #(#from_postgres_fields),*
41                }
42            }
43            fn try_from_postgres(row: &tusk_rs::Row) -> Result<#struct_name, tusk_rs::FromPostgresError> {
44                Ok(#struct_name {
45                    #(#try_from_postgres_fields),*
46                })
47            }
48        }
49    }.into()
50}
51
52/// Derive a default implementation of [`tusk_rs::PostgresJoins`] which returns no joins.
53#[proc_macro_derive(PostgresJoins)]
54pub fn derive_postgres_joins(item: TokenStream) -> TokenStream {
55    let input = parse_macro_input!(item as ItemStruct);
56    let struct_name = input.ident;
57
58    quote! {
59        impl tusk_rs::PostgresJoins for #struct_name {
60            fn joins() -> &'static [&'static tusk_rs::PostgresJoin] {
61                &[]
62            }
63        }
64    }
65    .into()
66}
67
68/// Derive [`tusk_rs::PostgresReadFields`] by reading all struct fields.
69#[proc_macro_derive(PostgresReadFields)]
70pub fn derive_postgres_read_fields(item: TokenStream) -> TokenStream {
71    let input = parse_macro_input!(item as ItemStruct);
72    let struct_name = input.ident;
73
74    let fields = input
75        .fields
76        .iter()
77        .map(|field| {
78            let field_name = field.ident.as_ref().unwrap().to_string();
79            quote! {
80                tusk_rs::local!(#field_name)
81            }
82        })
83        .collect::<Vec<_>>();
84
85    quote! {
86        impl tusk_rs::PostgresReadFields for #struct_name {
87            fn read_fields() -> &'static [&'static tusk_rs::PostgresField] {
88                &[#(#fields),*]
89            }
90        }
91    }
92    .into()
93}
94
95/// Blanket derive for [`tusk_rs::PostgresReadable`].
96#[proc_macro_derive(PostgresReadable)]
97pub fn derive_postgres_readable(item: TokenStream) -> TokenStream {
98    let input = parse_macro_input!(item as ItemStruct);
99    let struct_name = input.ident;
100
101    quote! {
102        impl tusk_rs::PostgresReadable for #struct_name {}
103    }
104    .into()
105}
106
107/// Derive [`tusk_rs::PostgresWriteFields`] by using all struct fields.
108#[proc_macro_derive(PostgresWriteFields)]
109pub fn derive_postgres_write_fields(item: TokenStream) -> TokenStream {
110    let input = parse_macro_input!(item as ItemStruct);
111    let struct_name = input.ident;
112
113    let fields = input
114        .fields
115        .iter()
116        .map(|field| field.ident.as_ref().unwrap().to_string())
117        .collect::<Vec<_>>();
118
119    quote! {
120        impl tusk_rs::PostgresWriteFields for #struct_name {
121            fn write_fields() -> &'static [&'static str] {
122                &[#(#fields),*]
123            }
124        }
125    }
126    .into()
127}
128/// Derive [`tusk_rs::PostgresWriteable`] for a struct so it can be inserted or updated.
129#[proc_macro_derive(PostgresWriteable)]
130pub fn derive_postgres_writeable(item: TokenStream) -> TokenStream {
131    let input = parse_macro_input!(item as ItemStruct);
132    let struct_name = input.ident;
133
134    let fields = input
135        .fields
136        .iter()
137        .map(|field| {
138            let f = field.ident.as_ref().unwrap();
139            let f_name = field.ident.as_ref().unwrap().to_string();
140            quote! {
141                #f_name => Box::new(self.#f.clone())
142            }
143        })
144        .collect::<Vec<_>>();
145
146    quote! {
147        impl tusk_rs::PostgresWriteable for #struct_name {
148            fn write(mut self) -> tusk_rs::PostgresWrite {
149                let mut arguments: Vec<Box<(dyn tusk_rs::ToSql + Sync)>> = vec![];
150                let fields = <Self as tusk_rs::PostgresWriteFields>::write_fields();
151                for f in fields {
152                    arguments.push(
153                        match *f {
154                            #(#fields),*,
155                            _ => panic!("Unknown field {}!", f)
156                        }
157                    )
158                }
159                tusk_rs::PostgresWrite {
160                    fields,
161                    arguments
162                }
163            }
164        }
165    }
166    .into()
167}
168
169/// Embed a file into the binary as a string.
170/// This is useful for HTML files or other static files
171/// that need to be represented as a string.
172///
173/// The path is derived relative to the project root, which makes
174/// it easier to import from /static, /public, or other directories.
175#[proc_macro]
176pub fn embed(item: TokenStream) -> TokenStream {
177    let path = item.to_string().replace('\"', "");
178    let resolved_path = std::fs::canonicalize(path).expect("Invalid path!");
179    let contents = std::fs::read(&resolved_path)
180        .unwrap_or_else(|_| panic!("Could not read contents at {}", resolved_path.display()));
181    let contents_string = String::from_utf8(contents).unwrap();
182    quote! {
183        #contents_string
184    }
185    .into()
186}
187
188/// Embed a file into the binary as a byte array.
189/// This is useful for binary files that need to be represented
190/// as a byte array.
191///
192/// This is similar to [`std::core::include_bytes`], but the path
193/// is derived relative to the project root, which makes it easier
194/// to import from /static, /public, or other directories.
195#[proc_macro]
196pub fn embed_binary(item: TokenStream) -> TokenStream {
197    let path = item.to_string().replace('\"', "");
198    let resolved_path = std::fs::canonicalize(path).expect("Invalid path!");
199    let contents = std::fs::read(&resolved_path)
200        .unwrap_or_else(|_| panic!("Could not read contents at {}", resolved_path.display()));
201    quote! {
202        &[#(#contents),*]
203    }
204    .into()
205}
206
207/// Derive a ToJson implementation.
208#[proc_macro_derive(ToJson)]
209pub fn derive_to_json(item: TokenStream) -> TokenStream {
210    let input = parse_macro_input!(item as DeriveInput);
211    match input.data {
212        Data::Struct(struct_ident) => {
213            let struct_name = input.ident;
214            let struct_fields = struct_ident.fields.iter().map(|x| {
215                let x_ident = &x.ident;
216                let x_key = x.ident.to_token_stream().to_string();
217                quote! {
218                    output += "\"";
219                    output += #x_key;
220                    output += "\" : ";
221                    output += &self.#x_ident.to_json();
222                    output += ",";
223                }
224            });
225            let generics = input.generics;
226            let impl_types = generics
227                .params
228                .iter()
229                .map(|x| {
230                    let d = format_ident!(
231                        "{}",
232                        x.to_token_stream()
233                            .to_string()
234                            .split(':')
235                            .next()
236                            .unwrap()
237                            .trim()
238                    );
239                    quote! {#d}
240                })
241                .collect::<Vec<_>>();
242            let impl_insert = if impl_types.is_empty() {
243                quote! {}
244            } else {
245                quote! {<#(#impl_types),*>}
246            };
247
248            let output_new = quote! {
249                impl #generics tusk_rs::ToJson for #struct_name #impl_insert {
250                    fn to_json(&self) -> String {
251                        let mut output = String::new();
252                        output += "{";
253                        #(#struct_fields)*
254                        output.pop();
255                        output += "}";
256                        return output;
257                    }
258                }
259            };
260
261            output_new.into()
262        }
263        Data::Enum(enum_ident) => {
264            let name = &input.ident;
265            let opts = enum_ident
266                .variants
267                .iter()
268                .map(|x| {
269                    let ident = &x.ident;
270                    let ident_str = format!("\"{}\"", x.ident);
271                    quote! {
272                        Self::#ident => #ident_str.to_string()
273                    }
274                })
275                .collect::<Vec<_>>();
276            quote! {
277                impl tusk_rs::ToJson for #name {
278                    fn to_json(&self) -> String {
279                        match self {
280                            #(#opts),*
281                        }
282                    }
283                }
284            }
285            .into()
286        }
287        _ => panic!("Cannot derive for this kind!"),
288    }
289}
290
291/// Derive [`tusk_rs::JsonRetrieve`] for an enum. Each variant name is expected
292/// to match its string representation in JSON.
293#[proc_macro_derive(JsonRetrieve)]
294pub fn derive_json_retrieve(item: TokenStream) -> TokenStream {
295    let enm = parse_macro_input!(item as ItemEnum);
296    let struct_name = &enm.ident;
297    let struct_name_str = enm.ident.to_string();
298
299    let fields_map = enm
300        .variants
301        .iter()
302        .map(|x| {
303            let name = &x.ident;
304            let str = format!("\"{}\"", x.ident.to_token_stream());
305            quote! {
306                #str => Ok(Self::#name)
307            }
308        })
309        .collect::<Vec<_>>();
310
311    quote! {
312        impl tusk_rs::JsonRetrieve for #struct_name {
313            fn parse(key: String, value: Option<&String>) -> Result<Self, tusk_rs::JsonParseError> {
314                let value = value.ok_or_else(|| tusk_rs::JsonParseError::NotFound(key.clone()))?;
315                match value.as_str() {
316                    #(#fields_map),*,
317                    _ => return Err(tusk_rs::JsonParseError::InvalidType(key, #struct_name_str))
318                }
319            }
320        }
321    }
322    .into()
323}
324
325/// Derive [`tusk_rs::FromJson`] for a struct.
326#[proc_macro_derive(FromJson)]
327pub fn derive_from_json(item: TokenStream) -> TokenStream {
328    let strct = parse_macro_input!(item as ItemStruct);
329    let struct_name = &strct.ident;
330
331    let fields_get = strct.fields.iter().map(|x| {
332        let x_ident = &x.ident;
333        let x_key = x.ident.to_token_stream().to_string();
334        quote! {
335            #x_ident: json.get(#x_key)?
336        }
337    });
338
339    quote! {
340        impl tusk_rs::FromJson for #struct_name {
341            fn from_json(json: &tusk_rs::JsonObject) -> Result<#struct_name, tusk_rs::JsonParseError> {
342                Ok(#struct_name {
343                    #(#fields_get),*
344                })
345            }
346        }
347    }.into()
348}