json_api_derive 0.1.0

Code generation for json_api
mod symbol;
use symbol::*;

use proc_macro2::TokenStream;
use quote::quote;
use syn::export::ToTokens;
use syn::{
    Attribute,
    Data::Struct,
    DeriveInput, Error, Field, Fields, Ident,
    Lit::Str,
    Meta::{List, NameValue, Path},
    NestedMeta::Meta,
};

pub struct ResourceContext {
    url: Option<String>,
    errors: Vec<Error>,
    fields: Vec<Ident>,
    id_field: Option<Ident>,
    relationship_fields: Vec<Ident>,
    resource_name: Option<Ident>,
}

impl ResourceContext {
    pub fn new() -> Self {
        ResourceContext {
            url: None,
            errors: vec![],
            fields: vec![],
            id_field: None,
            relationship_fields: vec![],
            resource_name: None,
        }
    }

    pub fn from(ast: &DeriveInput) -> ResourceContext {
        let mut context = ResourceContext::new();
        parse(&ast, &mut context);
        context
    }

    pub fn add_error(&mut self, err: Error) {
        self.errors.push(err);
    }

    pub fn check(&self) -> Result<(), Vec<Error>> {
        let errors = self.errors.clone();
        match errors.len() {
            0 => Ok(()),
            _ => Err(errors),
        }
    }

    pub fn to_token_stream(&self) -> TokenStream {
        match self.check() {
            Ok(_) => {
                let name = self.resource_name.as_ref().unwrap();
                let id_field = self.id_field.as_ref().unwrap();
                let fields = &self.fields;
                let mut definitions = vec![];
                for ident in fields {
                    let name = ident.to_string();
                    definitions.push(quote!(map.insert(#name.to_string(), serde_json::json!(self.#ident.clone()));));
                }

                let relationships: Vec<TokenStream> = self.relationship_fields.iter().map(|ident| {
                    let resource = name.to_string().to_lowercase();
                    let name = ident.to_string();
                    let x = quote!(Some(json_api::Data::Singular(json_api::ResourceType::Identifier(self.#ident.clone().to_identifier()))));
                    quote!(relationships.insert(#name.to_string(), json_api::Relationship::new(json_api::Links::new(vec![json_api::Link::Related(&(format!("/{}/{}/{}", #resource, self.get_id(), #name)))]), #x));)
               }).collect();

                let add_relationships = if self.relationship_fields.is_empty() {
                    quote!(object.relationships(None);)
                } else {
                    quote! {
                     let mut relationships = std::collections::HashMap::new();
                      #(#relationships)*
                      object.relationships(Some(relationships));
                    }
                };

                return quote! {
                    use json_api::*;

                    impl ResourceTrait for #name {
                        fn get_attributes(&self) -> Option<std::collections::HashMap<String, serde_json::Value>> {
                            let mut map = std::collections::HashMap::new();
                            #(#definitions)*
                            Some(map)
                        }

                        fn get_id(&self) -> String {
                            self.#id_field.to_string()
                        }

                        fn to_identifier(&self) -> ResourceIdentifierObject {
                            ResourceIdentifierObject::new(stringify!(#name).to_lowercase(), self.get_id())
                        }

                        fn to_resource_object(&self) -> ResourceObject {
                            let mut object = ResourceObject::new(stringify!(#name).to_lowercase(), self.get_id());
                            object.add_attributes(self.get_attributes());
                            #add_relationships
                            object
                        }
                    }
                };
            }
            Err(errors) => to_compile_errors(errors),
        }
    }
}

fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
    let compile_errors = errors.iter().map(syn::Error::to_compile_error);
    quote!(#(#compile_errors)*)
}

fn parse(ast: &DeriveInput, context: &mut ResourceContext) {
    parse_name(ast, context);
    parse_attrs(ast, context);
    parse_data(ast, context);
}

fn parse_data(ast: &DeriveInput, context: &mut ResourceContext) {
    match &ast.data {
        Struct(data) => {
            parse_fields(&data.fields, context);
            if context.id_field.is_none() {
                context.add_error(Error::new_spanned(
                    ast.to_token_stream(),
                    "an id field is required for this resource",
                ));
            }
        }
        _ => context.add_error(Error::new(ast.ident.span(), "Expected Struct")),
    }
}

fn parse_field_attributes(field: &Field, attributes: &[Attribute], context: &mut ResourceContext) {
    for attribute in attributes {
        if let Ok(metadata) = attribute.parse_meta() {
            if let List(meta_list) = metadata {
                for nested_metadata in meta_list.nested.iter() {
                    match nested_metadata {
                        Meta(Path(word)) if word.clone() == ID => {
                            if let Some(ident) = &field.ident {
                                match context.id_field {
                                    None => {
                                        context.id_field = Some(Ident::new(
                                            ident.to_string().as_str(),
                                            ident.span(),
                                        ));
                                    }
                                    Some(_) => context.add_error(Error::new_spanned(
                                        field.to_token_stream(),
                                        "only one id field is expected",
                                    )),
                                }
                            }
                        }
                        Meta(Path(word)) if word.clone() == TO_ONE => {
                            if let Some(ident) = &field.ident {
                                context
                                    .relationship_fields
                                    .push(Ident::new(ident.to_string().as_str(), ident.span()))
                            }
                        }
                        _ => {}
                    }
                }
            }
        }
    }
}

fn parse_fields(fields: &Fields, context: &mut ResourceContext) {
    if let Fields::Named(named_fields) = fields {
        for field in named_fields.named.iter() {
            parse_field_attributes(field, &field.attrs, context);
            if let Some(ident) = &field.ident {
                if ident != context.id_field.as_ref().unwrap()
                    && !context.relationship_fields.contains(ident)
                {
                    context.fields.push(ident.clone());
                }
            };
        }
    };
}

fn parse_name(ast: &DeriveInput, context: &mut ResourceContext) {
    context.resource_name = Some(Ident::new(
        &ast.ident.to_string().as_str(),
        ast.ident.span(),
    ));
}

fn parse_attrs(ast: &DeriveInput, context: &mut ResourceContext) {
    ast.attrs.iter().for_each(|attr| {
        if let Ok(metadata) = attr.parse_meta() {
            if let List(meta_list) = metadata {
                for nested_metadata in meta_list.nested.iter() {
                    if let Meta(meta) = nested_metadata {
                        if let NameValue(name_value) = meta {
                            if name_value.path == URL {
                                if let Str(string) = &name_value.lit {
                                    context.url = Some(string.value());
                                }
                            }
                        }
                    }
                }
            }
        }
    })
}