diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ecb62a..2fc53c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 1.0.6 (2024-04-27) + +* Enhancements + * Add new functions to `argo_index_map`: + * `argo_index_map:groups_from_list/2` + * `argo_index_map:groups_from_list/3` + * Add new function to `argo_graphql_field`: + * `argo_graphql_field:get_response_key/1` + * Reorganize `argo_typer` to better match upstream. +* Fixes + * Minor correction to "Field Selection Merging" from [GraphQL Spec: 5.3.2 Field Selection Merging](https://spec.graphql.org/draft/#sec-Field-Selection-Merging) (see [msolomon/argo#19](https://github.com/msolomon/argo/pull/19)). + ## 1.0.5 (2024-04-23) * Enhancements diff --git a/apps/argo/include/argo_typer.hrl b/apps/argo/include/argo_typer.hrl new file mode 100644 index 0000000..dc78c08 --- /dev/null +++ b/apps/argo/include/argo_typer.hrl @@ -0,0 +1,26 @@ +%%%----------------------------------------------------------------------------- +%%% Copyright (c) Meta Platforms, Inc. and affiliates. +%%% Copyright (c) WhatsApp LLC +%%% +%%% This source code is licensed under the MIT license found in the +%%% LICENSE.md file in the root directory of this source tree. +%%% +%%% @author Andrew Bennett +%%% @copyright (c) Meta Platforms, Inc. and affiliates. +%%% @doc +%%% +%%% @end +%%% Created : 26 Apr 2024 by Andrew Bennett +%%%----------------------------------------------------------------------------- +%%% % @format +%% @oncall whatsapp_clr +-ifndef(ARGO_TYPER_HRL). +-define(ARGO_TYPER_HRL, 1). + +-record(argo_typer, { + service_document :: argo_graphql_service_document:t(), + executable_document :: argo_graphql_executable_document:t(), + options :: argo_typer:options() +}). + +-endif. diff --git a/apps/argo/src/argo.app.src b/apps/argo/src/argo.app.src index 0b22aa6..cd06df1 100644 --- a/apps/argo/src/argo.app.src +++ b/apps/argo/src/argo.app.src @@ -7,7 +7,7 @@ %%% % @format {application, argo, [ {description, "argo: Erlang implementation of Argo for GraphQL"}, - {vsn, "1.0.5"}, + {vsn, "1.0.6"}, {modules, []}, {registered, []}, %% NOTE: Remember to sync changes to `applications` to diff --git a/apps/argo/src/argo_index_map.erl b/apps/argo/src/argo_index_map.erl index 52c0ff3..ce195a9 100644 --- a/apps/argo/src/argo_index_map.erl +++ b/apps/argo/src/argo_index_map.erl @@ -35,6 +35,8 @@ get_full/2, get_index/2, get_index_of/2, + groups_from_list/2, + groups_from_list/3, is_index/2, is_key/2, iterator/1, @@ -66,6 +68,10 @@ -type filter_func(Index, Key, Value) :: fun((Index, Key, Value) -> boolean()). -type filtermap_func() :: filtermap_func(index(), key(), value(), value()). -type filtermap_func(Index, Key, Value1, Value2) :: fun((Index, Key, Value1) -> boolean() | {true, Value2}). +-type groups_from_list_key_fun() :: groups_from_list_key_fun(dynamic(), key()). +-type groups_from_list_key_fun(Elem, Key) :: fun((Elem) -> Key). +-type groups_from_list_value_fun() :: groups_from_list_value_fun(dynamic(), value()). +-type groups_from_list_value_fun(Elem, Value) :: fun((Elem) -> Value). -type index() :: non_neg_integer(). -type key() :: dynamic(). -type value() :: dynamic(). @@ -88,6 +94,10 @@ filter_func/3, filtermap_func/0, filtermap_func/4, + groups_from_list_key_fun/0, + groups_from_list_key_fun/2, + groups_from_list_value_fun/0, + groups_from_list_value_fun/2, index/0, iterator/0, iterator/2, @@ -293,6 +303,29 @@ get_index_of(Key, IndexMap = #argo_index_map{}) -> erlang:error({badkey, Key}, [Key, IndexMap], [{error_info, #{module => ?MODULE}}]) end. +-spec groups_from_list(KeyFun, List) -> IndexMap when + KeyFun :: groups_from_list_key_fun(Elem, Key), + List :: [Elem], + Elem :: dynamic(), + Key :: key(), + Value :: value(), + IndexMap :: t(Key, Value). +groups_from_list(KeyFun, List) when is_function(KeyFun, 1) andalso (is_list(List) andalso length(List) >= 0) -> + groups_from_list(KeyFun, fun identity/1, List). + +-spec groups_from_list(KeyFun, ValueFun, List) -> IndexMap when + KeyFun :: groups_from_list_key_fun(Elem, Key), + ValueFun :: groups_from_list_value_fun(Elem, Value), + List :: [Elem], + Elem :: dynamic(), + Key :: key(), + Value :: value(), + IndexMap :: t(Key, Value). +groups_from_list(KeyFun, ValueFun, List) when + is_function(KeyFun, 1) andalso is_function(ValueFun, 1) andalso (is_list(List) andalso length(List) >= 0) +-> + groups_from_list_internal(KeyFun, ValueFun, List, new()). + -spec is_index(Index, IndexMap) -> boolean() when Index :: index(), IndexMap :: t(). is_index(Index, IndexMap = #argo_index_map{}) -> Index < ?MODULE:size(IndexMap). @@ -707,6 +740,35 @@ foldr_iterator(Iterator, Function, Acc1, Entries1) -> from_list({Key, Value}, IndexMap) -> ?MODULE:put(Key, Value, IndexMap). +%% @private +-spec groups_from_list_internal(KeyFun, ValueFun, List, IndexMap1) -> IndexMap2 when + KeyFun :: groups_from_list_key_fun(Elem, Key), + ValueFun :: groups_from_list_value_fun(Elem, Value), + List :: [Elem], + Elem :: dynamic(), + Key :: key(), + Value :: value(), + IndexMap1 :: t(Key, Value), + IndexMap2 :: t(Key, Value). +groups_from_list_internal(_KeyFun, _ValueFun, [], IndexMap1) -> + IndexMap1; +groups_from_list_internal(KeyFun, ValueFun, [Elem | List], IndexMap1) -> + Key = KeyFun(Elem), + Value = ValueFun(Elem), + IndexMap2 = update_with( + Key, + fun(_Index, Values) -> + Values ++ [Value] + end, + [Value], + IndexMap1 + ), + groups_from_list_internal(KeyFun, ValueFun, List, IndexMap2). + +%% @private +-spec identity(T) -> T when T :: dynamic(). +identity(T) -> T. + %% @private -spec repair(Index, undefined | {Key, Value}, Acc) -> Acc when Index :: index(), diff --git a/apps/argo/src/argo_typer.erl b/apps/argo/src/argo_typer.erl index 5860c51..394f5c1 100644 --- a/apps/argo/src/argo_typer.erl +++ b/apps/argo/src/argo_typer.erl @@ -19,9 +19,25 @@ -include_lib("argo/include/argo_common.hrl"). -include_lib("argo/include/argo_graphql.hrl"). +-include_lib("argo/include/argo_typer.hrl"). -include_lib("argo/include/argo_wire_type.hrl"). -%% API +%% New API +-export([ + new/3 +]). +%% Instance API +-export([ + collect_field_wire_types/3, + collect_fields_static/2, + get_field_definition/3, + get_field_definition/4, + get_fragment_definition/2, + get_operation_definition/2, + get_operation_type_definition/2, + get_type_definition/2 +]). +%% Public API -export([ derive_wire_type/3, derive_wire_type/4, @@ -36,22 +52,206 @@ ]). %% Records +-record(collect_field_wire_types, { + typer :: t(), + selection_type_definition :: argo_graphql_type_definition:t(), + selection_set :: argo_graphql_selection_set:t(), + exact_selections = sets:new([{version, 2}]) :: sets:set(argo_types:name()), + record_fields = queue:new() :: queue:queue(argo_field_wire_type:t()) +}). +-record(field_map, { + node :: selected_field_node(), + definition :: argo_graphql_field_definition:t() +}). -record(selected_field_node, { by :: argo_graphql_selection_set:selection(), field :: argo_graphql_field:t() }). %% Types +-type collect_field_wire_types() :: #collect_field_wire_types{}. +-type field_map() :: [#field_map{}]. -type options() :: #{ resolver => argo_typer_resolver:t() }. +-type selected_field_node() :: #selected_field_node{}. +-type t() :: #argo_typer{}. -export_type([ options/0 ]). %%%============================================================================= -%%% API functions +%%% New API functions +%%%============================================================================= + +-spec new(ServiceDocument, ExecutableDocument, Options) -> Typer when + ServiceDocument :: argo_graphql_service_document:t(), + ExecutableDocument :: argo_graphql_executable_document:t(), + Options :: options(), + Typer :: t(). +new( + ServiceDocument = #argo_graphql_service_document{}, + ExecutableDocument = #argo_graphql_executable_document{}, + Options +) when is_map(Options) -> + #argo_typer{ + service_document = ServiceDocument, + executable_document = ExecutableDocument, + options = Options + }. + +%%%============================================================================= +%%% Instance API functions +%%%============================================================================= + +-spec collect_field_wire_types(Typer, SelectionTypeDefinition, SelectionSet) -> {Typer, WireType} when + Typer :: t(), + SelectionTypeDefinition :: argo_graphql_type_definition:t(), + SelectionSet :: argo_graphql_selection_set:t(), + WireType :: argo_wire_type:t(). +collect_field_wire_types(Typer1, SelectionTypeDefinition, SelectionSet) -> + State1 = #collect_field_wire_types{ + typer = Typer1, + selection_type_definition = SelectionTypeDefinition, + selection_set = SelectionSet + }, + {State2, WireType} = collect_field_wire_types(State1), + Typer2 = State2#collect_field_wire_types.typer, + {Typer2, WireType}. + +-spec collect_fields_static(Typer, SelectionSet) -> {Typer, GroupedFields, VisitedFragments} when + Typer :: t(), + SelectionSet :: argo_graphql_selection_set:t(), + VisitedFragments :: sets:set(FragmentName), + FragmentName :: argo_types:name(), + GroupedFields :: argo_index_map:t(ResponseKey, SelectedFieldNodeList), + SelectedFieldNodeList :: [SelectedFieldNode], + ResponseKey :: argo_types:name(), + SelectedFieldNode :: selected_field_node(). +collect_fields_static( + Typer1 = #argo_typer{service_document = ServiceDocument, executable_document = ExecutableDocument}, + SelectionSet = #argo_graphql_selection_set{} +) -> + VisitedFragments1 = sets:new([{version, 2}]), + {GroupedFields, VisitedFragments2} = collect_fields_static( + ServiceDocument, ExecutableDocument, SelectionSet, VisitedFragments1 + ), + {Typer1, GroupedFields, VisitedFragments2}. + +-spec get_field_definition(Typer, TypeDefinition, FieldName) -> FieldDefinition when + Typer :: t(), + TypeDefinition :: argo_graphql_type_definition:t(), + FieldName :: argo_types:name(), + FieldDefinition :: argo_graphql_field_definition:t(). +get_field_definition( + _Typer = #argo_typer{service_document = ServiceDocument, options = Options}, + TypeDefinition = #argo_graphql_type_definition{}, + FieldName +) when is_binary(FieldName) -> + try argo_graphql_type_definition:get_field_definition(TypeDefinition, FieldName, ServiceDocument) of + FieldDefinition = #argo_graphql_field_definition{} -> + FieldDefinition + catch + error:badarg:Stacktrace -> + case maps:find(resolver, Options) of + {ok, Resolver} when is_atom(Resolver) -> + Result = argo_typer_resolver:find_field_definition( + Resolver, TypeDefinition, FieldName, ServiceDocument + ), + case Result of + {ok, FieldDefinition = #argo_graphql_field_definition{}} -> + FieldDefinition; + error -> + erlang:raise(error, badarg, Stacktrace) + end; + error -> + erlang:raise(error, badarg, Stacktrace) + end + end. + +-spec get_field_definition(Typer, SelectedTypeDefinition, FieldName, OptionTypeCondition) -> FieldDefinition when + Typer :: t(), + SelectedTypeDefinition :: argo_graphql_type_definition:t(), + FieldName :: argo_types:name(), + OptionTypeCondition :: argo_types:option(TypeCondition), + TypeCondition :: argo_types:name(), + FieldDefinition :: argo_graphql_field_definition:t(). +get_field_definition( + Typer = #argo_typer{}, SelectedTypeDefinition = #argo_graphql_type_definition{}, FieldName, none +) when is_binary(FieldName) -> + get_field_definition(Typer, SelectedTypeDefinition, FieldName); +get_field_definition( + Typer = #argo_typer{}, _SelectedTypeDefinition = #argo_graphql_type_definition{}, FieldName, {some, TypeCondition} +) when is_binary(FieldName) -> + TypeDefinition = get_type_definition(Typer, TypeCondition), + get_field_definition(Typer, TypeDefinition, FieldName). + +-spec get_fragment_definition(Typer, FragmentName) -> FragmentDefinition when + Typer :: t(), FragmentName :: argo_types:name(), FragmentDefinition :: argo_graphql_fragment_definition:t(). +get_fragment_definition(_Typer = #argo_typer{executable_document = ExecutableDocument}, FragmentName) when + is_binary(FragmentName) +-> + argo_graphql_executable_document:get_fragment_definition( + ExecutableDocument, FragmentName + ). + +-spec get_operation_definition(Typer, OptionOperationName) -> + {OptionOperationName, OperationDefinition} +when + Typer :: t(), + OptionOperationName :: argo_types:option(OperationName), + OperationName :: argo_types:name(), + OperationDefinition :: argo_graphql_operation_definition:t(). +get_operation_definition( + _Typer = #argo_typer{executable_document = ExecutableDocument}, + OptionOperationName +) when ?is_option_binary(OptionOperationName) -> + argo_graphql_executable_document:get_operation_definition(ExecutableDocument, OptionOperationName). + +-spec get_operation_type_definition(Typer, OperationDefinition) -> DataTypeDefinition when + Typer :: t(), + OperationDefinition :: argo_graphql_operation_definition:t(), + DataTypeDefinition :: argo_graphql_type_definition:t(). +get_operation_type_definition( + Typer = #argo_typer{service_document = ServiceDocument}, + _OperationDefinition = #argo_graphql_operation_definition{operation = Operation} +) -> + case Operation of + 'query' -> + case ServiceDocument of + #argo_graphql_service_document{'query' = none} -> + get_type_definition(Typer, <<"Query">>); + #argo_graphql_service_document{'query' = {some, QueryType}} -> + get_type_definition(Typer, QueryType) + end; + 'mutation' -> + case ServiceDocument of + #argo_graphql_service_document{'mutation' = none} -> + get_type_definition(Typer, <<"Mutation">>); + #argo_graphql_service_document{'mutation' = {some, MutationType}} -> + get_type_definition(Typer, MutationType) + end; + 'subscription' -> + case ServiceDocument of + #argo_graphql_service_document{'subscription' = none} -> + get_type_definition(Typer, <<"Subscription">>); + #argo_graphql_service_document{'subscription' = {some, SubscriptionType}} -> + get_type_definition(Typer, SubscriptionType) + end + end. + +-spec get_type_definition(Typer, TypeName) -> TypeDefinition when + Typer :: t(), + TypeName :: argo_types:name(), + TypeDefinition :: argo_graphql_type_definition:t(). +get_type_definition(_Typer = #argo_typer{service_document = ServiceDocument, options = Options}, TypeName) when + is_binary(TypeName) +-> + get_type_definition(ServiceDocument, TypeName, Options). + +%%%============================================================================= +%%% Public API functions %%%============================================================================= -spec derive_wire_type(ServiceDocument, ExecutableDocument, OptionOperationName) -> {OptionOperationName, WireType} when @@ -82,17 +282,12 @@ derive_wire_type( OptionOperationName1, Options ) when ?is_option_binary(OptionOperationName1) andalso is_map(Options) -> + Typer1 = new(ServiceDocument, ExecutableDocument, Options), % "data" Field - {OptionOperationName2, OperationDefinition} = argo_graphql_executable_document:get_operation_definition( - ExecutableDocument, OptionOperationName1 - ), - DataTypeDefinition = get_data_type_definition(ServiceDocument, OperationDefinition, Options), - DataWireType = collect_field_wire_types( - ServiceDocument, - ExecutableDocument, - DataTypeDefinition, - OperationDefinition#argo_graphql_operation_definition.selection_set, - Options + {OptionOperationName2, OperationDefinition} = get_operation_definition(Typer1, OptionOperationName1), + DataTypeDefinition = get_operation_type_definition(Typer1, OperationDefinition), + {_Typer2, DataWireType} = collect_field_wire_types( + Typer1, DataTypeDefinition, OperationDefinition#argo_graphql_operation_definition.selection_set ), NullableDataWireType = argo_wire_type:nullable(argo_nullable_wire_type:new(DataWireType)), DataFieldWireType = argo_field_wire_type:new(<<"data">>, NullableDataWireType, false), @@ -110,10 +305,15 @@ derive_wire_type( WireType = argo_wire_type:record(RecordWireType4), {OptionOperationName2, WireType}. --spec graphql_type_to_wire_type(ServiceDocument, Type) -> WireType when +-spec graphql_type_to_wire_type(Typer | ServiceDocument, Type) -> WireType when + Typer :: t(), ServiceDocument :: argo_graphql_service_document:t(), Type :: argo_graphql_type:t(), WireType :: argo_wire_type:t(). +graphql_type_to_wire_type( + _Typer = #argo_typer{service_document = ServiceDocument, options = Options}, Type = #argo_graphql_type{} +) -> + graphql_type_to_wire_type(ServiceDocument, Type, Options); graphql_type_to_wire_type(ServiceDocument = #argo_graphql_service_document{}, Type = #argo_graphql_type{}) -> graphql_type_to_wire_type(ServiceDocument, Type, #{}). @@ -308,44 +508,211 @@ format_error_description(_Key, Value) -> Value. %%%----------------------------------------------------------------------------- -%%% Internal functions +%%% Internal collect_field_wire_types functions %%%----------------------------------------------------------------------------- %% @private --spec get_data_type_definition(ServiceDocument, OperationDefinition, Options) -> DataTypeDefinition when - ServiceDocument :: argo_graphql_service_document:t(), - OperationDefinition :: argo_graphql_operation_definition:t(), - Options :: options(), - DataTypeDefinition :: argo_graphql_type_definition:t(). -get_data_type_definition( - ServiceDocument = #argo_graphql_service_document{}, - _OperationDefinition = #argo_graphql_operation_definition{operation = Operation}, - Options -) when is_map(Options) -> - case Operation of - 'query' -> - case ServiceDocument of - #argo_graphql_service_document{'query' = none} -> - get_type_definition(ServiceDocument, <<"Query">>, Options); - #argo_graphql_service_document{'query' = {some, QueryType}} -> - get_type_definition(ServiceDocument, QueryType, Options) - end; - 'mutation' -> - case ServiceDocument of - #argo_graphql_service_document{'mutation' = none} -> - get_type_definition(ServiceDocument, <<"Mutation">>, Options); - #argo_graphql_service_document{'mutation' = {some, MutationType}} -> - get_type_definition(ServiceDocument, MutationType, Options) - end; - 'subscription' -> - case ServiceDocument of - #argo_graphql_service_document{'subscription' = none} -> - get_type_definition(ServiceDocument, <<"Subscription">>, Options); - #argo_graphql_service_document{'subscription' = {some, SubscriptionType}} -> - get_type_definition(ServiceDocument, SubscriptionType, Options) +-spec collect_field_wire_types(State) -> {State, WireType} when + State :: collect_field_wire_types(), WireType :: argo_wire_type:t(). +collect_field_wire_types(State1 = #collect_field_wire_types{typer = Typer1, selection_set = SelectionSet}) -> + {Typer2, GroupedFields, _VisitedFragments} = collect_fields_static(Typer1, SelectionSet), + State2 = State1#collect_field_wire_types{typer = Typer2}, + GroupedFieldsIterator = argo_index_map:iterator(GroupedFields), + {State3, RecordWireType} = collect_field_wire_types__record_wire_type(State2, GroupedFieldsIterator), + WireType = argo_wire_type:record(RecordWireType), + {State3, WireType}. + +%% @private +-spec collect_field_wire_types__record_wire_type(State, GroupedFieldsIterator) -> {State, RecordWireType} when + State :: collect_field_wire_types(), + GroupedFieldsIterator :: argo_index_map:iterator(ResponseKey, SelectedFieldNodeList), + ResponseKey :: argo_types:name(), + SelectedFieldNodeList :: [SelectedFieldNode], + SelectedFieldNode :: selected_field_node(), + RecordWireType :: argo_record_wire_type:t(). +collect_field_wire_types__record_wire_type(State1, GroupedFieldsIterator1) -> + case argo_index_map:next(GroupedFieldsIterator1) of + none -> + collect_field_wire_types__group_overlapping(State1); + {_Index, _ResponseKey, SelectedFieldNodeList, GroupedFieldsIterator2} -> + {State2, FieldMap} = collect_field_wire_types__field_map(State1, SelectedFieldNodeList, []), + State3 = collect_field_wire_types__record_fields(State2, FieldMap), + collect_field_wire_types__record_wire_type(State3, GroupedFieldsIterator2) + end. + +%% @private +-spec collect_field_wire_types__field_map(State, SelectedFieldNodeList, FieldMap) -> {State, FieldMap} when + State :: collect_field_wire_types(), + SelectedFieldNodeList :: [SelectedFieldNode], + SelectedFieldNode :: selected_field_node(), + FieldMap :: field_map(). +collect_field_wire_types__field_map(State1, [], FieldMap1) -> + {State1, lists:reverse(FieldMap1)}; +collect_field_wire_types__field_map( + State1 = #collect_field_wire_types{ + typer = Typer1, selection_type_definition = SelectionTypeDefinition, exact_selections = ExactSelections1 + }, + [SelectedFieldNode = #selected_field_node{by = SelectedBy, field = Field} | SelectedFieldNodeList], + FieldMap1 +) -> + SelectionType = SelectionTypeDefinition#argo_graphql_type_definition.name, + FieldName = Field#argo_graphql_field.name, + ResponseKey = argo_graphql_field:get_response_key(Field), + OptionTypeCondition = + case SelectedBy of + #argo_graphql_field{} -> + none; + #argo_graphql_fragment_spread{name = FragmentName} -> + FragmentDefinition = get_fragment_definition(Typer1, FragmentName), + FragmentTypeCondition = FragmentDefinition#argo_graphql_fragment_definition.type_condition, + {some, FragmentTypeCondition}; + #argo_graphql_inline_fragment{type_condition = OptionInlineFragmentTypeCondition} -> + OptionInlineFragmentTypeCondition + end, + ExactSelection = + case OptionTypeCondition of + none -> + true; + {some, SelectionType} -> + true; + {some, _TypeCondition} -> + false + end, + ExactSelections2 = + case ExactSelection of + false -> + ExactSelections1; + true -> + sets:add_element(ResponseKey, ExactSelections1) + end, + FieldDefinition = get_field_definition(Typer1, SelectionTypeDefinition, FieldName, OptionTypeCondition), + FieldMap2 = [#field_map{node = SelectedFieldNode, definition = FieldDefinition} | FieldMap1], + State2 = State1#collect_field_wire_types{exact_selections = ExactSelections2}, + collect_field_wire_types__field_map(State2, SelectedFieldNodeList, FieldMap2). + +%% @private +-spec collect_field_wire_types__record_fields(State, FieldMap) -> State when + State :: collect_field_wire_types(), + FieldMap :: field_map(). +collect_field_wire_types__record_fields(State1 = #collect_field_wire_types{}, []) -> + State1; +collect_field_wire_types__record_fields( + State1 = #collect_field_wire_types{typer = Typer1, exact_selections = ExactSelections}, [ + #field_map{node = #selected_field_node{by = SelectedBy, field = Field}, definition = FieldDefinition} | FieldMap + ] +) -> + ResponseKey = argo_graphql_field:get_response_key(Field), + Omittable = + (not sets:is_element(ResponseKey, ExactSelections)) orelse + maybe_omit_selection(Field) orelse + maybe_omit_selection(SelectedBy), + FieldSelectionSet = Field#argo_graphql_field.selection_set, + FieldType = FieldDefinition#argo_graphql_field_definition.type, + {State2, FieldWireType} = + case length(FieldSelectionSet#argo_graphql_selection_set.selections) of + 0 -> + WireType = graphql_type_to_wire_type(Typer1, FieldType), + {State1, argo_field_wire_type:new(ResponseKey, WireType, Omittable)}; + _ -> + State1_1 = State1, + Typer1_1 = Typer1, + FieldTypeName = argo_graphql_type:get_type_name(FieldType), + FieldTypeDefinition = get_type_definition(Typer1_1, FieldTypeName), + {Typer1_2, UnwrappedWireType} = collect_field_wire_types( + Typer1_1, FieldTypeDefinition, FieldSelectionSet + ), + State1_2 = State1_1#collect_field_wire_types{typer = Typer1_2}, + WireType = wrap_wire_type(FieldType, UnwrappedWireType), + {State1_2, argo_field_wire_type:new(ResponseKey, WireType, Omittable)} + end, + State3 = collect_field_wire_types__record_fields_push(State2, FieldWireType), + collect_field_wire_types__record_fields(State3, FieldMap). + +%% @private +-spec collect_field_wire_types__record_fields_push(State, FieldWireType) -> State when + State :: collect_field_wire_types(), + FieldWireType :: argo_field_wire_type:t(). +collect_field_wire_types__record_fields_push( + State1 = #collect_field_wire_types{record_fields = RecordFields1}, FieldWireType = #argo_field_wire_type{} +) -> + RecordFields2 = queue:in(FieldWireType, RecordFields1), + State2 = State1#collect_field_wire_types{record_fields = RecordFields2}, + State2. + +%% @private +-spec collect_field_wire_types__group_overlapping(State) -> {State, RecordWireType} when + State :: collect_field_wire_types(), + RecordWireType :: argo_record_wire_type:t(). +collect_field_wire_types__group_overlapping(State1 = #collect_field_wire_types{record_fields = RecordFields}) -> + % if we have overlapping selections, merge them into a canonical order + GroupedFields = argo_index_map:groups_from_list( + fun(#argo_field_wire_type{name = Name}) -> Name end, queue:to_list(RecordFields) + ), + GroupedFieldsIterator = argo_index_map:iterator(GroupedFields), + RecordWireType1 = argo_record_wire_type:new(), + {State2, RecordWireType2} = collect_field_wire_types__group_overlapping( + State1, GroupedFieldsIterator, RecordWireType1 + ), + {State2, RecordWireType2}. + +%% @private +-spec collect_field_wire_types__group_overlapping(State, GroupedFieldsIterator, RecordWireType) -> + {State, RecordWireType} +when + State :: collect_field_wire_types(), + GroupedFieldsIterator :: argo_index_map:iterator(ResponseKey, FieldWireTypeList), + ResponseKey :: argo_types:name(), + FieldWireTypeList :: [FieldWireType], + FieldWireType :: argo_field_wire_type:t(), + RecordWireType :: argo_record_wire_type:t(). +collect_field_wire_types__group_overlapping( + State1 = #collect_field_wire_types{}, GroupedFieldsIterator1, RecordWireType1 +) -> + case argo_index_map:next(GroupedFieldsIterator1) of + none -> + {State1, RecordWireType1}; + {_Index, _ResponseKey, [FieldWireType], GroupedFieldsIterator2} -> + RecordWireType2 = argo_record_wire_type:insert(RecordWireType1, FieldWireType), + collect_field_wire_types__group_overlapping(State1, GroupedFieldsIterator2, RecordWireType2); + {_Index, ResponseKey, [FieldWireTypeA | FieldWireTypeBs], GroupedFieldsIterator2} -> + try collect_field_wire_types__merge_field_wire_type(State1, FieldWireTypeA, FieldWireTypeBs) of + {State2 = #collect_field_wire_types{}, FieldWireTypeC = #argo_field_wire_type{}} -> + RecordWireType2 = argo_record_wire_type:insert(RecordWireType1, FieldWireTypeC), + collect_field_wire_types__group_overlapping(State2, GroupedFieldsIterator2, RecordWireType2) + catch + throw:{badshape, BadShapePath, _A, _B} -> + error_with_info( + badarg, + [State1, GroupedFieldsIterator1, RecordWireType1], + #{ + 3 => + {field_selection_type_shape_mismatch, #{ + field_alias => ResponseKey, path => BadShapePath + }} + } + ) end end. +%% @private +-spec collect_field_wire_types__merge_field_wire_type(State, FieldWireTypeA, FieldWireTypeBs) -> + {State, FieldWireTypeC} +when + State :: collect_field_wire_types(), + FieldWireTypeA :: argo_field_wire_type:t(), + FieldWireTypeBs :: [FieldWireTypeB], + FieldWireTypeB :: argo_field_wire_type:t(), + FieldWireTypeC :: argo_field_wire_type:t(). +collect_field_wire_types__merge_field_wire_type(State1, FieldWireTypeC, []) -> + {State1, FieldWireTypeC}; +collect_field_wire_types__merge_field_wire_type(State1, FieldWireTypeA, [FieldWireTypeB | FieldWireTypeBs]) -> + FieldWireTypeC = merge_field_wire_type(argo_path_value:new(), FieldWireTypeA, FieldWireTypeB), + collect_field_wire_types__merge_field_wire_type(State1, FieldWireTypeC, FieldWireTypeBs). + +%%%----------------------------------------------------------------------------- +%%% Internal functions +%%%----------------------------------------------------------------------------- + %% @private -spec collect_fields_static(ServiceDocument, ExecutableDocument, SelectionSet, VisitedFragments) -> {GroupedFields, VisitedFragments} @@ -505,133 +872,6 @@ collect_fields_static(ServiceDocument, ExecutableDocument, [Selection | Selectio collect_fields_static(_ServiceDocument, _ExecutableDocument, [], VisitedFragments1, GroupedFields1) -> {GroupedFields1, VisitedFragments1}. -%% @private --spec collect_field_wire_types(ServiceDocument, ExecutableDocument, SelectionTypeDefinition, SelectionSet, Options) -> - WireType -when - ServiceDocument :: argo_graphql_service_document:t(), - ExecutableDocument :: argo_graphql_executable_document:t(), - SelectionTypeDefinition :: argo_graphql_type_definition:t(), - SelectionSet :: argo_graphql_selection_set:t(), - Options :: options(), - WireType :: argo_wire_type:t(). -collect_field_wire_types(ServiceDocument, ExecutableDocument, SelectionTypeDefinition, SelectionSet, Options) -> - RecordWireType1 = argo_record_wire_type:new(), - VisitedFragments1 = sets:new([{version, 2}]), - {GroupedFields1, _VisitedFragments2} = collect_fields_static( - ServiceDocument, ExecutableDocument, SelectionSet, VisitedFragments1 - ), - RecordWireType2 = argo_index_map:foldl( - fun(_Index, FieldAlias, Fields, RecordWireType1_Acc1) -> - RecordWireType1_Acc2 = - lists:foldl( - fun(Selected, RecordWireType1_Acc1_Acc1) -> - {Omittable1, OptionTypeCondition} = - case Selected#selected_field_node.by of - Field = #argo_graphql_field{} -> - case maybe_omit_selection(Field#argo_graphql_field.directives) of - false -> - {false, none}; - true -> - {true, none} - end; - FragmentSpread = #argo_graphql_fragment_spread{name = FragmentName} -> - FragmentSpreadOmittable = maybe_omit_selection( - FragmentSpread#argo_graphql_fragment_spread.directives - ), - FragmentDefinition = argo_graphql_executable_document:get_fragment_definition( - ExecutableDocument, FragmentName - ), - {FragmentSpreadOmittable, - {some, FragmentDefinition#argo_graphql_fragment_definition.type_condition}}; - InlineFragment = #argo_graphql_inline_fragment{} -> - InlineFragmentOmittable = maybe_omit_selection( - InlineFragment#argo_graphql_inline_fragment.directives - ), - {InlineFragmentOmittable, - InlineFragment#argo_graphql_inline_fragment.type_condition} - end, - {Omittable2, TypeConditionDefinition} = - case OptionTypeCondition of - none -> - {Omittable1, SelectionTypeDefinition}; - {some, TypeCondition} -> - case SelectionTypeDefinition#argo_graphql_type_definition.name of - TypeCondition -> - {Omittable1, SelectionTypeDefinition}; - _ -> - {true, get_type_definition(ServiceDocument, TypeCondition, Options)} - end - end, - Omittable3 = - Omittable2 orelse - maybe_omit_selection(Selected#selected_field_node.field#argo_graphql_field.directives), - FieldName = Selected#selected_field_node.field#argo_graphql_field.name, - FieldDefinition = get_field_definition( - TypeConditionDefinition, FieldName, ServiceDocument, Options - ), - FieldSelectionSet = Selected#selected_field_node.field#argo_graphql_field.selection_set, - FieldType = FieldDefinition#argo_graphql_field_definition.type, - FieldWireType = - case length(FieldSelectionSet#argo_graphql_selection_set.selections) of - 0 -> - WireType = graphql_type_to_wire_type(ServiceDocument, FieldType, Options), - argo_field_wire_type:new(FieldAlias, WireType, Omittable3); - _ -> - FieldTypeName = argo_graphql_type:get_type_name(FieldType), - FieldTypeDefinition = get_type_definition(ServiceDocument, FieldTypeName, Options), - WireType = wrap_wire_type( - FieldType, - collect_field_wire_types( - ServiceDocument, - ExecutableDocument, - FieldTypeDefinition, - FieldSelectionSet, - Options - ) - ), - argo_field_wire_type:new(FieldAlias, WireType, Omittable3) - end, - RecordWireType1_Acc1_Acc2 = - case argo_record_wire_type:find(RecordWireType1_Acc1_Acc1, FieldAlias) of - {ok, FieldWireType} -> - RecordWireType1_Acc1_Acc1; - {ok, ExistingFieldWireType} -> - try - merge_field_wire_type( - argo_path_value:new(), ExistingFieldWireType, FieldWireType - ) - of - MergedFieldWireType = #argo_field_wire_type{} -> - argo_record_wire_type:update(RecordWireType1_Acc1_Acc1, MergedFieldWireType) - catch - throw:{badshape, BadShapePath, _A, _B} -> - error_with_info( - badarg, - [ServiceDocument, SelectionTypeDefinition, SelectionSet], - #{ - 3 => - {field_selection_type_shape_mismatch, #{ - field_alias => FieldAlias, path => BadShapePath - }} - } - ) - end; - error -> - argo_record_wire_type:insert(RecordWireType1_Acc1_Acc1, FieldWireType) - end, - RecordWireType1_Acc1_Acc2 - end, - RecordWireType1_Acc1, - Fields - ), - RecordWireType1_Acc2 - end, - RecordWireType1, - GroupedFields1 - ), - argo_wire_type:record(RecordWireType2). - %% @private -spec merge_field_wire_type(Path, A, B) -> C when Path :: argo_path_value:t(), @@ -769,7 +1009,18 @@ always_skip_selection_filter(#argo_graphql_directive{}) -> false. %% @private --spec maybe_omit_selection(Directives) -> boolean() when Directives :: argo_graphql_directives:t(). +-spec maybe_omit_selection(Node) -> boolean() when + Node :: + argo_graphql_field:t() + | argo_graphql_fragment_spread:t() + | argo_graphql_inline_fragment:t() + | argo_graphql_directives:t(). +maybe_omit_selection(#argo_graphql_field{directives = Directives}) -> + maybe_omit_selection(Directives); +maybe_omit_selection(#argo_graphql_fragment_spread{directives = Directives}) -> + maybe_omit_selection(Directives); +maybe_omit_selection(#argo_graphql_inline_fragment{directives = Directives}) -> + maybe_omit_selection(Directives); maybe_omit_selection(#argo_graphql_directives{directives = Directives}) -> lists:any(fun maybe_omit_selection_filter/1, Directives). @@ -904,49 +1155,13 @@ get_argo_deduplicate_directive_value(#argo_graphql_type_definition{ get_argo_deduplicate_directive_value(#argo_graphql_type_definition{}) -> none. -%% @private --spec get_field_definition(TypeDefinition, FieldName, ServiceDocument, Options) -> FieldDefinition when - TypeDefinition :: argo_graphql_type_definition:t(), - FieldName :: argo_types:name(), - ServiceDocument :: argo_graphql_service_document:t(), - Options :: options(), - FieldDefinition :: argo_graphql_field_definition:t(). -get_field_definition( - TypeDefinition = #argo_graphql_type_definition{}, - FieldName, - ServiceDocument = #argo_graphql_service_document{}, - Options -) when is_binary(FieldName) andalso is_map(Options) -> - try argo_graphql_type_definition:get_field_definition(TypeDefinition, FieldName, ServiceDocument) of - FieldDefinition = #argo_graphql_field_definition{} -> - FieldDefinition - catch - error:badarg:Stacktrace -> - case maps:find(resolver, Options) of - {ok, Resolver} when is_atom(Resolver) -> - Result = argo_typer_resolver:find_field_definition( - Resolver, TypeDefinition, FieldName, ServiceDocument - ), - case Result of - {ok, FieldDefinition = #argo_graphql_field_definition{}} -> - FieldDefinition; - error -> - erlang:raise(error, badarg, Stacktrace) - end; - error -> - erlang:raise(error, badarg, Stacktrace) - end - end. - %% @private -spec get_type_definition(ServiceDocument, TypeName, Options) -> TypeDefinition when ServiceDocument :: argo_graphql_service_document:t(), TypeName :: argo_types:name(), Options :: options(), TypeDefinition :: argo_graphql_type_definition:t(). -get_type_definition(ServiceDocument = #argo_graphql_service_document{}, TypeName, Options) when - is_binary(TypeName) andalso is_map(Options) --> +get_type_definition(ServiceDocument, TypeName, Options) -> try argo_graphql_service_document:get_type_definition(ServiceDocument, TypeName) of TypeDefinition = #argo_graphql_type_definition{} -> TypeDefinition diff --git a/apps/argo/src/graphql/argo_graphql_field.erl b/apps/argo/src/graphql/argo_graphql_field.erl index 35a298b..b269dbc 100644 --- a/apps/argo/src/graphql/argo_graphql_field.erl +++ b/apps/argo/src/graphql/argo_graphql_field.erl @@ -34,6 +34,7 @@ add_selection/2, find_field/3, fold_fields/4, + get_response_key/1, get_shape/2, set_alias/2 ]). @@ -160,6 +161,12 @@ fold_fields( ) when is_function(Fun, 3) -> argo_graphql_selection_set:fold_fields(SelectionSet, AccIn, Fun, ExecutableDocument). +-spec get_response_key(Field) -> ResponseKey when Field :: t(), ResponseKey :: argo_types:name(). +get_response_key(#argo_graphql_field{'alias' = none, name = FieldName}) when is_binary(FieldName) -> + FieldName; +get_response_key(#argo_graphql_field{'alias' = {some, FieldAlias}}) when is_binary(FieldAlias) -> + FieldAlias. + -spec get_shape(Field, ExecutableDocument) -> Shape when Field :: t(), ExecutableDocument :: argo_graphql_executable_document:t(), diff --git a/apps/argo_test/test/argo_typer_SUITE.erl b/apps/argo_test/test/argo_typer_SUITE.erl index 6b6054a..b2be5ce 100644 --- a/apps/argo_test/test/argo_typer_SUITE.erl +++ b/apps/argo_test/test/argo_typer_SUITE.erl @@ -266,7 +266,7 @@ test_issue_19_field_selection_merging(Config) -> " data: {\n" " root: {\n" " __typename: STRING\n" - " required?: {\n" + " required: {\n" " __typename: STRING\n" " object?: STRING\n" " otherObject?: STRING\n"