Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement field merging #472

Merged
merged 17 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 100 additions & 79 deletions src/builder.rs

Large diffs are not rendered by default.

253 changes: 222 additions & 31 deletions src/parser_util.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::graphql::{EnumSource, __InputValue, __Type, ___Type};
use crate::graphql::*;
use crate::gson;
use graphql_parser::query::*;
use graphql_parser::Pos;
use std::collections::HashMap;

pub fn alias_or_name<'a, T>(query_field: &graphql_parser::query::Field<'a, T>) -> String
Expand All @@ -14,34 +15,221 @@ where
.unwrap_or_else(|| query_field.name.as_ref().to_string())
}

pub fn normalize_selection_set<'a, 'b, T>(
selection_set: &'b SelectionSet<'a, T>,
fragment_definitions: &'b Vec<FragmentDefinition<'a, T>>,
pub fn merge_fields<'a, T, I>(
target_fields: &mut Vec<Field<'a, T>>,
next_fields: I,
type_name: &str,
field_map: &HashMap<String, __Field>,
) -> Result<(), String>
where
T: Text<'a> + Eq + AsRef<str> + std::fmt::Debug + Clone,
I: IntoIterator<Item = Field<'a, T>>,
{
for field in next_fields {
merge_field(target_fields, field, type_name, field_map)?
}
Ok(())
}

pub fn merge_field<'a, T>(
target_fields: &mut Vec<Field<'a, T>>,
mut field: Field<'a, T>,
type_name: &str,
field_map: &HashMap<String, __Field>,
) -> Result<(), String>
where
T: Text<'a> + Eq + AsRef<str> + std::fmt::Debug + Clone,
{
let Some((matching_idx, matching_field)) = target_fields
.iter()
.enumerate()
.find(|(_, target)| alias_or_name(target) == alias_or_name(&field))
else {
target_fields.push(field);
return Ok(());
};

can_fields_merge(&matching_field, &field, type_name, field_map)?;

field.position = field.position.min(matching_field.position);

field.selection_set.span =
min_encapsulating_span(field.selection_set.span, matching_field.selection_set.span);

// Subfields will be normalized and properly merged on a later pass.
field
.selection_set
.items
.extend(matching_field.selection_set.items.clone());

target_fields[matching_idx] = field;

Ok(())
}

pub fn can_fields_merge<'a, T>(
field_a: &Field<'a, T>,
field_b: &Field<'a, T>,
type_name: &str,
field_map: &HashMap<String, __Field>,
) -> Result<(), String>
where
T: Text<'a> + Eq + AsRef<str> + std::fmt::Debug + Clone,
{
let Some(_field_a) = field_map.get(field_a.name.as_ref()) else {
return Err(format!(
"Unknown field '{}' on type '{}'",
field_a.name.as_ref(),
type_name
));
};
let Some(_field_b) = field_map.get(field_b.name.as_ref()) else {
return Err(format!(
"Unknown field '{}' on type '{}'",
field_b.name.as_ref(),
type_name
));
};

has_same_type_shape(
&alias_or_name(field_a),
type_name,
&_field_a.type_,
&_field_b.type_,
)?;

if field_a.name != field_b.name {
return Err(format!(
"Fields '{}' on type '{}' conflict because '{}' and '{}' are different fields",
alias_or_name(field_a),
type_name,
field_a.name.as_ref(),
field_b.name.as_ref(),
));
}

for (arg_a_name, arg_a_value) in field_a.arguments.iter() {
let arg_b_value = field_b.arguments.iter().find_map(|(name, value)| {
if name == arg_a_name {
Some(value)
} else {
None
}
});
let args_match = match arg_b_value {
None => false,
Some(arg_b_value) => arg_b_value == arg_a_value,
};
if !args_match {
return Err(format!(
"Fields '{}' on type '{}' conflict because they have differing arguments",
alias_or_name(field_a),
type_name,
));
}
}

Ok(())
}

pub fn has_same_type_shape(
field_name: &str,
type_name: &str,
type_a: &__Type,
type_b: &__Type,
) -> Result<(), String> {
let mut type_a = type_a;
let mut type_b = type_b;

if matches!(type_a, __Type::NonNull(_)) || matches!(type_b, __Type::NonNull(_)) {
if let (__Type::NonNull(nullable_type_a), __Type::NonNull(nullable_type_b)) =
(type_a, type_b)
{
type_a = nullable_type_a.type_.as_ref();
type_b = nullable_type_b.type_.as_ref();
} else {
return Err(format!(
"Fields '{}' on type '{}' conflict because only one is non nullable",
field_name, type_name,
));
}
}

if matches!(type_a, __Type::List(_)) || matches!(type_b, __Type::List(_)) {
if let (__Type::List(list_type_a), __Type::List(list_type_b)) = (type_a, type_b) {
type_a = list_type_a.type_.as_ref();
type_b = list_type_b.type_.as_ref();
} else {
return Err(format!(
"Fields '{}' on type '{}' conflict because only one is a list type",
field_name, type_name,
));
}

return has_same_type_shape(field_name, type_name, type_a, type_b);
}

if matches!(type_a, __Type::Enum(_))
|| matches!(type_b, __Type::Enum(_))
|| matches!(type_a, __Type::Scalar(_))
|| matches!(type_b, __Type::Scalar(_))
{
return if type_a == type_b {
Ok(())
} else {
Err(format!(
"Fields '{}' on type '{}' conflict due to mismatched types",
field_name, type_name,
))
};
}

// TODO handle composite types?

// Subfield type shapes will be checked on a later pass.
Ok(())
}

