bluejay_typegen_macro/
lib.rs

1use bluejay_core::definition::{EnumTypeDefinition, EnumValueDefinition};
2use bluejay_typegen_codegen::{
3    generate_schema, names::field_ident, CodeGenerator, ExecutableEnum, ExecutableField,
4    ExecutableStruct, Input,
5};
6use proc_macro2::Span;
7use quote::ToTokens;
8use syn::{parse_macro_input, parse_quote};
9
10/// Generates Rust types from GraphQL schema definitions and queries.
11///
12/// ### Arguments
13///
14/// **Positional:**
15///
16/// 1. String literal with path to the file containing the schema definition. If relative, should be with respect to
17///    the project root (wherever `Cargo.toml` is located). Alternatively, include the schema contents enclosed in square
18///    brackets.
19///
20/// **Optional keyword:**
21///
22/// _borrow_: Boolean literal indicating whether the generated types should borrow where possible. Defaults to `false`.
23/// When `true`, deserializing must be done from a string as a opposed to `serde_json::Value` or a reader.
24///
25/// ### Trait implementations
26///
27/// By default, will implement `PartialEq`, `Eq`, `Clone`, and `Debug` for all types. Will implement `Copy` for enums.
28/// For types corresponding to values returned from queries, `serde::Deserialize` is implemented. For types that would
29/// be arguments to a query, `serde::Serialize` is implemented.
30///
31/// ### Usage
32///
33/// Must be used with a module. Inside the module, type aliases must be defined for any custom scalars in the schema.
34///
35/// #### Queries
36///
37/// To use a query, define a module within the aforementioned module, and annotate it with
38/// `#[query("path/to/query.graphql")]`, where the argument is a string literal path to the query document, or the
39/// query contents enclosed in square brackets.
40///
41/// ##### Custom scalar overrides
42///
43/// To override the type of a custom scalar for a path within a query, use the `custom_scalar_overrides` named argument
44/// inside of the `#[query(...)]` attribute. The argument is a map from a path to a type, where the path is a string literal
45/// path to the field in the query, and the type is the type to override the field with.
46///
47/// For example, with the following query:
48/// ```graphql
49/// query MyQuery {
50///     myField: myScalar!
51/// }
52/// ```
53/// do something like the following:
54/// ```ignore
55/// #[query("path/to/query.graphql", custom_scalar_overrides = {
56///     "MyQuery.myField" => ::std::primitive::i32,
57/// })]
58/// ```
59/// Any type path that does not start with `::` is assumed to be relative to the schema definition module.
60/// Types may have a single lifetime parameter, which must be named `a`. Generic types are not supported.
61/// If you need to use a generic type, use an alias for the type to remove the generic parameters.
62/// Arrays and tuples are not supported.
63///
64/// ### Naming
65///
66/// To generate idiomatic Rust code, some renaming of types, enum variants, and fields is performed. Types are
67/// renamed with `PascalCase`, as are enum variants. Fields are renamed with `snake_case`.
68///
69/// ### Query restrictions
70///
71/// In order to keep the type generation code relatively simple, there are some restrictions on the queries that are
72/// permitted. This may be relaxed in future versions.
73/// * Selection sets on object and interface types must contain either a single fragment spread, or entirely field
74///   selections.
75/// * Selection sets on union types must contain either a single fragment spread, or both an unaliased `__typename`
76///   selection and inline fragments for all or a subset of the objects contained in the union.
77///
78/// ### Example
79/// See top-level documentation of `bluejay-typegen` for an example.
80#[proc_macro_attribute]
81pub fn typegen(
82    attr: proc_macro::TokenStream,
83    item: proc_macro::TokenStream,
84) -> proc_macro::TokenStream {
85    let input = parse_macro_input!(attr as Input);
86    let mut module = parse_macro_input!(item as syn::ItemMod);
87
88    if let Err(error) = generate_schema(input, &mut module, Default::default(), SerdeCodeGenerator)
89    {
90        return error.to_compile_error().into();
91    }
92
93    module.to_token_stream().into()
94}
95
96struct SerdeCodeGenerator;
97
98impl CodeGenerator for SerdeCodeGenerator {
99    fn attributes_for_executable_struct(
100        &self,
101        _executable_struct: &ExecutableStruct,
102    ) -> Vec<syn::Attribute> {
103        vec![
104            parse_quote! { #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::fmt::Debug, ::bluejay_typegen::serde::Deserialize)] },
105            parse_quote! { #[serde(crate = "bluejay_typegen::serde")] },
106        ]
107    }
108
109    fn fields_for_executable_struct(&self, executable_struct: &ExecutableStruct) -> syn::Fields {
110        let fields: Vec<syn::Field> = executable_struct
111            .fields()
112            .iter()
113            .map(|executable_field| {
114                let name_ident = field_ident(executable_field.graphql_name());
115
116                let attributes = self.attributes_for_field(executable_field);
117                let ty = executable_struct.compute_type(executable_field.r#type());
118
119                parse_quote! {
120                    #(#attributes)*
121                    pub #name_ident: #ty
122                }
123            })
124            .collect();
125
126        let fields_named: syn::FieldsNamed = parse_quote! { { #(#fields,)* } };
127
128        fields_named.into()
129    }
130
131    fn attributes_for_executable_enum(
132        &self,
133        _executable_enum: &ExecutableEnum,
134    ) -> Vec<syn::Attribute> {
135        vec![
136            parse_quote! { #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::fmt::Debug, ::bluejay_typegen::serde::Deserialize)] },
137            parse_quote! { #[serde(crate = "bluejay_typegen::serde")] },
138            parse_quote! { #[serde(tag = "__typename")] },
139        ]
140    }
141
142    fn attributes_for_executable_enum_variant(
143        &self,
144        executable_struct: &ExecutableStruct,
145    ) -> Vec<syn::Attribute> {
146        let mut attributes = Vec::new();
147
148        let serialized_as = syn::LitStr::new(executable_struct.parent_name(), Span::call_site());
149        attributes.push(parse_quote! { #[serde(rename = #serialized_as)] });
150
151        if executable_struct.borrows() {
152            attributes.push(parse_quote! { #[serde(borrow)] });
153        }
154
155        attributes
156    }
157
158    fn attributes_for_executable_enum_variant_other(&self) -> Vec<syn::Attribute> {
159        vec![parse_quote! { #[serde(other)] }]
160    }
161
162    fn attributes_for_enum(
163        &self,
164        _enum_type_definition: &impl EnumTypeDefinition,
165    ) -> Vec<syn::Attribute> {
166        vec![
167            parse_quote! { #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::fmt::Debug, ::bluejay_typegen::serde::Serialize, ::bluejay_typegen::serde::Deserialize)] },
168            parse_quote! { #[serde(crate = "bluejay_typegen::serde")] },
169        ]
170    }
171
172    fn attributes_for_enum_variant(
173        &self,
174        enum_value_definition: &impl EnumValueDefinition,
175    ) -> Vec<syn::Attribute> {
176        let serialized_as = syn::LitStr::new(enum_value_definition.name(), Span::call_site());
177        vec![parse_quote! { #[serde(rename = #serialized_as)] }]
178    }
179
180    fn attributes_for_enum_variant_other(&self) -> Vec<syn::Attribute> {
181        vec![parse_quote! { #[serde(other)] }]
182    }
183
184    fn attributes_for_input_object(
185        &self,
186        #[allow(unused_variables)]
187        input_object_type_definition: &impl bluejay_core::definition::InputObjectTypeDefinition,
188    ) -> Vec<syn::Attribute> {
189        vec![
190            parse_quote! { #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::fmt::Debug, ::bluejay_typegen::serde::Serialize)] },
191            parse_quote! { #[serde(crate = "bluejay_typegen::serde")] },
192        ]
193    }
194
195    fn attributes_for_input_object_field(
196        &self,
197        input_value_definition: &impl bluejay_core::definition::InputValueDefinition,
198        borrows: bool,
199    ) -> Vec<syn::Attribute> {
200        let serialized_as = syn::LitStr::new(input_value_definition.name(), Span::call_site());
201        let mut attributes = vec![parse_quote! { #[serde(rename = #serialized_as)] }];
202
203        if borrows {
204            attributes.push(parse_quote! { #[serde(borrow)] });
205        }
206
207        attributes
208    }
209
210    fn attributes_for_one_of_input_object(
211        &self,
212        #[allow(unused_variables)]
213        input_object_type_definition: &impl bluejay_core::definition::InputObjectTypeDefinition,
214    ) -> Vec<syn::Attribute> {
215        // the attributes are the same as for a normal input object
216        self.attributes_for_input_object(input_object_type_definition)
217    }
218
219    fn attributes_for_one_of_input_object_field(
220        &self,
221        input_value_definition: &impl bluejay_core::definition::InputValueDefinition,
222        borrows: bool,
223    ) -> Vec<syn::Attribute> {
224        // the attributes are the same as for a normal input object field
225        self.attributes_for_input_object_field(input_value_definition, borrows)
226    }
227}
228
229impl SerdeCodeGenerator {
230    fn attributes_for_field(&self, executable_field: &ExecutableField) -> Vec<syn::Attribute> {
231        let mut attributes = Vec::new();
232
233        let serialized_as = syn::LitStr::new(executable_field.graphql_name(), Span::call_site());
234        attributes.push(parse_quote! { #[serde(rename = #serialized_as)] });
235
236        if executable_field.r#type().base().borrows() {
237            attributes.push(parse_quote! { #[serde(borrow)] });
238        }
239
240        attributes
241    }
242}