#![allow(dead_code, unused_variables, unused_mut)]
use std::{
collections::BTreeMap, convert::TryFrom, iter::successors, num::NonZeroUsize, sync::Arc,
};
use async_graphql_parser::{
types::{BaseType, ExecutableDocument, FieldDefinition, Type, TypeDefinition, TypeKind},
Positioned,
};
use async_graphql_value::Name;
use smallvec::SmallVec;
use crate::{
graphql_query::{
directives::{FilterDirective, FoldGroup, OperatorArgument, RecurseDirective},
query::{parse_document, FieldConnection, FieldNode, Query},
},
ir::{
indexed::IndexedQuery,
types::{intersect_types, is_argument_type_valid, NamedTypedValue},
Argument, ContextField, EdgeParameters, Eid, FieldRef, FieldValue, FoldSpecificField,
FoldSpecificFieldKind, IREdge, IRFold, IRQuery, IRQueryComponent, IRVertex, LocalField,
Operation, Recursive, TransformationKind, VariableRef, Vid, TYPENAME_META_FIELD,
TYPENAME_META_FIELD_ARC, TYPENAME_META_FIELD_NAME, TYPENAME_META_FIELD_TYPE,
},
schema::{FieldOrigin, Schema, BUILTIN_SCALARS},
util::{BTreeMapTryInsertExt, TryCollectUniqueKey},
};
use self::{
error::{DuplicatedNamesConflict, FilterTypeError, FrontendError, ValidationError},
outputs::OutputHandler,
tags::{TagHandler, TagLookupError},
util::{get_underlying_named_type, ComponentPath},
validation::validate_query_against_schema,
};
pub mod error;
mod outputs;
mod tags;
mod util;
mod validation;
pub fn parse(schema: &Schema, query: impl AsRef<str>) -> Result<Arc<IndexedQuery>, FrontendError> {
let ir_query = parse_to_ir(schema, query)?;
let indexed_query: IndexedQuery = ir_query.try_into().unwrap();
Ok(Arc::from(indexed_query))
}
pub fn parse_to_ir<T: AsRef<str>>(schema: &Schema, query: T) -> Result<IRQuery, FrontendError> {
let document = async_graphql_parser::parse_query(query)?;
let q = parse_document(&document)?;
make_ir_for_query(schema, &q)
}
pub fn parse_doc(schema: &Schema, document: &ExecutableDocument) -> Result<IRQuery, FrontendError> {
let q = parse_document(document)?;
make_ir_for_query(schema, &q)
}
fn get_field_name_and_type_from_schema<'a>(
defined_fields: &'a [Positioned<FieldDefinition>],
field_node: &FieldNode,
) -> (&'a Name, Arc<str>, Arc<str>, &'a Type) {
if field_node.name.as_ref() == TYPENAME_META_FIELD {
return (
&TYPENAME_META_FIELD_NAME,
TYPENAME_META_FIELD_ARC.clone(),
TYPENAME_META_FIELD_ARC.clone(),
&TYPENAME_META_FIELD_TYPE,
);
}
for defined_field in defined_fields {
let field_name = &defined_field.node.name.node;
let field_raw_type = &defined_field.node.ty.node;
if field_name.as_ref() == field_node.name.as_ref() {
let pre_coercion_type_name: Arc<str> =
get_underlying_named_type(field_raw_type).to_string().into();
let post_coercion_type_name = if let Some(coerced_to) = &field_node.coerced_to {
coerced_to.clone()
} else {
pre_coercion_type_name.clone()
};
return (
field_name,
pre_coercion_type_name,
post_coercion_type_name,
field_raw_type,
);
}
}
unreachable!()
}
fn get_vertex_type_definition_from_schema<'a>(
schema: &'a Schema,
vertex_type_name: &str,
) -> Result<&'a TypeDefinition, FrontendError> {
schema.vertex_types.get(vertex_type_name).ok_or_else(|| {
FrontendError::ValidationError(ValidationError::NonExistentType(
vertex_type_name.to_owned(),
))
})
}
fn get_edge_definition_from_schema<'a>(
schema: &'a Schema,
type_name: &str,
edge_name: &str,
) -> &'a FieldDefinition {
let defined_fields = get_vertex_field_definitions(schema, type_name);
for defined_field in defined_fields {
let field_name = defined_field.node.name.node.as_str();
if field_name == edge_name {
return &defined_field.node;
}
}
unreachable!()
}
fn get_vertex_field_definitions<'a>(
schema: &'a Schema,
type_name: &str,
) -> &'a Vec<Positioned<FieldDefinition>> {
match &schema.vertex_types[type_name].kind {
TypeKind::Object(o) => &o.fields,
TypeKind::Interface(i) => &i.fields,
_ => unreachable!(),
}
}
fn make_edge_parameters(
edge_definition: &FieldDefinition,
specified_arguments: &BTreeMap<Arc<str>, FieldValue>,
) -> Result<Option<Arc<EdgeParameters>>, Vec<FrontendError>> {
let mut errors: Vec<FrontendError> = vec![];
let mut edge_arguments: BTreeMap<Arc<str>, FieldValue> = BTreeMap::new();
for arg in &edge_definition.arguments {
let arg_name = arg.node.name.node.as_ref();
let specified_value = match specified_arguments.get(arg_name) {
None => {
arg.node
.default_value
.as_ref()
.map(|v| {
let value = FieldValue::try_from(&v.node).unwrap();
assert!(is_argument_type_valid(&arg.node.ty.node, &value));
value
})
.or({
if arg.node.ty.node.nullable {
Some(FieldValue::Null)
} else {
None
}
})
}
Some(value) => {
if !is_argument_type_valid(&arg.node.ty.node, value) {
errors.push(FrontendError::InvalidEdgeParameterType(
arg_name.to_string(),
edge_definition.name.node.to_string(),
arg.node.ty.to_string(),
value.clone(),
));
}
Some(value.clone())
}
};
match specified_value {
None => {
errors.push(FrontendError::MissingRequiredEdgeParameter(
arg_name.to_string(),
edge_definition.name.node.to_string(),
));
}
Some(value) => {
edge_arguments
.insert_or_error(arg_name.to_owned().into(), value)
.unwrap(); }
}
}
for specified_argument_name in specified_arguments.keys() {
if !edge_arguments.contains_key(specified_argument_name) {
errors.push(FrontendError::UnexpectedEdgeParameter(
specified_argument_name.to_string(),
edge_definition.name.node.to_string(),
))
}
}
if !errors.is_empty() {
Err(errors)
} else if edge_arguments.is_empty() {
Ok(None)
} else {
Ok(Some(Arc::new(EdgeParameters(edge_arguments))))
}
}
fn infer_variable_type(
property_name: &str,
property_type: &Type,
operation: &Operation<(), OperatorArgument>,
) -> Result<Type, Box<FilterTypeError>> {
match operation {
Operation::Equals(..) | Operation::NotEquals(..) => {
Ok(property_type.clone())
}
Operation::LessThan(..)
| Operation::LessThanOrEqual(..)
| Operation::GreaterThan(..)
| Operation::GreaterThanOrEqual(..) => {
Ok(Type {
base: property_type.base.clone(),
nullable: false,
})
}
Operation::Contains(..) | Operation::NotContains(..) => {
let inner_type = match &property_type.base {
BaseType::Named(_) => {
return Err(Box::new(
FilterTypeError::ListFilterOperationOnNonListField(
operation.operation_name().to_string(),
property_name.to_string(),
property_type.to_string(),
),
))
}
BaseType::List(inner) => inner.as_ref(),
};
Ok(inner_type.clone())
}
Operation::OneOf(..) | Operation::NotOneOf(..) => {
Ok(Type {
base: BaseType::List(Box::new(property_type.clone())),
nullable: false,
})
}
Operation::HasPrefix(..)
| Operation::NotHasPrefix(..)
| Operation::HasSuffix(..)
| Operation::NotHasSuffix(..)
| Operation::HasSubstring(..)
| Operation::NotHasSubstring(..)
| Operation::RegexMatches(..)
| Operation::NotRegexMatches(..) => {
Ok(Type {
base: BaseType::Named(Name::new("String")),
nullable: false,
})
}
Operation::IsNull(..) | Operation::IsNotNull(..) => {
unreachable!()
}
}
}
#[allow(clippy::too_many_arguments)]
fn make_local_field_filter_expr(
schema: &Schema,
component_path: &ComponentPath,
tags: &mut TagHandler,
current_vertex_vid: Vid,
property_name: &Arc<str>,
property_type: &Type,
filter_directive: &FilterDirective,
) -> Result<Operation<LocalField, Argument>, Vec<FrontendError>> {
let left = LocalField {
field_name: property_name.clone(),
field_type: property_type.clone(),
};
make_filter_expr(
schema,
component_path,
tags,
current_vertex_vid,
left,
filter_directive,
)
}
#[allow(clippy::too_many_arguments)]
fn make_filter_expr<LeftT: NamedTypedValue>(
schema: &Schema,
component_path: &ComponentPath,
tags: &mut TagHandler,
current_vertex_vid: Vid,
left_operand: LeftT,
filter_directive: &FilterDirective,
) -> Result<Operation<LeftT, Argument>, Vec<FrontendError>> {
let filter_operation = filter_directive
.operation
.try_map(
|_| Ok(left_operand.clone()),
|arg| {
Ok(match arg {
OperatorArgument::VariableRef(var_name) => Argument::Variable(VariableRef {
variable_name: var_name.clone(),
variable_type: infer_variable_type(
left_operand.named(),
left_operand.typed(),
&filter_directive.operation,
)
.map_err(|e| *e)?,
}),
OperatorArgument::TagRef(tag_name) => {
let defined_tag = match tags.reference_tag(
tag_name.as_ref(),
component_path,
current_vertex_vid,
) {
Ok(defined_tag) => defined_tag,
Err(TagLookupError::UndefinedTag(tag_name)) => {
return Err(FrontendError::UndefinedTagInFilter(
left_operand.named().to_string(),
tag_name,
));
}
Err(TagLookupError::TagDefinedInsideFold(tag_name)) => {
return Err(FrontendError::TagUsedOutsideItsFoldedSubquery(
left_operand.named().to_string(),
tag_name,
));
}
Err(TagLookupError::TagUsedBeforeDefinition(tag_name)) => {
return Err(FrontendError::TagUsedBeforeDefinition(
left_operand.named().to_string(),
tag_name,
))
}
};
Argument::Tag(defined_tag.field.clone())
}
})
},
)
.map_err(|e| vec![e])?;
let maybe_tag_name = match filter_directive.operation.right() {
Some(OperatorArgument::TagRef(tag_name)) => Some(tag_name.as_ref()),
_ => None,
};
if let Err(e) = filter_operation.operand_types_valid(maybe_tag_name) {
Err(e.into_iter().map(|x| x.into()).collect())
} else {
Ok(filter_operation)
}
}
pub(crate) fn make_ir_for_query(schema: &Schema, query: &Query) -> Result<IRQuery, FrontendError> {
validate_query_against_schema(schema, query)?;
let mut vid_maker = successors(Some(Vid::new(NonZeroUsize::new(1).unwrap())), |x| {
let inner_number = x.0.get();
Some(Vid::new(
NonZeroUsize::new(inner_number.checked_add(1).unwrap()).unwrap(),
))
});
let mut eid_maker = successors(Some(Eid::new(NonZeroUsize::new(1).unwrap())), |x| {
let inner_number = x.0.get();
Some(Eid::new(
NonZeroUsize::new(inner_number.checked_add(1).unwrap()).unwrap(),
))
});
let mut errors: Vec<FrontendError> = vec![];
let (root_field_name, root_field_pre_coercion_type, root_field_post_coercion_type, _) =
get_field_name_and_type_from_schema(&schema.query_type.fields, &query.root_field);
let starting_vid = vid_maker.next().unwrap();
let root_parameters = make_edge_parameters(
get_edge_definition_from_schema(schema, schema.query_type_name(), root_field_name.as_ref()),
&query.root_connection.arguments,
);
let mut component_path = ComponentPath::new(starting_vid);
let mut tags = Default::default();
let mut output_handler = OutputHandler::new(starting_vid, None);
let mut root_component = make_query_component(
schema,
query,
&mut vid_maker,
&mut eid_maker,
&mut component_path,
&mut output_handler,
&mut tags,
None,
starting_vid,
root_field_pre_coercion_type,
root_field_post_coercion_type,
&query.root_field,
);
if let Err(e) = &root_parameters {
errors.extend(e.iter().cloned());
}
let root_component = match root_component {
Ok(r) => r,
Err(e) => {
errors.extend(e);
return Err(errors.into());
}
};
let mut variables: BTreeMap<Arc<str>, Type> = Default::default();
if let Err(v) = fill_in_query_variables(&mut variables, &root_component) {
errors.extend(v.into_iter().map(|x| x.into()));
}
if let Err(e) = tags.finish() {
errors.push(FrontendError::UnusedTags(
e.into_iter().map(String::from).collect(),
));
}
let all_outputs = output_handler.finish();
if let Err(duplicates) = check_for_duplicate_output_names(all_outputs) {
let all_vertices = collect_ir_vertices(&root_component);
let errs = make_duplicated_output_names_error(&all_vertices, duplicates);
errors.extend(errs.into_iter());
}
if errors.is_empty() {
Ok(IRQuery {
root_name: root_field_name.as_ref().to_owned().into(),
root_parameters: root_parameters.unwrap(),
root_component: root_component.into(),
variables,
})
} else {
Err(errors.into())
}
}
fn collect_ir_vertices(root_component: &IRQueryComponent) -> BTreeMap<Vid, IRVertex> {
let mut result = Default::default();
collect_ir_vertices_recursive_step(&mut result, root_component);
result
}
fn collect_ir_vertices_recursive_step(
result: &mut BTreeMap<Vid, IRVertex>,
component: &IRQueryComponent,
) {
result.extend(component.vertices.iter().map(|(k, v)| (*k, v.clone())));
component
.folds
.values()
.for_each(move |fold| collect_ir_vertices_recursive_step(result, &fold.component))
}
fn fill_in_query_variables(
variables: &mut BTreeMap<Arc<str>, Type>,
component: &IRQueryComponent,
) -> Result<(), Vec<FilterTypeError>> {
let mut errors: Vec<FilterTypeError> = vec![];
let all_variable_uses = component
.vertices
.values()
.flat_map(|vertex| &vertex.filters)
.map(|filter| filter.right())
.chain(
component
.folds
.values()
.flat_map(|fold| &fold.post_filters)
.map(|filter| filter.right()),
)
.filter_map(|rhs| match rhs {
Some(Argument::Variable(vref)) => Some(vref),
_ => None,
});
for vref in all_variable_uses {
let existing_type = variables
.entry(vref.variable_name.clone())
.or_insert_with(|| vref.variable_type.clone());
match intersect_types(existing_type, &vref.variable_type) {
Some(intersection) => {
*existing_type = intersection;
}
None => {
errors.push(FilterTypeError::IncompatibleVariableTypeRequirements(
vref.variable_name.to_string(),
existing_type.to_string(),
vref.variable_type.to_string(),
));
}
}
}
for fold in component.folds.values() {
if let Err(e) = fill_in_query_variables(variables, fold.component.as_ref()) {
errors.extend(e.into_iter());
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn make_duplicated_output_names_error(
ir_vertices: &BTreeMap<Vid, IRVertex>,
duplicates: BTreeMap<Arc<str>, Vec<FieldRef>>,
) -> Vec<FrontendError> {
let conflict_info = DuplicatedNamesConflict {
duplicates: duplicates
.iter()
.map(|(k, fields)| {
let duplicate_values = fields
.iter()
.map(|field| match field {
FieldRef::ContextField(field) => {
let vid = field.vertex_id;
(
ir_vertices[&vid].type_name.to_string(),
field.field_name.to_string(),
)
}
FieldRef::FoldSpecificField(field) => {
let vid = field.fold_root_vid;
match field.kind {
FoldSpecificFieldKind::Count => (
ir_vertices[&vid].type_name.to_string(),
"fold count value".to_string(),
),
}
}
})
.collect();
(k.to_string(), duplicate_values)
})
.collect(),
};
vec![FrontendError::MultipleOutputsWithSameName(conflict_info)]
}
#[allow(clippy::type_complexity)]
fn check_for_duplicate_output_names(
maybe_duplicated_outputs: BTreeMap<Arc<str>, Vec<FieldRef>>,
) -> Result<BTreeMap<Arc<str>, FieldRef>, BTreeMap<Arc<str>, Vec<FieldRef>>> {
maybe_duplicated_outputs
.into_iter()
.flat_map(|(name, outputs)| outputs.into_iter().map(move |o| (name.clone(), o)))
.try_collect_unique()
}
#[allow(clippy::too_many_arguments)]
fn make_query_component<'schema, 'query, V, E>(
schema: &'schema Schema,
query: &'query Query,
vid_maker: &mut V,
eid_maker: &mut E,
component_path: &mut ComponentPath,
output_handler: &mut OutputHandler<'query>,
tags: &mut TagHandler<'query>,
parent_vid: Option<Vid>,
starting_vid: Vid,
pre_coercion_type: Arc<str>,
post_coercion_type: Arc<str>,
starting_field: &'query FieldNode,
) -> Result<IRQueryComponent, Vec<FrontendError>>
where
'schema: 'query,
V: Iterator<Item = Vid>,
E: Iterator<Item = Eid>,
{
let mut errors: Vec<FrontendError> = vec![];
let mut vertices: BTreeMap<Vid, (Arc<str>, &'query FieldNode)> = Default::default();
let mut edges: BTreeMap<Eid, (Vid, Vid, &'query FieldConnection)> = Default::default();
let mut property_names_by_vertex: BTreeMap<Vid, Vec<Arc<str>>> = Default::default();
#[allow(clippy::type_complexity)]
let mut properties: BTreeMap<
(Vid, Arc<str>),
(Arc<str>, &'schema Type, SmallVec<[&'query FieldNode; 1]>),
> = Default::default();
output_handler.begin_subcomponent();
let mut folds: BTreeMap<Eid, Arc<IRFold>> = Default::default();
if let Err(e) = fill_in_vertex_data(
schema,
query,
vid_maker,
eid_maker,
&mut vertices,
&mut edges,
&mut folds,
&mut property_names_by_vertex,
&mut properties,
component_path,
output_handler,
tags,
None,
starting_vid,
pre_coercion_type,
post_coercion_type,
starting_field,
) {
errors.extend(e);
}
let vertex_results = vertices
.iter()
.map(|(vid, (uncoerced_type_name, field_node))| {
make_vertex(
schema,
&property_names_by_vertex,
&properties,
tags,
component_path,
*vid,
uncoerced_type_name,
field_node,
)
});
let ir_vertices: BTreeMap<Vid, IRVertex> = vertex_results
.filter_map(|res| match res {
Ok(v) => Some((v.vid, v)),
Err(e) => {
errors.extend(e);
None
}
})
.try_collect_unique()
.unwrap();
if !errors.is_empty() {
return Err(errors);
}
let mut ir_edges: BTreeMap<Eid, Arc<IREdge>> = BTreeMap::new();
for (eid, (from_vid, to_vid, field_connection)) in edges.iter() {
let from_vertex_type = &ir_vertices[from_vid].type_name;
let edge_definition = get_edge_definition_from_schema(
schema,
from_vertex_type.as_ref(),
field_connection.name.as_ref(),
);
let edge_name = edge_definition.name.node.as_ref().to_owned().into();
let parameters_result = make_edge_parameters(edge_definition, &field_connection.arguments);
let optional = field_connection.optional.is_some();
let recursive = match field_connection.recurse.as_ref() {
None => None,
Some(d) => {
match get_recurse_implicit_coercion(
schema,
&ir_vertices[from_vid],
edge_definition,
d,
) {
Ok(coerce_to) => Some(Recursive::new(d.depth, coerce_to)),
Err(e) => {
errors.push(e);
None
}
}
}
};
match parameters_result {
Ok(parameters) => {
ir_edges.insert(
*eid,
IREdge {
eid: *eid,
from_vid: *from_vid,
to_vid: *to_vid,
edge_name,
parameters,
optional,
recursive,
}
.into(),
);
}
Err(e) => {
errors.extend(e);
}
}
}
if !errors.is_empty() {
return Err(errors);
}
let maybe_duplicated_outputs = output_handler.end_subcomponent();
let component_outputs = match check_for_duplicate_output_names(maybe_duplicated_outputs) {
Ok(outputs) => outputs,
Err(duplicates) => {
return Err(make_duplicated_output_names_error(&ir_vertices, duplicates))
}
};
let hacked_outputs = component_outputs
.into_iter()
.filter_map(|(k, v)| match v {
FieldRef::ContextField(c) => Some((k, c)),
FieldRef::FoldSpecificField(_) => None,
})
.collect();
Ok(IRQueryComponent {
root: starting_vid,
vertices: ir_vertices,
edges: ir_edges,
folds,
outputs: hacked_outputs,
})
}
fn get_recurse_implicit_coercion(
schema: &Schema,
from_vertex: &IRVertex,
edge_definition: &FieldDefinition,
d: &RecurseDirective,
) -> Result<Option<Arc<str>>, FrontendError> {
let source_type = &from_vertex.type_name;
let destination_type = get_underlying_named_type(&edge_definition.ty.node).as_ref();
if !schema.is_named_type_subtype(destination_type, source_type) {
if !schema.is_named_type_subtype(source_type, destination_type) {
return Err(FrontendError::RecursingNonRecursableEdge(
edge_definition.name.node.to_string(),
source_type.to_string(),
destination_type.to_string(),
));
} else {
return Err(FrontendError::RecursionToSubtype(
edge_definition.name.node.to_string(),
source_type.to_string(),
destination_type.to_string(),
));
}
}
if source_type.as_ref() == destination_type {
return Ok(None);
}
let edge_name: Arc<str> = Arc::from(edge_definition.name.node.as_ref());
let destination_edge = schema
.fields
.get(&(Arc::from(destination_type), edge_name.clone()));
match destination_edge {
Some(destination_edge) => {
let edge_type = get_underlying_named_type(&destination_edge.ty.node).as_ref();
if edge_type == destination_type {
Ok(None)
} else {
Err(FrontendError::EdgeRecursionNeedingMultipleCoercions(
edge_name.to_string(),
))
}
}
None => {
let edge_origin = &schema.field_origins[&(source_type.clone(), edge_name.clone())];
match edge_origin {
FieldOrigin::SingleAncestor(ancestor) => {
let ancestor_edge = &schema.fields[&(ancestor.clone(), edge_name.clone())];
let edge_type = get_underlying_named_type(&ancestor_edge.ty.node).as_ref();
if edge_type == destination_type {
Ok(Some(ancestor.clone()))
} else {
Err(FrontendError::EdgeRecursionNeedingMultipleCoercions(
edge_name.to_string(),
))
}
}
FieldOrigin::MultipleAncestors(multiple) => {
Err(FrontendError::AmbiguousOriginEdgeRecursion(
edge_name.to_string(),
))
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn make_vertex<'schema, 'query>(
schema: &'schema Schema,
property_names_by_vertex: &BTreeMap<Vid, Vec<Arc<str>>>,
properties: &BTreeMap<
(Vid, Arc<str>),
(Arc<str>, &'schema Type, SmallVec<[&'query FieldNode; 1]>),
>,
tags: &mut TagHandler,
component_path: &ComponentPath,
vid: Vid,
uncoerced_type_name: &Arc<str>,
field_node: &'query FieldNode,
) -> Result<IRVertex, Vec<FrontendError>> {
let mut errors: Vec<FrontendError> = vec![];
let is_fold_root = component_path.is_component_root(vid);
if !is_fold_root && !field_node.output.is_empty() {
errors.push(FrontendError::UnsupportedEdgeOutput(
field_node.name.as_ref().to_owned(),
));
}
if let Some(first_filter) = field_node.filter.first() {
errors.push(FrontendError::UnsupportedEdgeFilter(
field_node.name.as_ref().to_owned(),
));
}
let (type_name, coerced_from_type) = match field_node.coerced_to.clone().map_or_else(
|| {
Result::<(Arc<str>, Option<Arc<str>>), FrontendError>::Ok((
uncoerced_type_name.clone(),
None,
))
},
|coerced_to_type| {
let coerced_type =
get_vertex_type_definition_from_schema(schema, coerced_to_type.as_ref())?;
Ok((
coerced_type.name.node.as_ref().to_owned().into(),
Some(uncoerced_type_name.clone()),
))
},
) {
Ok(x) => x,
Err(e) => {
errors.push(e);
return Err(errors);
}
};
let mut filters = vec![];
for property_name in property_names_by_vertex.get(&vid).into_iter().flatten() {
let (_, property_type, property_fields) =
properties.get(&(vid, property_name.clone())).unwrap();
for property_field in property_fields.iter() {
for filter_directive in property_field.filter.iter() {
match make_local_field_filter_expr(
schema,
component_path,
tags,
vid,
property_name,
property_type,
filter_directive,
) {
Ok(filter_operation) => {
filters.push(filter_operation);
}
Err(e) => {
errors.extend(e);
}
}
}
}
}
if errors.is_empty() {
Ok(IRVertex {
vid,
type_name,
coerced_from_type,
filters,
})
} else {
Err(errors)
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn fill_in_vertex_data<'schema, 'query, V, E>(
schema: &'schema Schema,
query: &'query Query,
vid_maker: &mut V,
eid_maker: &mut E,
vertices: &mut BTreeMap<Vid, (Arc<str>, &'query FieldNode)>,
edges: &mut BTreeMap<Eid, (Vid, Vid, &'query FieldConnection)>,
folds: &mut BTreeMap<Eid, Arc<IRFold>>,
property_names_by_vertex: &mut BTreeMap<Vid, Vec<Arc<str>>>,
properties: &mut BTreeMap<
(Vid, Arc<str>),
(Arc<str>, &'schema Type, SmallVec<[&'query FieldNode; 1]>),
>,
component_path: &mut ComponentPath,
output_handler: &mut OutputHandler<'query>,
tags: &mut TagHandler<'query>,
parent_vid: Option<Vid>,
current_vid: Vid,
pre_coercion_type: Arc<str>,
post_coercion_type: Arc<str>,
current_field: &'query FieldNode,
) -> Result<(), Vec<FrontendError>>
where
'schema: 'query,
V: Iterator<Item = Vid>,
E: Iterator<Item = Eid>,
{
let mut errors: Vec<FrontendError> = vec![];
vertices
.insert_or_error(current_vid, (pre_coercion_type, current_field))
.unwrap();
let defined_fields = get_vertex_field_definitions(schema, post_coercion_type.as_ref());
for (connection, subfield) in ¤t_field.connections {
let (
subfield_name,
subfield_pre_coercion_type,
subfield_post_coercion_type,
subfield_raw_type,
) = get_field_name_and_type_from_schema(defined_fields, subfield);
if schema
.vertex_types
.contains_key(subfield_post_coercion_type.as_ref())
{
let next_vid = vid_maker.next().unwrap();
let next_eid = eid_maker.next().unwrap();
output_handler
.begin_nested_scope(next_vid, subfield.alias.as_ref().map(|x| x.as_ref()));
if let Some(fold_group) = &connection.fold {
if connection.optional.is_some() {
errors.push(FrontendError::UnsupportedDirectiveOnFoldedEdge(
subfield.name.to_string(),
"@optional".to_owned(),
));
}
if connection.recurse.is_some() {
errors.push(FrontendError::UnsupportedDirectiveOnFoldedEdge(
subfield.name.to_string(),
"@recurse".to_owned(),
));
}
let edge_definition = get_edge_definition_from_schema(
schema,
post_coercion_type.as_ref(),
connection.name.as_ref(),
);
match make_edge_parameters(edge_definition, &connection.arguments) {
Ok(edge_parameters) => {
match make_fold(
schema,
query,
vid_maker,
eid_maker,
component_path,
output_handler,
tags,
fold_group,
next_eid,
edge_definition.name.node.as_str().to_owned().into(),
edge_parameters,
current_vid,
next_vid,
subfield_pre_coercion_type,
subfield_post_coercion_type,
subfield,
) {
Ok(fold) => {
folds.insert(next_eid, fold.into());
}
Err(e) => {
errors.extend(e);
}
}
}
Err(e) => {
errors.extend(e);
}
}
} else {
edges
.insert_or_error(next_eid, (current_vid, next_vid, connection))
.expect("Unexpectedly encountered duplicate eid");
if let Err(e) = fill_in_vertex_data(
schema,
query,
vid_maker,
eid_maker,
vertices,
edges,
folds,
property_names_by_vertex,
properties,
component_path,
output_handler,
tags,
Some(current_vid),
next_vid,
subfield_pre_coercion_type.clone(),
subfield_post_coercion_type.clone(),
subfield,
) {
errors.extend(e);
}
}
output_handler.end_nested_scope(next_vid);
} else if BUILTIN_SCALARS.contains(subfield_post_coercion_type.as_ref())
|| schema
.scalars
.contains_key(subfield_post_coercion_type.as_ref())
|| subfield_name.as_ref() == TYPENAME_META_FIELD
{
let subfield_name: Arc<str> = subfield_name.as_ref().to_owned().into();
let key = (current_vid, subfield_name.clone());
properties
.entry(key)
.and_modify(|(prior_name, prior_type, subfields)| {
assert_eq!(subfield_name.as_ref(), prior_name.as_ref());
assert_eq!(&subfield_raw_type, prior_type);
subfields.push(subfield);
})
.or_insert_with(|| {
property_names_by_vertex
.entry(current_vid)
.or_default()
.push(subfield_name.clone());
(subfield_name, subfield_raw_type, SmallVec::from([subfield]))
});
for output_directive in &subfield.output {
let field_ref = FieldRef::ContextField(ContextField {
vertex_id: current_vid,
field_name: subfield.name.clone(),
field_type: subfield_raw_type.clone(),
});
if let Some(explicit_name) = output_directive.name.as_ref() {
output_handler
.register_explicitly_named_output(explicit_name.clone(), field_ref);
} else {
let local_name = subfield
.alias
.as_ref()
.map(|x| x.as_ref())
.unwrap_or_else(|| subfield.name.as_ref());
output_handler.register_locally_named_output(local_name, None, field_ref);
}
}
for tag_directive in &subfield.tag {
let tag_name = tag_directive
.name
.as_ref()
.map(|x| x.as_ref())
.unwrap_or_else(|| {
subfield
.alias
.as_ref()
.map(|x| x.as_ref())
.unwrap_or_else(|| subfield.name.as_ref())
});
let tag_field = ContextField {
vertex_id: current_vid,
field_name: subfield.name.clone(),
field_type: subfield_raw_type.to_owned(),
};
if let Err(e) =
tags.register_tag(tag_name, FieldRef::ContextField(tag_field), component_path)
{
errors.push(FrontendError::MultipleTagsWithSameName(
tag_name.to_string(),
));
}
}
} else {
unreachable!("field name: {}", subfield_name.as_ref());
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
#[allow(clippy::too_many_arguments)]
fn make_fold<'schema, 'query, V, E>(
schema: &'schema Schema,
query: &'query Query,
vid_maker: &mut V,
eid_maker: &mut E,
component_path: &mut ComponentPath,
output_handler: &mut OutputHandler<'query>,
tags: &mut TagHandler<'query>,
fold_group: &'query FoldGroup,
fold_eid: Eid,
edge_name: Arc<str>,
edge_parameters: Option<Arc<EdgeParameters>>,
parent_vid: Vid,
starting_vid: Vid,
starting_pre_coercion_type: Arc<str>,
starting_post_coercion_type: Arc<str>,
starting_field: &'query FieldNode,
) -> Result<IRFold, Vec<FrontendError>>
where
'schema: 'query,
V: Iterator<Item = Vid>,
E: Iterator<Item = Eid>,
{
component_path.push(starting_vid);
tags.begin_subcomponent(starting_vid);
let mut errors = vec![];
let component = make_query_component(
schema,
query,
vid_maker,
eid_maker,
component_path,
output_handler,
tags,
Some(parent_vid),
starting_vid,
starting_pre_coercion_type,
starting_post_coercion_type,
starting_field,
)?;
component_path.pop(starting_vid);
let imported_tags = tags.end_subcomponent(starting_vid);
if !starting_field.output.is_empty() {
errors.push(FrontendError::UnsupportedEdgeOutput(
starting_field.name.as_ref().to_owned(),
));
}
let mut post_filters = vec![];
let mut fold_specific_outputs = BTreeMap::new();
if let Some(transform_group) = &fold_group.transform {
if transform_group.retransform.is_some() {
unimplemented!("re-transforming a @fold @transform value is currently not supported");
}
let fold_specific_field = match transform_group.transform.kind {
TransformationKind::Count => FoldSpecificField {
fold_eid,
fold_root_vid: starting_vid,
kind: FoldSpecificFieldKind::Count,
},
};
let field_ref = FieldRef::FoldSpecificField(fold_specific_field.clone());
for filter_directive in &transform_group.filter {
match make_filter_expr(
schema,
component_path,
tags,
starting_vid,
fold_specific_field.kind,
filter_directive,
) {
Ok(filter) => post_filters.push(filter),
Err(e) => errors.extend(e.into_iter()),
}
}
for output in &transform_group.output {
let final_output_name = match output.name.as_ref() {
Some(explicit_name) => {
output_handler
.register_explicitly_named_output(explicit_name.clone(), field_ref.clone());
explicit_name.clone()
}
None => {
let local_name = if starting_field.alias.is_some() {
""
} else {
starting_field.name.as_ref()
};
output_handler.register_locally_named_output(
local_name,
Some(&[fold_specific_field.kind.transform_suffix()]),
field_ref.clone(),
)
}
};
let prior_output_by_that_name =
fold_specific_outputs.insert(final_output_name.clone(), fold_specific_field.kind);
if let Some(prior_output_kind) = prior_output_by_that_name {
errors.push(FrontendError::MultipleOutputsWithSameName(DuplicatedNamesConflict {
duplicates: btreemap! {
final_output_name.to_string() => vec![
(starting_field.name.to_string(), prior_output_kind.field_name().to_string()),
(starting_field.name.to_string(), fold_specific_field.kind.field_name().to_string()),
]
}
}))
}
}
for tag_directive in &transform_group.tag {
let tag_name = tag_directive.name.as_ref().map(|x| x.as_ref());
if let Some(tag_name) = tag_name {
let field = FieldRef::FoldSpecificField(fold_specific_field.clone());
if let Err(e) = tags.register_tag(tag_name, field, component_path) {
errors.push(FrontendError::MultipleTagsWithSameName(
tag_name.to_string(),
));
}
} else {
errors.push(FrontendError::ExplicitTagNameRequired(
starting_field.name.as_ref().to_owned(),
))
}
}
}
if !errors.is_empty() {
return Err(errors);
}
Ok(IRFold {
eid: fold_eid,
from_vid: parent_vid,
to_vid: starting_vid,
edge_name,
parameters: edge_parameters,
component: component.into(),
imported_tags,
post_filters,
fold_specific_outputs,
})
}
#[cfg(test)]
mod tests {
use std::{
fs,
path::{Path, PathBuf},
};
use trustfall_filetests_macros::parameterize;
use crate::{
frontend::make_ir_for_query,
schema::Schema,
util::{TestIRQuery, TestIRQueryResult, TestParsedGraphQLQueryResult},
};
lazy_static! {
static ref FILESYSTEM_SCHEMA: Schema =
Schema::parse(fs::read_to_string("src/resources/schemas/filesystem.graphql").unwrap())
.unwrap();
static ref NUMBERS_SCHEMA: Schema =
Schema::parse(fs::read_to_string("src/resources/schemas/numbers.graphql").unwrap())
.unwrap();
static ref NULLABLES_SCHEMA: Schema =
Schema::parse(fs::read_to_string("src/resources/schemas/nullables.graphql").unwrap())
.unwrap();
static ref RECURSES_SCHEMA: Schema =
Schema::parse(fs::read_to_string("src/resources/schemas/recurses.graphql").unwrap())
.unwrap();
}
#[test]
fn test_schemas_load_correctly() {
assert!(FILESYSTEM_SCHEMA.vertex_types.len() > 3);
assert!(!NUMBERS_SCHEMA.vertex_types.is_empty());
assert!(!NULLABLES_SCHEMA.vertex_types.is_empty());
assert!(!RECURSES_SCHEMA.vertex_types.is_empty());
}
#[parameterize("trustfall_core/src/resources/test_data/frontend_errors")]
fn frontend_errors(base: &Path, stem: &str) {
parameterizable_tester(base, stem, ".frontend-error.ron")
}
#[parameterize("trustfall_core/src/resources/test_data/execution_errors")]
fn execution_errors(base: &Path, stem: &str) {
parameterizable_tester(base, stem, ".ir.ron")
}
#[parameterize("trustfall_core/src/resources/test_data/valid_queries")]
fn valid_queries(base: &Path, stem: &str) {
parameterizable_tester(base, stem, ".ir.ron")
}
fn parameterizable_tester(base: &Path, stem: &str, check_file_suffix: &str) {
let mut input_path = PathBuf::from(base);
input_path.push(format!("{}.graphql-parsed.ron", stem));
let input_data = fs::read_to_string(input_path).unwrap();
let test_query: TestParsedGraphQLQueryResult = ron::from_str(&input_data).unwrap();
if test_query.is_err() {
return;
}
let test_query = test_query.unwrap();
let schema: &Schema = match test_query.schema_name.as_str() {
"filesystem" => &FILESYSTEM_SCHEMA,
"numbers" => &NUMBERS_SCHEMA,
"nullables" => &NULLABLES_SCHEMA,
"recurses" => &RECURSES_SCHEMA,
_ => unimplemented!("unrecognized schema name: {:?}", test_query.schema_name),
};
let mut check_path = PathBuf::from(base);
check_path.push(format!("{}{}", stem, check_file_suffix));
let check_data = fs::read_to_string(check_path).unwrap();
let arguments = test_query.arguments;
let constructed_test_item =
make_ir_for_query(schema, &test_query.query).map(move |ir_query| TestIRQuery {
schema_name: test_query.schema_name,
ir_query,
arguments,
});
let check_parsed: TestIRQueryResult = ron::from_str(&check_data).unwrap();
assert_eq!(check_parsed, constructed_test_item);
}
}