pub fn min_encapsulating_span(a: (Pos, Pos), b: (Pos, Pos)) -> (Pos, Pos) {
(a.0.min(b.0), a.1.max(b.1))
}

pub fn normalize_selection_set<'a, T>(
selection_set: &SelectionSet<'a, T>,
fragment_definitions: &Vec<FragmentDefinition<'a, T>>,
type_name: &String, // for inline fragments
variables: &serde_json::Value, // for directives
) -> Result<Vec<&'b Field<'a, T>>, String>
field_type: &__Type,
) -> Result<Vec<Field<'a, T>>, String>
where
T: Text<'a> + Eq + AsRef<str>,
T: Text<'a> + Eq + AsRef<str> + std::fmt::Debug + Clone,
{
let mut selections: Vec<&'b Field<'a, T>> = vec![];
let mut normalized_fields: Vec<Field<'a, T>> = vec![];

let field_map = field_map(&field_type.unmodified_type());

for selection in &selection_set.items {
let sel = selection;
match normalize_selection(sel, fragment_definitions, type_name, variables) {
Ok(sels) => selections.extend(sels),
match normalize_selection(
selection,
fragment_definitions,
type_name,
variables,
field_type,
) {
Ok(fields) => merge_fields(&mut normalized_fields, fields, type_name, &field_map)?,
Err(err) => return Err(err),
}
}
Ok(selections)
Ok(normalized_fields)
}

/// Combines @skip and @include
pub fn selection_is_skipped<'a, 'b, T>(
query_selection: &'b Selection<'a, T>,
pub fn selection_is_skipped<'a, T>(
query_selection: &Selection<'a, T>,
variables: &serde_json::Value,
) -> Result<bool, String>
where
T: Text<'a> + Eq + AsRef<str>,
T: Text<'a> + Eq + AsRef<str> + std::fmt::Debug,
{
let directives = match query_selection {
Selection::Field(x) => &x.directives,
Expand Down Expand Up @@ -130,24 +318,27 @@ where
}

/// Normalizes literal selections, fragment spreads, and inline fragments
pub fn normalize_selection<'a, 'b, T>(
query_selection: &'b Selection<'a, T>,
fragment_definitions: &'b Vec<FragmentDefinition<'a, T>>,
pub fn normalize_selection<'a, T>(
query_selection: &Selection<'a, T>,
fragment_definitions: &Vec<FragmentDefinition<'a, T>>,
type_name: &String, // for inline fragments
variables: &serde_json::Value, // for directives
) -> Result<Vec<&'b Field<'a, T>>, String>
field_type: &__Type, // for field merging shape check
) -> Result<Vec<Field<'a, T>>, String>
where
T: Text<'a> + Eq + AsRef<str>,
T: Text<'a> + Eq + AsRef<str> + std::fmt::Debug + Clone,
{
let mut selections: Vec<&Field<'a, T>> = vec![];
let mut normalized_fields: Vec<Field<'a, T>> = vec![];

if selection_is_skipped(query_selection, variables)? {
return Ok(selections);
return Ok(normalized_fields);
}

let field_map = field_map(&field_type.unmodified_type());

match query_selection {
Selection::Field(field) => {
selections.push(field);
merge_field(&mut normalized_fields, field.clone(), type_name, &field_map)?;
}
Selection::FragmentSpread(fragment_spread) => {
let frag_name = &fragment_spread.fragment_name;
Expand All @@ -173,14 +364,15 @@ where
};

// TODO handle directives?
let frag_selections = normalize_selection_set(
let frag_fields = normalize_selection_set(
&frag_def.selection_set,
fragment_definitions,
type_name,
variables,
field_type,
);
match frag_selections {
Ok(sels) => selections.extend(sels.iter()),
match frag_fields {
Ok(fields) => merge_fields(&mut normalized_fields, fields, type_name, &field_map)?,
Err(err) => return Err(err),
};
}
Expand All @@ -193,18 +385,19 @@ where
};

if inline_fragment_applies {
let infrag_selections = normalize_selection_set(
let infrag_fields = normalize_selection_set(
&inline_fragment.selection_set,
fragment_definitions,
type_name,
variables,
field_type,
)?;
selections.extend(infrag_selections.iter());
merge_fields(&mut normalized_fields, infrag_fields, type_name, &field_map)?;
}
}
}

Ok(selections)
Ok(normalized_fields)
}

pub fn to_gson<'a, T>(
Expand All @@ -213,7 +406,7 @@ pub fn to_gson<'a, T>(
variable_definitions: &Vec<VariableDefinition<'a, T>>,
) -> Result<gson::Value, String>
where
T: Text<'a> + AsRef<str>,
T: Text<'a> + AsRef<str> + std::fmt::Debug,
{
let result = match graphql_value {
Value::Null => gson::Value::Null,
Expand Down Expand Up @@ -274,7 +467,6 @@ where
}

pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result<gson::Value, String> {
use crate::graphql::Scalar;
use crate::gson::Number as GsonNumber;
use crate::gson::Value as GsonValue;

Expand Down Expand Up @@ -483,7 +675,6 @@ pub fn validate_arg_from_input_object(
input_type: &__Type,
value: &gson::Value,
) -> Result<gson::Value, String> {
use crate::graphql::__TypeKind;
use crate::gson::Value as GsonValue;

let input_type_name = input_type.name().unwrap_or_default();
Expand Down
Loading
Loading