1#![warn(missing_docs)]
7
8extern crate proc_macro;
9use self::proc_macro::TokenStream;
10
11use proc_macro_crate::{FoundCrate, crate_name};
12use quote::quote;
13use syn::{DeriveInput, Fields, GenericParam, Generics, parse_macro_input, parse_quote};
14
15fn firebae_cm_path() -> proc_macro2::TokenStream {
25 match crate_name("firebae-cm") {
26 Ok(FoundCrate::Name(name)) => {
27 let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
28 quote!(::#ident)
29 }
30 Ok(FoundCrate::Itself) | Err(_) => quote!(::firebae_cm),
31 }
32}
33
34fn add_trait_bounds(mut generics: Generics, krate: &proc_macro2::TokenStream) -> Generics {
35 for param in &mut generics.params {
36 if let GenericParam::Type(ref mut type_param) = *param {
37 type_param
38 .bounds
39 .push(parse_quote!(#krate::FirebaseMapValue));
40 }
41 }
42 generics
43}
44
45fn error(span: proc_macro2::Span, message: &str) -> proc_macro2::TokenStream {
46 syn::Error::new(span, message).into_compile_error()
47}
48
49fn is_option(ty: &syn::Type) -> bool {
53 let syn::Type::Path(tp) = ty else {
54 return false;
55 };
56 if tp.qself.is_some() {
57 return false;
58 }
59 let Some(segment) = tp.path.segments.last() else {
60 return false;
61 };
62 segment.ident == "Option"
63 && matches!(
64 &segment.arguments,
65 syn::PathArguments::AngleBracketed(args) if args.args.len() == 1
66 )
67}
68
69#[proc_macro_derive(AsFirebaseMap)]
84pub fn impl_as_firebase_map(input: TokenStream) -> TokenStream {
85 let ast = parse_macro_input!(input as DeriveInput);
86 let span = ast.ident.span();
87
88 let name = &ast.ident;
89 let krate = firebae_cm_path();
90 let generics = add_trait_bounds(ast.generics, &krate);
91 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
92
93 let data = match ast.data {
94 syn::Data::Struct(data) => data,
95 _ => return error(span, "AsFirebaseMap should be called on a struct").into(),
96 };
97
98 let fields = match data.fields {
99 Fields::Named(fields) => fields.named,
100 _ => return error(span, "AsFirebaseMap only works on named fields").into(),
101 };
102
103 let inserts = fields.into_iter().map(|f| {
104 let name = f.ident.unwrap();
105 if is_option(&f.ty) {
108 quote! { if let Some(ref val) = self.#name { h.insert(stringify!(#name), val); } }
109 } else {
110 quote! { h.insert(stringify!(#name), &self.#name); }
111 }
112 });
113
114 TokenStream::from(quote! {
115 impl #impl_generics #krate::IntoFirebaseMap for #name #ty_generics #where_clause {
116 fn as_map(&self) -> #krate::FirebaseMap {
117 let mut h = #krate::FirebaseMap::new();
118 #(#inserts)*
119 h
120 }
121 }
122 })
123}