juniper-compose-macros 0.0.3

Macros for juniper-compose crate
Documentation
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::missing_panics_doc)]

use heck::ToLowerCamelCase;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
    parenthesized,
    parse::Parse,
    parse2, parse_macro_input,
    punctuated::Punctuated,
    token::{Comma, Paren},
    Error, Ident, ImplItem, ItemImpl, LitStr, Path, Result, Token, Type, Visibility,
};

#[proc_macro_attribute]
pub fn composable_object(
    _: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let item_impl = parse_macro_input!(item as ItemImpl);
    expand_composable_object(&item_impl).into()
}

#[proc_macro]
pub fn composite_object(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as CompositeObjectInput);
    let context = input
        .context_ty
        .map_or_else(|| parse2(quote! { () }).unwrap(), |input| input.ty);
    expand_composite_object(&input.vis, &input.ident, &context, &input.composables).into()
}

struct CompositeObjectInput {
    vis: Visibility,
    ident: Ident,
    context_ty: Option<CompositeObjectCustomContextType>,
    #[allow(dead_code)]
    paren: Paren,
    composables: Punctuated<Path, Comma>,
}

impl Parse for CompositeObjectInput {
    fn parse(input: syn::parse::ParseStream) -> Result<Self> {
        let vis = input.parse()?;
        let ident = input.parse()?;
        let context_ty = if input.peek(Token![<]) {
            Some(input.parse()?)
        } else {
            None
        };
        let composables;
        let paren = parenthesized!(composables in input);
        Ok(Self {
            vis,
            ident,
            context_ty,
            paren,
            composables: composables.parse_terminated(Path::parse)?,
        })
    }
}

struct CompositeObjectCustomContextType {
    #[allow(dead_code)]
    left_angle_bracket: Token![<],
    #[allow(dead_code)]
    context_ident: Ident,
    #[allow(dead_code)]
    eq_token: Token![=],
    ty: Type,
    #[allow(dead_code)]
    right_angle_bracket: Token![>],
}

impl Parse for CompositeObjectCustomContextType {
    fn parse(input: syn::parse::ParseStream) -> Result<Self> {
        let left_angle_bracket = input.parse()?;
        let context_ident = input.parse::<Ident>()?;
        if context_ident != "Context" {
            return Err(Error::new(context_ident.span(), "expected `Context`"));
        }
        let eq_token = input.parse()?;
        let ty = input.parse()?;
        let right_angle_bracket = input.parse()?;
        Ok(Self {
            left_angle_bracket,
            context_ident,
            eq_token,
            ty,
            right_angle_bracket,
        })
    }
}

fn expand_composable_object(item_impl: &ItemImpl) -> TokenStream {
    let ty = &item_impl.self_ty;

    let fields = item_impl
        .items
        .iter()
        .filter_map(|item| {
            if let ImplItem::Method(method) = item {
                Some(method)
            } else {
                None
            }
        })
        .map(|method| {
            LitStr::new(
                &method.sig.ident.to_string().to_lower_camel_case(),
                Span::call_site(),
            )
        });

    quote! {
        impl ::juniper_compose::ComposableObject for #ty {
            fn fields() -> &'static [&'static str] {
                &[#( #fields ),*]
            }
        }

        #item_impl
    }
}

fn expand_composite_object<P>(
    vis: &Visibility,
    name: &Ident,
    context: &Type,
    composables: &Punctuated<Path, P>,
) -> TokenStream {
    let name_lit = LitStr::new(&name.to_string(), Span::call_site());
    let impl_graphql_type = expand_impl_graphql_type(name, &name_lit, composables.iter());
    let impl_graphql_value =
        expand_impl_graphql_value(name, &name_lit, context, composables.iter());
    let impl_graphql_value_async =
        expand_impl_graphql_value_async(name, &name_lit, composables.iter());
    quote! {
        #[derive(::std::default::Default)]
        #vis struct #name;
        #impl_graphql_type
        #impl_graphql_value
        #impl_graphql_value_async
    }
}

