rabbithole-derive 0.3.1

The macro system helping users to model the relationship of their data
Documentation
extern crate proc_macro;
#[macro_use]
extern crate thiserror;
#[macro_use]
extern crate lazy_static;

mod backend;
mod error;
mod field;

use crate::error::EntityDecoratorError;
use crate::field::{get_field_type, FieldType};
use proc_macro::TokenStream;
use quote::{quote, TokenStreamExt};
use std::collections::HashSet;
use syn::DeriveInput;

type FieldBundle<'a> =
    (&'a syn::Ident, Vec<&'a syn::Ident>, Vec<&'a syn::Ident>, Vec<&'a syn::Ident>);

#[proc_macro_derive(EntityDecorator, attributes(entity))]
pub fn derive(input: TokenStream) -> TokenStream {
    inner_derive(input).unwrap_or_else(|err| err.to_compile_error()).into()
}

#[allow(clippy::cognitive_complexity)]
fn inner_derive(input: TokenStream) -> syn::Result<proc_macro2::TokenStream> {
    let ast: DeriveInput = syn::parse(input)?;
    let decorated_struct: &syn::Ident = &ast.ident;
    let struct_lifetime = &ast.generics;

    let (entity_type, backends) = get_entity_type(&ast)?;

    let (id, attrs, to_ones, to_manys) = get_fields(&ast)?;

    let mut res = quote! {
        impl #struct_lifetime rabbithole::entity::Entity for #decorated_struct#struct_lifetime {
            fn included(&self, uri: &str,
                include_query: &std::option::Option<rabbithole::query::IncludeQuery>,
                fields_query: &rabbithole::query::FieldsQuery,
            ) -> rabbithole::RbhResult<rabbithole::model::document::Included> {
                use rabbithole::entity::SingleEntity;
                use std::convert::TryInto;
                let mut included: rabbithole::model::document::Included = Default::default();

                if let Some(included_fields) = include_query {
                    for inc in included_fields {
                        if inc.contains('.') {
                            return Err(rabbithole::model::error::Error::RelationshipPathNotSupported(&inc, None));
                        }
                    }
                }
                #(
                    if let Some(included_fields) = include_query {
                        if included_fields.contains(stringify!(#to_ones)) {
                            if let Some(inc) = self.#to_ones.to_resource(uri, fields_query) {
                                included.insert(inc.id.clone(), inc);
                            }
                        }
                    } else {
                        if let Some(inc) = self.#to_ones.to_resource(uri, fields_query) {
                            included.insert(inc.id.clone(), inc);
                        }
                    }
                )*
                #(
                    if let Some(included_fields) = include_query {
                        if included_fields.contains(stringify!(#to_manys)) {
                            for item in &self.#to_manys {
                                if let Some(inc) = item.to_resource(uri, fields_query) {
                                    included.insert(inc.id.clone(), inc);
                                }
                            }
                        }
                    } else {
                        for item in &self.#to_manys {
                            if let Some(inc) = item.to_resource(uri, fields_query) {
                                included.insert(inc.id.clone(), inc);
                            }
                        }
                    }
                )*
                Ok(included)
             }

             fn to_document_automatically(&self, uri: &str, query: &rabbithole::query::Query, request_path: &rabbithole::model::link::RawUri) -> rabbithole::RbhResult<rabbithole::model::document::Document> {
                 rabbithole::entity::SingleEntity::to_document_automatically(&self, uri, query, request_path)
             }
        }

        impl #struct_lifetime rabbithole::entity::SingleEntity for #decorated_struct#struct_lifetime {
            fn ty() -> std::string::String { #entity_type.to_string() }
            fn id(&self) -> std::string::String { self.#id.to_string() }

            fn attributes(&self) -> rabbithole::model::resource::Attributes {
                let mut attr_map: std::collections::HashMap<String, serde_json::Value> = std::default::Default::default();
                #(  if let Ok(json_value) = serde_json::to_value(self.#attrs.clone()) { attr_map.insert(stringify!(#attrs).to_string(), json_value); } )*
                attr_map.into()
            }

            fn relationships(&self, uri: &str) -> rabbithole::model::relationship::Relationships {
                let mut relat_map: rabbithole::model::relationship::Relationships = std::default::Default::default();
                #(
                    if let Some(relat_id) = self.#to_ones.to_resource_identifier() {
                        let data = rabbithole::model::resource::IdentifierData::Single(Some(relat_id));
                        let relat = rabbithole::model::relationship::Relationship { data, links: self.to_relationship_links(stringify!(#to_ones), uri), ..std::default::Default::default() };
                        relat_map.insert(stringify!(#to_ones).to_string(), relat);
                    }
                )*

                #(
                    let mut relat_ids: rabbithole::model::resource::ResourceIdentifiers = std::default::Default::default();
                    for item in &self.#to_manys {
                        if let Some(relat_id) = item.to_resource_identifier() {
                            relat_ids.push(relat_id);
                        }
                    }
                    let data = rabbithole::model::resource::IdentifierData::Multiple(relat_ids);
                    let relat = rabbithole::model::relationship::Relationship { data, links: self.to_relationship_links(stringify!(#to_manys), uri), ..std::default::Default::default() };
                    relat_map.insert(stringify!(#to_manys).to_string(), relat);
                )*

                relat_map
            }
        }


    };

    for back in backends {
        if back == "actix" {
            res.append_all(vec![backend::actix::generate_app(
                decorated_struct,
                &entity_type,
                &to_ones,
                &to_manys,
            )]);
        }
    }

    Ok(res)
}

fn get_meta(attrs: &[syn::Attribute]) -> syn::Result<Vec<syn::Meta>> {
    Ok(attrs
        .iter()
        .filter(|a| a.path.is_ident("entity"))
        .filter_map(|a| {
            let res = a.parse_meta();
            res.ok()
        })
        .collect::<Vec<syn::Meta>>())
}

fn get_entity_type(ast: &syn::DeriveInput) -> syn::Result<(String, HashSet<String>)> {
    let mut ty_opt: Option<String> = None;
    let mut backends: HashSet<String> = Default::default();

    for meta in get_meta(&ast.attrs)? {
        if let syn::Meta::List(syn::MetaList { ref nested, .. }) = meta {
            if let Some(syn::NestedMeta::Meta(ref meta_item)) = nested.last() {
                match meta_item {
                    syn::Meta::NameValue(syn::MetaNameValue {
                        path,
                        lit: syn::Lit::Str(lit_str),
                        ..
                    }) => match path.segments.last() {
                        Some(syn::PathSegment { ident, .. }) if ident == "type" => {
                            ty_opt = Some(lit_str.value());
                        },
                        _ => {},
                    },
                    syn::Meta::List(syn::MetaList { path, nested, .. }) => {
                        match path.segments.last() {
                            Some(syn::PathSegment { ident, .. }) if ident == "backend" => {
                                for nested_backend in nested {
                                    if let syn::NestedMeta::Meta(syn::Meta::Path(backend_path)) =
                                        nested_backend
                                    {
                                        if let Some(syn::PathSegment { ident, .. }) =
                                            backend_path.segments.last()
                                        {
                                            backends.insert(ident.to_string());
                                        }
                                    }
                                }
                            },
                            _ => {},
                        }
                    },
                    _ => {},
                }
            }
        }
    }

    if let Some(ty) = ty_opt {
        Ok((ty, backends))
    } else {
        Err(syn::Error::new_spanned(ast, EntityDecoratorError::InvalidEntityType))
    }
}

fn get_fields(ast: &syn::DeriveInput) -> syn::Result<FieldBundle> {
    if let syn::Data::Struct(syn::DataStruct {
        fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
        ..
    }) = ast.data
    {
        let mut id = None;
        let mut attrs = vec![];
        let mut to_ones = vec![];
        let mut to_manys = vec![];

        for n in named {
            let f: FieldType = get_field_type(n)?;
            match (f, n.ident.as_ref()) {
                (FieldType::Id, Some(ident)) if id.is_none() => id = Some(ident),
                (FieldType::Id, _) => {
                    return Err(syn::Error::new_spanned(n, EntityDecoratorError::DuplicatedId))
                },
                (FieldType::ToOne, Some(ident)) => to_ones.push(ident),
                (FieldType::ToMany, Some(ident)) => to_manys.push(ident),
                (FieldType::Plain, Some(ident)) => {
                    attrs.push(ident);
                },
                _ => {
                    return Err(syn::Error::new_spanned(n, EntityDecoratorError::FieldWithoutName))
                },
            }
        }

        if let Some(id) = id {
            return Ok((id, attrs, to_ones, to_manys));
        }
    }
    Err(syn::Error::new_spanned(&ast.ident, EntityDecoratorError::InvalidEntityType))
}