firebae-derive 0.2.0

Derive macros for firebae-cm
Documentation
//! Derive macros for [firebae-cm](https://docs.rs/firebae-cm).
//!
//! You normally do not depend on this crate directly — `firebae-cm` re-exports
//! everything here.

#![warn(missing_docs)]

extern crate proc_macro;
use self::proc_macro::TokenStream;

use proc_macro_crate::{FoundCrate, crate_name};
use quote::quote;
use syn::{DeriveInput, Fields, GenericParam, Generics, parse_macro_input, parse_quote};

/// Resolves the path to the `firebae-cm` crate, so the generated code keeps working
/// when the dependency is renamed in the user's `Cargo.toml`, and when the macro is
/// used from inside `firebae-cm` itself.
///
/// The `Itself` case deliberately resolves to `::firebae_cm` rather than `crate`.
/// `proc_macro_crate` reports `Itself` for anything compiled against firebae-cm's own
/// manifest, which includes doctests and integration tests — and there the crate root
/// is the test, not the library. `firebae-cm` declares `extern crate self as
/// firebae_cm;` so that `::firebae_cm` resolves in all three positions.
fn firebae_cm_path() -> proc_macro2::TokenStream {
    match crate_name("firebae-cm") {
        Ok(FoundCrate::Name(name)) => {
            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
            quote!(::#ident)
        }
        Ok(FoundCrate::Itself) | Err(_) => quote!(::firebae_cm),
    }
}

fn add_trait_bounds(mut generics: Generics, krate: &proc_macro2::TokenStream) -> Generics {
    for param in &mut generics.params {
        if let GenericParam::Type(ref mut type_param) = *param {
            type_param
                .bounds
                .push(parse_quote!(#krate::FirebaseMapValue));
        }
    }
    generics
}

fn error(span: proc_macro2::Span, message: &str) -> proc_macro2::TokenStream {
    syn::Error::new(span, message).into_compile_error()
}

/// Returns true for `Option<T>`, including `std::option::Option<T>`.
/// Requires exactly one generic argument so that a user type merely *named*
/// `Option` is not mistaken for the real thing.
fn is_option(ty: &syn::Type) -> bool {
    let syn::Type::Path(tp) = ty else {
        return false;
    };
    if tp.qself.is_some() {
        return false;
    }
    let Some(segment) = tp.path.segments.last() else {
        return false;
    };
    segment.ident == "Option"
        && matches!(
            &segment.arguments,
            syn::PathArguments::AngleBracketed(args) if args.args.len() == 1
        )
}

/// Implements `firebae_cm::IntoFirebaseMap` for a struct with named fields.
///
/// FCM `data`, `headers` and `payload` fields are `map<string, string>`, so every
/// field is stringified via its [`Display`](std::fmt::Display) implementation.
/// `Option<T>` fields are omitted from the map entirely when `None`.
///
/// ```ignore
/// #[derive(AsFirebaseMap)]
/// struct MessageData {
///     custom_field: String,
///     another_type: i32,              // becomes "15"
///     optional_field: Option<String>, // omitted when None
/// }
/// ```
#[proc_macro_derive(AsFirebaseMap)]
pub fn impl_as_firebase_map(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let span = ast.ident.span();

    let name = &ast.ident;
    let krate = firebae_cm_path();
    let generics = add_trait_bounds(ast.generics, &krate);
    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

    let data = match ast.data {
        syn::Data::Struct(data) => data,
        _ => return error(span, "AsFirebaseMap should be called on a struct").into(),
    };

    let fields = match data.fields {
        Fields::Named(fields) => fields.named,
        _ => return error(span, "AsFirebaseMap only works on named fields").into(),
    };

    let inserts = fields.into_iter().map(|f| {
        let name = f.ident.unwrap();
        // `None` fields are omitted from the map entirely rather than being
        // serialized as an empty or placeholder value.
        if is_option(&f.ty) {
            quote! { if let Some(ref val) = self.#name { h.insert(stringify!(#name), val); } }
        } else {
            quote! { h.insert(stringify!(#name), &self.#name); }
        }
    });

    TokenStream::from(quote! {
        impl #impl_generics #krate::IntoFirebaseMap for #name #ty_generics #where_clause {
            fn as_map(&self) -> #krate::FirebaseMap {
                let mut h = #krate::FirebaseMap::new();
                #(#inserts)*
                h
            }
        }
    })
}