fn expand_impl_graphql_type<'a>(
    name: &Ident,
    name_lit: &LitStr,
    composables: impl IntoIterator<Item = &'a Path>,
) -> TokenStream {
    let composables = composables.into_iter();
    quote! {
        impl ::juniper::GraphQLType for #name {
            fn name(info: &Self::TypeInfo) -> ::std::option::Option<&str> {
                ::std::option::Option::Some(#name_lit)
            }

            fn meta<'r>(
                info: &Self::TypeInfo,
                registry: &mut ::juniper::executor::Registry<'r, ::juniper::DefaultScalarValue>
            ) -> ::juniper::meta::MetaType<'r, ::juniper::DefaultScalarValue>
            where
                ::juniper::DefaultScalarValue: 'r
            {
                let mut fields = ::std::vec![];
                let mut seen_field_names = ::std::collections::HashSet::<&str>::new();

                #(
                    let composable_meta = <#composables as ::juniper::GraphQLType>::meta(info, registry);

                    for field_name in <#composables as ::juniper_compose::ComposableObject>::fields() {
                        if !seen_field_names.insert(field_name) {
                            ::std::panic!("Conflicting field in composed objects: {}", field_name);
                        }

                        let composable_field = composable_meta
                            .field_by_name(field_name)
                            .unwrap_or_else(|| {
                                ::std::panic!(
                                    "Incorrect implementation of ComposableObject on type {}: unknown field {}",
                                    <#composables as ::juniper::GraphQLType>::name(&()).unwrap_or("<anonymous>"), field_name
                                )
                            });

                        fields.push(::juniper::meta::Field {
                            name: composable_field.name.clone(),
                            description: composable_field.description.clone(),
                            arguments: composable_field.arguments.as_ref().map(|arguments| {
                                arguments
                                    .iter()
                                    .map(|argument| ::juniper::meta::Argument {
                                        name: argument.name.clone(),
                                        description: argument.description.clone(),
                                        arg_type: ::juniper_compose::type_to_owned(&argument.arg_type),
                                        default_value: argument.default_value.clone(),
                                    })
                                    .collect()
                            }),
                            field_type: ::juniper_compose::type_to_owned(&composable_field.field_type),
                            deprecation_status: composable_field.deprecation_status.clone(),
                        });
                    }
                )*

                registry.build_object_type::<Self>(&(), &fields).into_meta()
            }
        }
    }
}

fn expand_impl_graphql_value<'a>(
    name: &Ident,
    name_lit: &LitStr,
    context: &Type,
    composables: impl IntoIterator<Item = &'a Path>,
) -> TokenStream {
    let composables = composables.into_iter();
    quote! {
        impl ::juniper::GraphQLValue for #name {
            type Context = #context;
            type TypeInfo = ();

            fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> {
                <Self as ::juniper::GraphQLType>::name(info)
            }

            fn resolve_field(
                &self,
                info: &Self::TypeInfo,
                field_name: &str,
                arguments: &::juniper::Arguments<'_, ::juniper::DefaultScalarValue>,
                executor: &::juniper::executor::Executor<'_, '_, Self::Context, ::juniper::DefaultScalarValue>
            ) -> ::juniper::executor::ExecutionResult<::juniper::DefaultScalarValue> {
                #(
                    if <#composables as ::juniper_compose::ComposableObject>::fields().contains(&field_name) {
                        return <#composables as ::juniper::GraphQLValue>::resolve_field(
                            &<#composables as ::std::default::Default>::default(),
                            info,
                            field_name,
                            arguments,
                            executor
                        );
                    }
                )*
                Err(::juniper::FieldError::from(::std::format!(
                    "Field `{}` not found on type `{}`",
                    field_name,
                    #name_lit,
                )))
            }

            fn concrete_type_name(
                &self,
                context: &Self::Context,
                info: &Self::TypeInfo
            ) -> String {
                String::from(#name_lit)
            }
        }
    }
}

fn expand_impl_graphql_value_async<'a>(
    name: &Ident,
    name_lit: &LitStr,
    composables: impl IntoIterator<Item = &'a Path>,
) -> TokenStream {
    let composables = composables.into_iter();
    quote! {
        impl ::juniper::GraphQLValueAsync for #name
        where
            Self::TypeInfo: Sync,
            Self::Context: Sync,
        {
            fn resolve_field_async<'a>(
                &'a self,
                info: &'a Self::TypeInfo,
                field_name: &'a str,
                arguments: &'a ::juniper::Arguments<'_, ::juniper::DefaultScalarValue>,
                executor: &'a ::juniper::executor::Executor<'_, '_, Self::Context, ::juniper::DefaultScalarValue>
            ) -> ::juniper::BoxFuture<'a, ::juniper::executor::ExecutionResult<::juniper::DefaultScalarValue>> {
                #(
                    if <#composables as ::juniper_compose::ComposableObject>::fields().contains(&field_name) {
                        return ::std::boxed::Box::pin(async move {
                            <#composables as ::juniper::GraphQLValueAsync>::resolve_field_async(
                                &<#composables as ::std::default::Default>::default(),
                                info,
                                field_name,
                                arguments,
                                executor
                            ).await
                        })
                    }
                )*
                ::std::boxed::Box::pin(async move { Err(::juniper::FieldError::from(::std::format!(
                    "Field `{}` not found on type `{}`",
                    field_name,
                    #name_lit,
                ))) })
            }
        }
    }
}