bluejay-typegen-codegen 0.4.0

Code generation utilities for `bluejay-typegen-macro`
Documentation
use crate::{
    input::parse_key_value_with, map_parser_errors, validation, CodeGenerator, Config,
    DocumentInput,
};
use bluejay_core::definition::SchemaDefinition;
use bluejay_parser::ast::{executable::ExecutableDocument, Parse as _};
use bluejay_validator::executable::{
    document::{BuiltinRulesValidator, Orchestrator},
    Cache,
};
use itertools::{Either, Itertools};
use syn::{parse::Parse, parse2, spanned::Spanned};

mod executable_enum_builder;
mod executable_enum_variant_builder;
mod executable_struct_builder;
mod executable_type_builder;
mod intermediate_representation;

use executable_enum_builder::ExecutableEnumBuilder;
use executable_enum_variant_builder::ExecutableEnumVariantBuilder;
use executable_struct_builder::ExecutableStructBuilder;
use executable_type_builder::ExecutableTypeBuilder;
pub use intermediate_representation::{
    ExecutableEnum, ExecutableField, ExecutableStruct, ExecutableType, WrappedExecutableType,
};

mod kw {
    syn::custom_keyword!(custom_scalar_overrides);
}

pub(crate) struct CustomScalarOverride {
    graphql_path_token: syn::LitStr,
    graphql_path: Vec<String>,
    type_token: syn::Type,
    borrows: bool,
}

impl Parse for CustomScalarOverride {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let graphql_path_token = input.parse()?;
        let graphql_path = Self::graphql_path(&graphql_path_token);
        input.parse::<syn::Token![=>]>()?;
        let type_token = input.parse()?;

        let borrows = Self::type_borrows(&type_token)?;

        Ok(Self {
            graphql_path_token,
            graphql_path,
            type_token,
            borrows,
        })
    }
}

impl CustomScalarOverride {
    fn graphql_path(lit_str: &syn::LitStr) -> Vec<String> {
        lit_str.value().split('.').map(|s| s.to_string()).collect()
    }

    fn type_borrows(ty: &syn::Type) -> syn::Result<bool> {
        let path = match ty {
            syn::Type::Path(path) => path,
            // allow the `()` type
            syn::Type::Tuple(tuple) if tuple.elems.is_empty() => return Ok(false),
            _ => {
                return Err(syn::Error::new(
                    ty.span(),
                    "Unsupported type for custom scalar overrides",
                ));
            }
        };

        let Some(last_segment) = path.path.segments.last() else {
            return Err(syn::Error::new(
                path.span(),
                "Path must have at least one segment",
            ));
        };

        let path_arguments = match &last_segment.arguments {
            syn::PathArguments::None => return Ok(false),
            syn::PathArguments::AngleBracketed(bracketed) => bracketed,
            syn::PathArguments::Parenthesized(parenthesized) => {
                return Err(syn::Error::new(
                    parenthesized.span(),
                    "Paths for custom scalar overrides must not contain parenthesized generic arguments",
                ));
            }
        };

        if path_arguments.args.len() != 1
            || !matches!(
                path_arguments.args.first(),
                Some(syn::GenericArgument::Lifetime(lifetime)) if lifetime.ident != "'a"
            )
        {
            return Err(syn::Error::new(
                ty.span(),
                "Paths for custom scalar overrides with generic arguments must contain a single lifetime parameter 'a",
            ));
        }

        Ok(true)
    }

    fn r#type(&self) -> &syn::Type {
        &self.type_token
    }
}

struct Input {
    query: DocumentInput,
    custom_scalar_overrides:
        Option<syn::punctuated::Punctuated<CustomScalarOverride, syn::Token![,]>>,
}

impl Parse for Input {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let query = input.parse()?;

        let mut custom_scalar_overrides = None;

        while !input.is_empty() {
            input.parse::<syn::Token![,]>()?;
            let lookahead = input.lookahead1();
            if lookahead.peek(kw::custom_scalar_overrides) {
                parse_key_value_with(input, &mut custom_scalar_overrides, |input| {
                    let content;
                    syn::braced!(content in input);
                    syn::punctuated::Punctuated::parse_terminated(&content)
                })?;
            } else {
                return Err(lookahead.error());
            }
        }

        Ok(Self {
            query,
            custom_scalar_overrides,
        })
    }
}

pub(crate) fn generate_executable_definition<S: SchemaDefinition, C: CodeGenerator>(
    config: &Config<S, C>,
    configuration: proc_macro2::TokenStream,
) -> syn::Result<Vec<syn::Item>> {
    let Input {
        query,
        custom_scalar_overrides,
    } = parse2(configuration)?;

    let (contents, path) = query.read_to_string_and_path()?;

    let executable_document = ExecutableDocument::parse(&contents)
        .result
        .map_err(|errors| map_parser_errors(&query, &contents, path.as_deref(), errors))?;
    let validation_cache = Cache::new(&executable_document, config.schema_definition());
    let validation_errors: Vec<_> = BuiltinRulesValidator::validate(
        &executable_document,
        config.schema_definition(),
        &validation_cache,
    )
    .collect();
    if !validation_errors.is_empty() {
        return Err(map_parser_errors(
            &query,
            &contents,
            path.as_deref(),
            validation_errors,
        ));
    }
    let (validation_errors, paths_with_custom_scalar_type) = Orchestrator::<
        _,
        _,
        (
            validation::Rule<_, _>,
            validation::PathsWithCustomScalarType<_>,
        ),
    >::validate_and_analyze(
        &executable_document,
        config.schema_definition(),
        &validation_cache,
    );

    let validation_errors: Vec<_> = validation_errors.collect();

    if !validation_errors.is_empty() {
        return Err(map_parser_errors(
            &query,
            &contents,
            path.as_deref(),
            validation_errors,
        ));
    }

    let custom_scalar_overrides: Vec<CustomScalarOverride> = custom_scalar_overrides
        .map(|c| c.into_iter().collect())
        .unwrap_or_default();

    let (valid_custom_scalar_overrides, custom_scalar_override_errors): (Vec<_>, Vec<syn::Error>) =
        custom_scalar_overrides
            .into_iter()
            .partition_map(|c| {
                if paths_with_custom_scalar_type.contains(&c.graphql_path) {
                    if c.borrows && !config.borrow() {
                        Either::Right(syn::Error::new(
                            c.type_token.span(),
                            "Custom scalar overrides must not borrow if the `borrow` option is not enabled",
                        ))
                    } else {
                        Either::Left(c)
                    }
                } else {
                    Either::Right(syn::Error::new(
                        c.graphql_path_token.span(),
                        "Custom scalar overrides must correspond to a path in the query that is a custom scalar type",
                    ))
                }
            });

    if let Some(combined_error) =
        custom_scalar_override_errors
            .into_iter()
            .reduce(|mut acc, error| {
                acc.combine(error);
                acc
            })
    {
        return Err(combined_error);
    }

    let executable_types = ExecutableType::for_executable_document(
        &executable_document,
        config,
        valid_custom_scalar_overrides,
    );

    Ok(executable_types
        .iter()
        .flat_map(|et| ExecutableTypeBuilder::build(et, config.code_generator()))
        .collect())
}