async_graphql_relay_derive/
lib.rs

1use darling::FromDeriveInput;
2use proc_macro::TokenStream;
3use syn::{parse_macro_input, Data, DeriveInput};
4
5#[macro_use]
6extern crate quote;
7extern crate proc_macro;
8
9#[derive(FromDeriveInput, Default)]
10#[darling(default, attributes(relay))]
11struct RelayNodeObjectAttributes {
12    node_suffix: Option<String>,
13}
14
15/// The RelayNodeObject macro is applied to a type to automatically implement the RelayNodeStruct trait.
16/// ```
17/// #[derive(SimpleObject, RelayNodeObject)] // See the 'RelayNodeObject' derive macro
18/// #[graphql(complex)]
19/// #[relay(node_suffix = "u")] // This controls the 'RelayNodeObject' macro. In this case the prefix is shortened to 'u', the default is in the name of the struct.
20/// pub struct User {
21///     pub id: RelayNodeID<User>,
22///     pub name: String,
23///     pub role: String,
24/// }
25/// ```
26#[proc_macro_derive(RelayNodeObject, attributes(relay))]
27pub fn derive_relay_node_object(input: TokenStream) -> TokenStream {
28    let input = parse_macro_input!(input);
29    let attrs = RelayNodeObjectAttributes::from_derive_input(&input)
30        .expect("Error parsing 'RelayNodeObject' macro options!");
31    let DeriveInput { ident, data, .. } = input;
32
33    if !matches!(data, Data::Struct(_)) {
34        panic!("The 'RelayNodeObject' macro can only be used on structs!");
35    }
36
37    let value = if let Some(node_suffix) = attrs.node_suffix {
38        node_suffix
39    } else {
40        ident.to_string()
41    };
42
43    quote! {
44        impl async_graphql_relay::RelayNodeStruct for #ident {
45            const ID_SUFFIX: &'static str = #value;
46        }
47    }
48    .into()
49}
50
51/// The RelayInterface macro is applied to a GraphQL Interface enum to allow it to be used for Relay's node query.
52/// This enum should contain all types that that exist in your GraphQL schema to work as designed in the Relay server specification.
53/// ```
54/// #[derive(Interface, RelayInterface)] // See the 'RelayInterface' derive macro
55/// #[graphql(field(name = "id", type = "NodeGlobalID"))] // The 'RelayInterface' macro generates a type called '{enum_name}GlobalID' which should be used like this to facilitate using the async_graphql_relay::RelayNodeID for globally unique ID's
56/// pub enum Node {
57///     User(User),
58///     Tenant(Tenant),
59///    // Put all of your Object's in this enum
60/// }
61/// ```
62#[proc_macro_derive(RelayInterface)]
63pub fn derive_relay_interface(input: TokenStream) -> TokenStream {
64    let DeriveInput { ident, data, .. } = parse_macro_input!(input);
65
66    let ident = format_ident!("{}GlobalID", ident);
67    let impls;
68    let node_matchers;
69    if let Data::Enum(data) = &data {
70        impls = data.variants.iter().map(|variant| {
71            let variant_ident = &variant.ident;
72            quote! {
73                impl std::convert::From<&async_graphql_relay::RelayNodeID<#variant_ident>> for #ident {
74                    fn from(t: &async_graphql_relay::RelayNodeID<#variant_ident>) -> Self {
75                        #ident(String::from(t))
76                    }
77                }
78            }
79        });
80
81        node_matchers = data.variants.iter().map(|variant| {
82            let variant_ident = &variant.ident;
83            quote! {
84                <#variant_ident as async_graphql_relay::RelayNodeStruct>::ID_SUFFIX => {
85                    <#variant_ident as async_graphql_relay::RelayNode>::get(
86                        ctx,
87                        async_graphql_relay::RelayNodeID::<#variant_ident>::new_from_relay_id(
88                            relay_id.to_string(),
89                        )?,
90                    )
91                    .await?
92                    .ok_or_else(|| async_graphql::Error::new("A node with the specified id could not be found!"))
93                }
94            }
95        });
96    } else {
97        panic!("The 'RelayNodeObject' macro can only be used on enums!");
98    }
99
100    quote! {
101                #[derive(Clone, Debug)]
102        pub struct #ident(String);
103
104        #(#impls)*
105
106        #[async_graphql::Scalar(name = "RelayNodeID")]
107        impl async_graphql::ScalarType for #ident {
108            fn parse(value: async_graphql::Value) -> async_graphql::InputValueResult<Self> {
109                unimplemented!();
110            }
111
112            fn to_value(&self) -> async_graphql::Value {
113                async_graphql::Value::String(self.0.clone())
114            }
115        }
116
117        #[async_graphql_relay::_async_trait]
118        impl async_graphql_relay::RelayNodeInterface for Node {
119            async fn fetch_node(ctx: async_graphql_relay::RelayContext, relay_id: String) -> Result<Self, async_graphql::Error> {
120                if relay_id.len() < 32 {
121                    return Err(async_graphql::Error::new("Invalid id provided to node query!"));
122                }
123                let (_, suffix) = relay_id.split_at(32);
124                match suffix {
125                    #(#node_matchers)*
126                    _ => Err(async_graphql::Error::new("A node with the specified id could not be found!")),
127                }
128            }
129        }
130            }
131    .into()
132}
133
134// TODO: Unit tests