Skip to main content

bluejay_typegen_codegen/
lib.rs

1use bluejay_core::{
2    definition::{prelude::*, SchemaDefinition, TypeDefinitionReference},
3    BuiltinScalarDefinition,
4};
5use bluejay_parser::{
6    ast::{
7        definition::{DefinitionDocument, SchemaDefinition as ParserSchemaDefinition},
8        Parse,
9    },
10    Error as ParserError,
11};
12use bluejay_validator::definition::BuiltinRulesValidator;
13use std::collections::{HashMap, HashSet};
14use syn::{parse_quote, spanned::Spanned};
15
16mod attributes;
17mod builtin_scalar;
18mod code_generator;
19mod enum_type_definition;
20mod executable_definition;
21mod input;
22mod input_object_type_definition;
23pub mod names;
24mod types;
25mod validation;
26
27use attributes::doc_string;
28pub use code_generator::CodeGenerator;
29use enum_type_definition::EnumTypeDefinitionBuilder;
30use executable_definition::generate_executable_definition;
31pub use executable_definition::{
32    ExecutableEnum, ExecutableField, ExecutableStruct, ExecutableType, WrappedExecutableType,
33};
34use input::DocumentInput;
35pub use input::Input;
36use input_object_type_definition::InputObjectTypeDefinitionBuilder;
37
38pub(crate) struct Config<'a, S: SchemaDefinition, C: CodeGenerator> {
39    borrow: bool,
40    schema_definition: &'a S,
41    custom_scalar_borrows: HashMap<String, bool>,
42    enums_as_str: HashSet<String>,
43    code_generator: &'a C,
44}
45
46impl<'a, S: SchemaDefinition, C: CodeGenerator> Config<'a, S, C> {
47    pub(crate) fn schema_definition(&self) -> &'a S {
48        self.schema_definition
49    }
50
51    pub(crate) fn borrow(&self) -> bool {
52        self.borrow
53    }
54
55    pub(crate) fn custom_scalar_borrows(&self, cstd: &S::CustomScalarTypeDefinition) -> bool {
56        *self
57            .custom_scalar_borrows
58            .get(&names::type_name(cstd.name()))
59            .expect("No type alias for custom scalar")
60    }
61
62    pub(crate) fn builtin_scalar_borrows(&self, bstd: BuiltinScalarDefinition) -> bool {
63        self.borrow && builtin_scalar::scalar_is_reference(bstd)
64    }
65
66    pub(crate) fn enum_as_str(&self, etd: &S::EnumTypeDefinition) -> bool {
67        self.enums_as_str.contains(etd.name())
68    }
69
70    pub(crate) fn code_generator(&self) -> &C {
71        self.code_generator
72    }
73}
74
75pub fn generate_schema(
76    input: Input,
77    module: &mut syn::ItemMod,
78    known_custom_scalar_types: HashMap<String, KnownCustomScalarType>,
79    code_generator: impl CodeGenerator,
80) -> syn::Result<()> {
81    let Input {
82        ref schema,
83        borrow,
84        enums_as_str,
85    } = input;
86
87    let borrow = borrow.is_some_and(|lit| lit.value());
88
89    let (schema_contents, schema_path) = schema.read_to_string_and_path()?;
90
91    let definition_document: DefinitionDocument = DefinitionDocument::parse(&schema_contents)
92        .result
93        .map_err(|errors| {
94            map_parser_errors(schema, &schema_contents, schema_path.as_deref(), errors)
95        })?;
96    let schema_definition =
97        ParserSchemaDefinition::try_from(&definition_document).map_err(|errors| {
98            map_parser_errors(schema, &schema_contents, schema_path.as_deref(), errors)
99        })?;
100    let schema_errors: Vec<_> = BuiltinRulesValidator::validate(&schema_definition).collect();
101    if !schema_errors.is_empty() {
102        return Err(map_parser_errors(
103            schema,
104            &schema_contents,
105            schema_path.as_deref(),
106            schema_errors,
107        ));
108    }
109
110    let custom_scalar_borrows = custom_scalar_borrows(
111        module,
112        &schema_definition,
113        borrow,
114        known_custom_scalar_types,
115    )?;
116
117    let enums_as_str = validate_enums_as_str(enums_as_str, &schema_definition)?;
118
119    let config = Config {
120        schema_definition: &schema_definition,
121        borrow,
122        custom_scalar_borrows,
123        enums_as_str,
124        code_generator: &code_generator,
125    };
126
127    if let Some((_, items)) = module.content.take() {
128        let new_items = process_module_items(&config, items)?;
129        module.content = Some((syn::token::Brace::default(), new_items));
130    } else {
131        let new_items = process_module_items(&config, Vec::new())?;
132        module.content = Some((syn::token::Brace::default(), new_items));
133    }
134
135    if let Some(description) = schema_definition.description() {
136        module.attrs.push(doc_string(description));
137    }
138
139    Ok(())
140}
141
142fn custom_scalar_borrows(
143    module: &mut syn::ItemMod,
144    schema_definition: &impl SchemaDefinition,
145    borrow: bool,
146    known_custom_scalar_types: HashMap<String, KnownCustomScalarType>,
147) -> syn::Result<HashMap<String, bool>> {
148    let items = module
149        .content
150        .as_ref()
151        .map(|(_, items)| items.as_slice())
152        .unwrap_or_default();
153
154    let type_aliases = items
155        .iter()
156        .filter_map(|item| match item {
157            syn::Item::Type(ty) => Some(ty),
158            _ => None,
159        })
160        .collect::<Vec<_>>();
161
162    type_aliases.iter().try_for_each(|type_alias| {
163        let generics = &type_alias.generics;
164
165        if let Some(type_param) = generics.type_params().next() {
166            return Err(syn::Error::new(
167                type_param.span(),
168                "Type aliases for custom scalars must not contain type parameters",
169            ));
170        }
171
172        if let Some(const_param) = generics.const_params().next() {
173            return Err(syn::Error::new(
174                const_param.span(),
175                "Type aliases for custom scalars must not contain const parameters",
176            ));
177        }
178
179        if !borrow {
180            if let Some(lifetime_param) = generics.lifetimes().next() {
181                return Err(syn::Error::new(
182                    lifetime_param.span(),
183                    "Type aliases for custom scalars cannot contain lifetime parameters when `borrow` is set to true",
184                ));
185            }
186        } else if let Some(lifetime_param) = generics.lifetimes().nth(1) {
187            return Err(syn::Error::new(
188                lifetime_param.span(),
189                "Type aliases for custom scalars must contain at most one lifetime parameter",
190            ));
191        }
192
193        let name = type_alias.ident.to_string();
194
195        if !schema_definition.type_definitions().any(|type_definition| {
196            matches!(type_definition, TypeDefinitionReference::CustomScalar(cstd) if names::type_name(cstd.name()) == name)
197        }) {
198            return Err(syn::Error::new(
199                type_alias.ident.span(),
200                format!("No custom scalar definition named {name}"),
201            ));
202        }
203
204        Ok(())
205    })?;
206
207    let mut custom_scalars: HashMap<String, bool> = type_aliases
208        .into_iter()
209        .map(|type_alias| {
210            (
211                type_alias.ident.to_string(),
212                type_alias.generics.lifetimes().next().is_some(),
213            )
214        })
215        .collect();
216
217    schema_definition
218        .type_definitions()
219        .try_for_each(|td| match td {
220            TypeDefinitionReference::CustomScalar(cstd) => {
221                let name = names::type_name(cstd.name());
222                #[allow(clippy::map_entry)]
223                if custom_scalars.contains_key(&name) {
224                    Ok(())
225                } else if let Some(known_custom_scalar_type) = known_custom_scalar_types.get(&name)
226                {
227                    let (ty, lifetime): (_, Option<syn::Generics>) =
228                        match known_custom_scalar_type.type_for_borrowed.as_ref() {
229                            Some(ty) if borrow => (ty, Some(parse_quote! { <'a> })),
230                            _ => (&known_custom_scalar_type.type_for_owned, None),
231                        };
232                    let ident = quote::format_ident!("{}", name);
233                    let alias: syn::ItemType = parse_quote! {
234                        pub type #ident #lifetime = #ty;
235                    };
236                    if let Some((_, items)) = module.content.as_mut() {
237                        items.push(syn::Item::Type(alias));
238                    }
239                    custom_scalars.insert(
240                        name,
241                        borrow && known_custom_scalar_type.type_for_borrowed.is_some(),
242                    );
243                    Ok(())
244                } else {
245                    Err(syn::Error::new(
246                        module.span(),
247                        format!("Missing type alias for custom scalar {name}"),
248                    ))
249                }
250            }
251            _ => Ok(()),
252        })?;
253
254    Ok(custom_scalars)
255}
256
257fn validate_enums_as_str(
258    enums_as_str: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]>,
259    schema_definition: &impl SchemaDefinition,
260) -> syn::Result<HashSet<String>> {
261    let mut enum_names = HashSet::new();
262    enums_as_str.iter().try_for_each(|lit| {
263        let name: String = lit.value();
264        if matches!(
265            schema_definition.get_type_definition(&name),
266            Some(TypeDefinitionReference::Enum(_))
267        ) {
268            if enum_names.insert(name.clone()) {
269                Ok(())
270            } else {
271                Err(syn::Error::new(
272                    lit.span(),
273                    format!("Duplicate enum definition named {name}"),
274                ))
275            }
276        } else {
277            Err(syn::Error::new(
278                lit.span(),
279                format!("No enum definition named {name}"),
280            ))
281        }
282    })?;
283    Ok(enum_names)
284}
285
286fn process_module_items<S: SchemaDefinition, C: CodeGenerator>(
287    config: &Config<S, C>,
288    items: Vec<syn::Item>,
289) -> syn::Result<Vec<syn::Item>> {
290    config
291        .schema_definition
292        .type_definitions()
293        .filter_map(|type_definition| match type_definition {
294            TypeDefinitionReference::Enum(etd) if !config.enum_as_str(etd) => Some(
295                EnumTypeDefinitionBuilder::<S, C>::build(etd, config.code_generator()),
296            ),
297            TypeDefinitionReference::InputObject(iotd) => {
298                Some(InputObjectTypeDefinitionBuilder::build(iotd, config))
299            }
300            _ => None,
301        })
302        .flatten()
303        .map(Ok)
304        .chain(
305            items
306                .into_iter()
307                .map(|item| process_module_item(config, item)),
308        )
309        .collect()
310}
311
312fn process_module_item<S: SchemaDefinition, C: CodeGenerator>(
313    config: &Config<S, C>,
314    item: syn::Item,
315) -> syn::Result<syn::Item> {
316    if let syn::Item::Mod(mut module) = item {
317        if let Some((attribute, &mut [])) = module.attrs.split_first_mut() {
318            if matches!(attribute.style, syn::AttrStyle::Inner(_)) {
319                Err(syn::Error::new(
320                    attribute.span(),
321                    "Expected an outer attribute",
322                ))
323            } else if let syn::Meta::List(list) = &mut attribute.meta {
324                if list.path.is_ident("query") {
325                    if !matches!(list.delimiter, syn::MacroDelimiter::Bracket(_)) {
326                        let items = generate_executable_definition(
327                            config,
328                            std::mem::take(&mut list.tokens),
329                        )?;
330                        module.content = Some((syn::token::Brace::default(), items));
331                        module.attrs = Vec::new();
332                        Ok(syn::Item::Mod(module))
333                    } else {
334                        Err(syn::Error::new(
335                            list.delimiter.span().open(),
336                            "Expected brackets",
337                        ))
338                    }
339                } else {
340                    Err(syn::Error::new(list.path.span(), "Expected `query`"))
341                }
342            } else {
343                Err(syn::Error::new(
344                    attribute.meta.span(),
345                    "Expected a list meta attribute, e.g. `#[query(...)]`",
346                ))
347            }
348        } else {
349            Err(syn::Error::new(
350                module.span(),
351                "Expected a single `#[query(...)]` attribute",
352            ))
353        }
354    } else if matches!(item, syn::Item::Type(_)) {
355        Ok(item)
356    } else {
357        Err(syn::Error::new(item.span(), "Expected a module"))
358    }
359}
360
361fn map_parser_errors<E: Into<ParserError>>(
362    span: &impl syn::spanned::Spanned,
363    schema_contents: &str,
364    schema_path: Option<&str>,
365    errors: impl IntoIterator<Item = E>,
366) -> syn::Error {
367    syn::Error::new(
368        span.span(),
369        ParserError::format_errors(schema_contents, schema_path, errors),
370    )
371}
372
373#[derive(Clone)]
374pub struct KnownCustomScalarType {
375    pub type_for_owned: syn::Type,
376    pub type_for_borrowed: Option<syn::Type>,
377}