Skip to main content

firebae_derive/
lib.rs

1//! Derive macros for [firebae-cm](https://docs.rs/firebae-cm).
2//!
3//! You normally do not depend on this crate directly — `firebae-cm` re-exports
4//! everything here.
5
6#![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
15/// Resolves the path to the `firebae-cm` crate, so the generated code keeps working
16/// when the dependency is renamed in the user's `Cargo.toml`, and when the macro is
17/// used from inside `firebae-cm` itself.
18///
19/// The `Itself` case deliberately resolves to `::firebae_cm` rather than `crate`.
20/// `proc_macro_crate` reports `Itself` for anything compiled against firebae-cm's own
21/// manifest, which includes doctests and integration tests — and there the crate root
22/// is the test, not the library. `firebae-cm` declares `extern crate self as
23/// firebae_cm;` so that `::firebae_cm` resolves in all three positions.
24fn 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
49/// Returns true for `Option<T>`, including `std::option::Option<T>`.
50/// Requires exactly one generic argument so that a user type merely *named*
51/// `Option` is not mistaken for the real thing.
52fn 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/// Implements `firebae_cm::IntoFirebaseMap` for a struct with named fields.
70///
71/// FCM `data`, `headers` and `payload` fields are `map<string, string>`, so every
72/// field is stringified via its [`Display`](std::fmt::Display) implementation.
73/// `Option<T>` fields are omitted from the map entirely when `None`.
74///
75/// ```ignore
76/// #[derive(AsFirebaseMap)]
77/// struct MessageData {
78///     custom_field: String,
79///     another_type: i32,              // becomes "15"
80///     optional_field: Option<String>, // omitted when None
81/// }
82/// ```
83#[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        // `None` fields are omitted from the map entirely rather than being
106        // serialized as an empty or placeholder value.
107        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}