use bluejay_core::{
definition::{prelude::*, SchemaDefinition, TypeDefinitionReference},
BuiltinScalarDefinition,
};
use bluejay_parser::{
ast::{
definition::{DefinitionDocument, SchemaDefinition as ParserSchemaDefinition},
Parse,
},
Error as ParserError,
};
use bluejay_validator::definition::BuiltinRulesValidator;
use std::collections::{HashMap, HashSet};
use syn::{parse_quote, spanned::Spanned};
mod attributes;
mod builtin_scalar;
mod code_generator;
mod enum_type_definition;
mod executable_definition;
mod input;
mod input_object_type_definition;
pub mod names;
mod types;
mod validation;
use attributes::doc_string;
pub use code_generator::CodeGenerator;
use enum_type_definition::EnumTypeDefinitionBuilder;
use executable_definition::generate_executable_definition;
pub use executable_definition::{
ExecutableEnum, ExecutableField, ExecutableStruct, ExecutableType, WrappedExecutableType,
};
use input::DocumentInput;
pub use input::Input;
use input_object_type_definition::InputObjectTypeDefinitionBuilder;
pub(crate) struct Config<'a, S: SchemaDefinition, C: CodeGenerator> {
borrow: bool,
schema_definition: &'a S,
custom_scalar_borrows: HashMap<String, bool>,
enums_as_str: HashSet<String>,
code_generator: &'a C,
}
impl<'a, S: SchemaDefinition, C: CodeGenerator> Config<'a, S, C> {
pub(crate) fn schema_definition(&self) -> &'a S {
self.schema_definition
}
pub(crate) fn borrow(&self) -> bool {
self.borrow
}
pub(crate) fn custom_scalar_borrows(&self, cstd: &S::CustomScalarTypeDefinition) -> bool {
*self
.custom_scalar_borrows
.get(&names::type_name(cstd.name()))
.expect("No type alias for custom scalar")
}
pub(crate) fn builtin_scalar_borrows(&self, bstd: BuiltinScalarDefinition) -> bool {
self.borrow && builtin_scalar::scalar_is_reference(bstd)
}
pub(crate) fn enum_as_str(&self, etd: &S::EnumTypeDefinition) -> bool {
self.enums_as_str.contains(etd.name())
}
pub(crate) fn code_generator(&self) -> &C {
self.code_generator
}
}
pub fn generate_schema(
input: Input,
module: &mut syn::ItemMod,
known_custom_scalar_types: HashMap<String, KnownCustomScalarType>,
code_generator: impl CodeGenerator,
) -> syn::Result<()> {
let Input {
ref schema,
borrow,
enums_as_str,
} = input;
let borrow = borrow.is_some_and(|lit| lit.value());
let (schema_contents, schema_path) = schema.read_to_string_and_path()?;
let definition_document: DefinitionDocument = DefinitionDocument::parse(&schema_contents)
.result
.map_err(|errors| {
map_parser_errors(schema, &schema_contents, schema_path.as_deref(), errors)
})?;
let schema_definition =
ParserSchemaDefinition::try_from(&definition_document).map_err(|errors| {
map_parser_errors(schema, &schema_contents, schema_path.as_deref(), errors)
})?;
let schema_errors: Vec<_> = BuiltinRulesValidator::validate(&schema_definition).collect();
if !schema_errors.is_empty() {
return Err(map_parser_errors(
schema,
&schema_contents,
schema_path.as_deref(),
schema_errors,
));
}
let custom_scalar_borrows = custom_scalar_borrows(
module,
&schema_definition,
borrow,
known_custom_scalar_types,
)?;
let enums_as_str = validate_enums_as_str(enums_as_str, &schema_definition)?;
let config = Config {
schema_definition: &schema_definition,
borrow,
custom_scalar_borrows,
enums_as_str,
code_generator: &code_generator,
};
if let Some((_, items)) = module.content.take() {
let new_items = process_module_items(&config, items)?;
module.content = Some((syn::token::Brace::default(), new_items));
} else {
let new_items = process_module_items(&config, Vec::new())?;
module.content = Some((syn::token::Brace::default(), new_items));
}
if let Some(description) = schema_definition.description() {
module.attrs.push(doc_string(description));
}
Ok(())
}
fn custom_scalar_borrows(
module: &mut syn::ItemMod,
schema_definition: &impl SchemaDefinition,
borrow: bool,
known_custom_scalar_types: HashMap<String, KnownCustomScalarType>,
) -> syn::Result<HashMap<String, bool>> {
let items = module
.content
.as_ref()
.map(|(_, items)| items.as_slice())
.unwrap_or_default();
let type_aliases = items
.iter()
.filter_map(|item| match item {
syn::Item::Type(ty) => Some(ty),
_ => None,
})
.collect::<Vec<_>>();
type_aliases.iter().try_for_each(|type_alias| {
let generics = &type_alias.generics;
if let Some(type_param) = generics.type_params().next() {
return Err(syn::Error::new(
type_param.span(),
"Type aliases for custom scalars must not contain type parameters",
));
}
if let Some(const_param) = generics.const_params().next() {
return Err(syn::Error::new(
const_param.span(),
"Type aliases for custom scalars must not contain const parameters",
));
}
if !borrow {
if let Some(lifetime_param) = generics.lifetimes().next() {
return Err(syn::Error::new(
lifetime_param.span(),
"Type aliases for custom scalars cannot contain lifetime parameters when `borrow` is set to true",
));
}
} else if let Some(lifetime_param) = generics.lifetimes().nth(1) {
return Err(syn::Error::new(
lifetime_param.span(),
"Type aliases for custom scalars must contain at most one lifetime parameter",
));
}
let name = type_alias.ident.to_string();
if !schema_definition.type_definitions().any(|type_definition| {
matches!(type_definition, TypeDefinitionReference::CustomScalar(cstd) if names::type_name(cstd.name()) == name)
}) {
return Err(syn::Error::new(
type_alias.ident.span(),
format!("No custom scalar definition named {name}"),
));
}
Ok(())
})?;
let mut custom_scalars: HashMap<String, bool> = type_aliases
.into_iter()
.map(|type_alias| {
(
type_alias.ident.to_string(),
type_alias.generics.lifetimes().next().is_some(),
)
})
.collect();
schema_definition
.type_definitions()
.try_for_each(|td| match td {
TypeDefinitionReference::CustomScalar(cstd) => {
let name = names::type_name(cstd.name());
#[allow(clippy::map_entry)]
if custom_scalars.contains_key(&name) {
Ok(())
} else if let Some(known_custom_scalar_type) = known_custom_scalar_types.get(&name)
{
let (ty, lifetime): (_, Option<syn::Generics>) =
match known_custom_scalar_type.type_for_borrowed.as_ref() {
Some(ty) if borrow => (ty, Some(parse_quote! { <'a> })),
_ => (&known_custom_scalar_type.type_for_owned, None),
};
let ident = quote::format_ident!("{}", name);
let alias: syn::ItemType = parse_quote! {
pub type #ident #lifetime = #ty;
};
if let Some((_, items)) = module.content.as_mut() {
items.push(syn::Item::Type(alias));
}
custom_scalars.insert(
name,
borrow && known_custom_scalar_type.type_for_borrowed.is_some(),
);
Ok(())
} else {
Err(syn::Error::new(
module.span(),
format!("Missing type alias for custom scalar {name}"),
))
}
}
_ => Ok(()),
})?;
Ok(custom_scalars)
}
fn validate_enums_as_str(
enums_as_str: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]>,
schema_definition: &impl SchemaDefinition,
) -> syn::Result<HashSet<String>> {
let mut enum_names = HashSet::new();
enums_as_str.iter().try_for_each(|lit| {
let name: String = lit.value();
if matches!(
schema_definition.get_type_definition(&name),
Some(TypeDefinitionReference::Enum(_))
) {
if enum_names.insert(name.clone()) {
Ok(())
} else {
Err(syn::Error::new(
lit.span(),
format!("Duplicate enum definition named {name}"),
))
}
} else {
Err(syn::Error::new(
lit.span(),
format!("No enum definition named {name}"),
))
}
})?;
Ok(enum_names)
}
fn process_module_items<S: SchemaDefinition, C: CodeGenerator>(
config: &Config<S, C>,
items: Vec<syn::Item>,
) -> syn::Result<Vec<syn::Item>> {
config
.schema_definition
.type_definitions()
.filter_map(|type_definition| match type_definition {
TypeDefinitionReference::Enum(etd) if !config.enum_as_str(etd) => Some(
EnumTypeDefinitionBuilder::<S, C>::build(etd, config.code_generator()),
),
TypeDefinitionReference::InputObject(iotd) => {
Some(InputObjectTypeDefinitionBuilder::build(iotd, config))
}
_ => None,
})
.flatten()
.map(Ok)
.chain(
items
.into_iter()
.map(|item| process_module_item(config, item)),
)
.collect()
}
fn process_module_item<S: SchemaDefinition, C: CodeGenerator>(
config: &Config<S, C>,
item: syn::Item,
) -> syn::Result<syn::Item> {
if let syn::Item::Mod(mut module) = item {
if let Some((attribute, &mut [])) = module.attrs.split_first_mut() {
if matches!(attribute.style, syn::AttrStyle::Inner(_)) {
Err(syn::Error::new(
attribute.span(),
"Expected an outer attribute",
))
} else if let syn::Meta::List(list) = &mut attribute.meta {
if list.path.is_ident("query") {
if !matches!(list.delimiter, syn::MacroDelimiter::Bracket(_)) {
let items = generate_executable_definition(
config,
std::mem::take(&mut list.tokens),
)?;
module.content = Some((syn::token::Brace::default(), items));
module.attrs = Vec::new();
Ok(syn::Item::Mod(module))
} else {
Err(syn::Error::new(
list.delimiter.span().open(),
"Expected brackets",
))
}
} else {
Err(syn::Error::new(list.path.span(), "Expected `query`"))
}
} else {
Err(syn::Error::new(
attribute.meta.span(),
"Expected a list meta attribute, e.g. `#[query(...)]`",
))
}
} else {
Err(syn::Error::new(
module.span(),
"Expected a single `#[query(...)]` attribute",
))
}
} else if matches!(item, syn::Item::Type(_)) {
Ok(item)
} else {
Err(syn::Error::new(item.span(), "Expected a module"))
}
}
fn map_parser_errors<E: Into<ParserError>>(
span: &impl syn::spanned::Spanned,
schema_contents: &str,
schema_path: Option<&str>,
errors: impl IntoIterator<Item = E>,
) -> syn::Error {
syn::Error::new(
span.span(),
ParserError::format_errors(schema_contents, schema_path, errors),
)
}
#[derive(Clone)]
pub struct KnownCustomScalarType {
pub type_for_owned: syn::Type,
pub type_for_borrowed: Option<syn::Type>,
}