├── .gitignore ├── .travis.yml ├── README.md ├── include └── types.hrl ├── rebar.config ├── rebar.lock └── src ├── graphql.app.src ├── graphql.erl ├── graphql_execution.erl ├── graphql_introspection.erl ├── graphql_parser ├── graphql_lexer.xrl ├── graphql_parser.erl ├── graphql_parser_test.erl └── graphql_parser_yecc.yrl ├── test ├── graphql_context_test.erl ├── graphql_introspection_test.erl ├── graphql_mutation_test.erl ├── graphql_old_test.erl ├── graphql_old_test_schema.erl └── graphql_resolver_test.erl └── types ├── graphql_type.erl ├── graphql_type_boolean.erl ├── graphql_type_enum.erl ├── graphql_type_enum_value.erl ├── graphql_type_float.erl ├── graphql_type_int.erl ├── graphql_type_list.erl ├── graphql_type_non_null.erl ├── graphql_type_object.erl ├── graphql_type_schema.erl ├── graphql_type_string.erl └── graphql_type_union.erl /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | ebin 3 | .rebar3 4 | src/graphql_parser/graphql_lexer.erl 5 | src/graphql_parser/graphql_parser_yecc.erl 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | sudo: false 3 | 4 | install: 5 | - wget https://s3.amazonaws.com/rebar3/rebar3 && chmod +x rebar3 6 | 7 | otp_release: 8 | - 18.0 9 | 10 | script: 11 | # - ./rebar3 dialyzer 12 | - ./rebar3 eunit 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Erlang GraphQL [![Build Status](https://travis-ci.org/graphql-erlang/graphql.svg?branch=master)](https://travis-ci.org/graphql-erlang/graphql) 2 | 3 | An Erlang implementation of Facebook's GraphQL. 4 | 5 | This is the core GraphQL query parsing and execution engine which goal is to be transport, server and datastore agnostic. 6 | 7 | 8 | ## Status 9 | 10 | This is a work in progress, here's todo list: 11 | 12 | - [X] Parser for GraphQL 13 | - [X] Schema definition 14 | - [X] Query execution 15 | - [X] Pre-defined basic scalar types 16 | - [X] Complex types (List, Object, Union, etc) 17 | - [X] Arguments 18 | - [X] Variables 19 | - [X] Fragments and inline fragments in queries 20 | - [X] Custom types 21 | - [ ] Parallel execution 22 | - [ ] Return maps option 23 | - [X] Mutations 24 | - [ ] Subscriptions 25 | - [X] Introspection 26 | - [ ] Directives 27 | - [ ] "Compile" and validate schema before start the app 28 | - [ ] GraphQL Guard (may be another app) 29 | - [ ] Calculate query/mutation complexity 30 | - [ ] White list of queries with big complexity 31 | 32 | ## Installation 33 | 34 | Rebar3 - hex package 35 | ```erlang 36 | {deps, [ 37 | {graphql, "0.2.10", {pkg, graphql_erlang}} 38 | ]}. 39 | ``` 40 | 41 | Rebar - git with version tag 42 | ```erlang 43 | {deps, [ 44 | {graphql, "", {git, "https://github.com/graphql-erlang/graphql.git", {tag, "v0.2.10"}}} 45 | ]] 46 | ``` 47 | 48 | ## Get started 49 | 50 | First - define you schema: 51 | ```erlang 52 | -module(graphql_example). 53 | -export([schema/0]). 54 | -include_lib("graphql/include/types.hrl"). 55 | 56 | schema() -> ?SCHEMA(#{ 57 | query => query() 58 | }). 59 | 60 | query() -> ?OBJECT("QueryRoot", "Example Query", #{ 61 | "greatings" => ?FIELD(?STRING, "Greating", #{ 62 | "name" => ?ARG(?NON_NULL(?STRING) "The name of who you'd like to great") 63 | }, 64 | fun(_ParrentObject, Args, _Context) -> 65 | maps:get(<<"name">>, Args) 66 | end 67 | ) 68 | }). 69 | ``` 70 | 71 | In handler - call query for schema with optional options: 72 | ```erlang 73 | Document = <<"query($name: String!) { greatings(name: $name) }">>, % or list 74 | Context = #{}, % you can pass request through context or whatever you want use in resolver acress the schema 75 | InitValue = #{}, % root value, passed to query/mutation root resolver 76 | VariableValues = #{ % variables passed with query 77 | <<"name">> => <<"world">> 78 | }, 79 | 80 | Result = graphql:execute(graphql_example:schema(), Document, InitValue, Context), 81 | 82 | io:format("Result: ~p~n", [Result]). % #{data => [{<<"name">>, <<"world">>}]} 83 | ``` 84 | 85 | ## Types include 86 | 87 | We provide macros for simple types definition. Include: `-include_lib("graphql/include/types.hrl").` 88 | 89 | All values for types can be `null` except `?NON_NULL` and ObjectType 90 | 91 | #### Scalars 92 | `?BOOLEAN` - boolean type. Represent atoms true/false. 93 | 94 | `?FLOAT` and `?INT` - represent float and integer. 95 | 96 | `?LIST(OfType)` - represent list where `OfType` - inner list type. Example: `?LIST(?INT)` - `[Int]` 97 | 98 | `?NON_NULL(OfType)` - non nullable type - wrapper over nullable type. Example: `?NON_NULL(?LIST(?INT))` - `[Int]!` 99 | 100 | `?STRING` - binary string 101 | 102 | #### Complex types 103 | 104 | `?ENUM(Name, Description, EnumValues)` - enumaration: 105 | ```erlang 106 | my_enum() -> ?ENUM("MyEnum", "MyEnumDescription", [ 107 | % ?ENUM_VAL(InternalValue, EnumValue, Description, Options) % Options - optional 108 | ?ENUM_VAL(1, "ONE", "REPRESENT 1"), 109 | ?DEPRECATED("Deprecation reason", ?ENUM_VAL(2, "TWO", "Deprecated exempla")) 110 | ]). 111 | ``` 112 | 113 | #### Deal with Object type: 114 | 115 | `?DEPRECATED(Reason, FieldOrEnumVal)` - mark whole field or enum value deprecated 116 | 117 | `?OBJECT(Name, Fields)` or `?OBJECT(Name, Description, Fields)` - declare object type. Name and Description can be list - it automatically converts to binary. Fields should be map. 118 | 119 | `?FIELD(Type)` or `?FIELD(Type, Description)` or `?FIELD(Type, Description, Resolver)` or `?FIELD(Type, Description, Args, Resolver)` 120 | 121 | `?ARG(Type)` or `?ARG(Type, Description)` or `?ARG(Type, DefaultValue, Description)` - define argument item 122 | 123 | 124 | The most basic components of a GraphQL schema are object types, which just represent a kind 125 | of object you can fetch from your service, and what fields it has. 126 | 127 | Creating object type example: 128 | ```erlang 129 | ?OBJECT("Name", "Description", #{ 130 | "field_name" => ?DEPRECATED("Human redable deprecation reason", ?FIELD( 131 | ?STRING, % what type of value returned by resolver 132 | "Human redable description", % optional - can be null 133 | #{ % map of field arguments 134 | "arg_name">> => ?ARG(?STRING, <<"Default value">>, "Optional description") 135 | } 136 | fun() -> <<"resolved string">> end % resolver function 137 | )) 138 | }). 139 | ``` 140 | 141 | Resolver - function that resolves field value. Arity and arguments passing: 142 | ```erlang 143 | case erlang:fun_info(Resolver, arity) of 144 | {arity, 0} -> Resolver(); 145 | {arity, 1} -> Resolver(ObjectValue); 146 | {arity, 2} -> Resolver(ObjectValue, ArgumentValues); 147 | {arity, 3} -> Resolver(ObjectValue, ArgumentValues, Context) 148 | end. 149 | ``` 150 | 151 | where: 152 | 153 | - `ObjectValue` - result of the parent resolver (or InitialValue for query root) 154 | - `ArgumentsValues` - map of arguments. When argument not provided in query - value is 'null' 155 | - `Context` - context from parent resolver. 156 | 157 | Resolver can return resolved value witch passed to scalar serialize function if type is scalar or as ObjectValue 158 | for each field in child object type 159 | 160 | Resolver can also overwrite context for his childrens. In this case resolver may look like: 161 | ```erlang 162 | resolver => fun(_,_,Context) -> 163 | {overvrite_context, 164 | <<"resolved string">>, 165 | Context#{ 166 | additional_context_data => <<"Data">> 167 | } 168 | } 169 | end 170 | ``` 171 | 172 | NOTE: additional_context_data avaliable only for children object type in query 173 | 174 | #### Deal with Union type 175 | 176 | `?UNION(Name, Description, PossibleTypes, ResolveTypeFun)` - type can be one of the listed in PossibleTypes. PossibleTypes must be list of ObjectTypes. 177 | 178 | Example: 179 | 180 | Type definitions: 181 | ```erlang 182 | one() -> ?OBJECT("One", "Has field named one", #{ 183 | "one" => ?ARG(?INT) 184 | }). 185 | 186 | two() -> ?OBJECT("Two", "Has field named two", #{ 187 | "two" => ?ARG(?INT) 188 | }). 189 | 190 | union_default() -> ?UNION("UnionDefaultResolve", "Used default type resolver", [ 191 | fun one/0, 192 | fun two/0 193 | ]). 194 | 195 | union_custom() -> ?UNION("UnionCastomResolve", "Used custom type resolver", [ 196 | fun one/0, 197 | fun two/0 198 | ], fun(Value, _ThisTypeDefinition)-> 199 | Type = case maps:get(<<"one">>, Value, undefined) of 200 | undefined -> fun two/0; 201 | _ -> fun one/0 202 | end, 203 | {Type, Value} % resolver should return tuple {ResolvedType, UnwrappedValue} 204 | end). 205 | ``` 206 | 207 | Object definition: 208 | ```erlang 209 | test_unions() -> ?OBJECT("TestUnions", "", #{ 210 | "default_resolver" => ?FIELD(?LIST(fun unions_default/0), "Default resolver example", 211 | % default resolver just compares first elem in tuple with possibleTypes in union 212 | fun() -> 213 | [ 214 | {fun one/0, #{<<"one">> => 1}}, 215 | {fun two/0, #{<<"two">> => 2}}, 216 | {fun one/0, #{<<"one">> => 3}} 217 | ] 218 | end 219 | ), 220 | 221 | "custom_resolver" => ?FIELD(?LIST(fun unions_custom/0), "Custom resolver example", 222 | fun()-> 223 | [ 224 | #{<<"one">> => 1}, 225 | #{<<"two">> => 2}, 226 | #{<<"one">> => 3} 227 | ] 228 | end 229 | ) 230 | }). 231 | ``` 232 | 233 | ## Custom scalar types 234 | 235 | ```erlang 236 | custom_type()-> #{ 237 | kind => 'SCALAR', % required kind for scalar type 238 | name => 'IntCustom', % atom 239 | ofType => null, % used for nested data types 240 | description => << 241 | "The `Int` scalar type represents non-fractional signed whole numeric ", 242 | "values. Int can represent values between -(2^53 - 1) and 2^53 - 1 since ", 243 | "represented in JSON as double-precision floating point numbers specified ", 244 | "by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point)." 245 | >>, 246 | 247 | % this function serialize resolved value 248 | % Serialize(Result, FieldType, Context); 249 | serialize => fun serialize/3, 250 | 251 | % this function parse variable value 252 | % ParseValue(maps:get(VariableName, VariableValues, null), ArgumentType); 253 | parse_value => fun parse_value/2, 254 | 255 | % this function parse value defined in query 256 | % ParseLiteral(Value, ArgumentType) 257 | parse_literal => fun parse_literal/2 258 | }. 259 | ``` 260 | 261 | ## Special thanks 262 | 263 | Thanks graphql-elixir for great lex/yecc erlang parser :) -------------------------------------------------------------------------------- /include/types.hrl: -------------------------------------------------------------------------------- 1 | 2 | -define(BOOLEAN, fun graphql_type_boolean:type/0). 3 | -define(STRING, fun graphql_type_string:type/0). 4 | -define(INT, fun graphql_type_int:type/0). 5 | -define(FLOAT, fun graphql_type_float:type/0). 6 | -define(LIST(OfType), graphql_type_list:type(OfType)). 7 | -define(NON_NULL(OfType), graphql_type_non_null:type(OfType)). 8 | 9 | -define(ENUM(Name, Description, Values), graphql_type_enum:type(Name, Description, Values)). 10 | -define(ENUM_VAL(Val, Name, Description), graphql_type_enum_value:type(Val, Name, Description)). 11 | -define(ENUM_VAL(Val, Name, Description, O), graphql_type_enum_value:type(Val, Name, Description, O)). 12 | 13 | -define(UNION(Name, Description, PossibleTypes), graphql_type_union:type(Name, Description, PossibleTypes)). 14 | -define(UNION(Name, Description, PossibleTypes, ResolveType), graphql_type_union:type(Name, Description, PossibleTypes, ResolveType)). 15 | 16 | -define(SCHEMA(Schema), graphql_type_schema:new(Schema)). 17 | 18 | -define(OBJECT(Name, Fields), graphql_type_object:type(Name, Fields)). 19 | -define(OBJECT(Name, Description, Fields), graphql_type_object:type(Name, Description, Fields)). 20 | 21 | -define(FIELD(Type), graphql_type_object:field(Type)). 22 | -define(FIELD(Type, Description), graphql_type_object:field(Type, Description)). 23 | -define(FIELD(Type, Description, Resolver), graphql_type_object:field(Type, Description, Resolver)). 24 | -define(FIELD(Type, Description, Args, Resolver), graphql_type_object:field(Type, Description, Args, Resolver)). 25 | 26 | -define(ARG(Type), graphql_type_object:arg(Type)). 27 | -define(ARG(Type, Description), graphql_type_object:arg(Type, Description)). 28 | -define(ARG(Type, DefaultValue, Description), graphql_type_object:arg(Type, DefaultValue, Description)). 29 | 30 | -define(DEPRECATED(Reason, FieldOrEnumVal), graphql_type:deprecated(Reason, FieldOrEnumVal)). 31 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-erlang/graphql/feef75d955e3404c3c8e4500b1e2c4d4821c1dc6/rebar.config -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. -------------------------------------------------------------------------------- /src/graphql.app.src: -------------------------------------------------------------------------------- 1 | {application, graphql, 2 | [{description, "GraphQL Erlang Implementation"}, 3 | {vsn, "0.2.10"}, 4 | {pkg_name, "graphql_erlang"}, 5 | {maintainers, [" Ruslan Cheshko", "Alexandr Inkov"]}, 6 | {links,[{"Github","https://github.com/graphql-erlang/graphql"}]}, 7 | {licenses,["MIT/X11"]}, 8 | {registered, []}, 9 | {applications, 10 | [ 11 | kernel, 12 | stdlib 13 | ]}, 14 | {env,[]}, 15 | {modules, [ 16 | graphql 17 | ]}, 18 | 19 | {maintainers, []}, 20 | {licenses, []}, 21 | {links, []} 22 | ]}. 23 | -------------------------------------------------------------------------------- /src/graphql.erl: -------------------------------------------------------------------------------- 1 | -module(graphql). 2 | -include("types.hrl"). 3 | 4 | %% API 5 | -export([ 6 | 7 | % back capability 8 | schema/1, 9 | objectType/2, objectType/3, 10 | 11 | 12 | % execution 13 | exec/2, exec/3, 14 | execute/3, execute/4, execute/5, execute/6, % deprecated, use exec instead 15 | 16 | % helpers 17 | upmap/3, pmap/3 % not used now 18 | ]). 19 | 20 | %%%% for macros haters %%%% 21 | 22 | schema(Schema) -> ?SCHEMA(Schema). 23 | 24 | objectType(Name, Fields) -> ?OBJECT(Name, Fields). 25 | objectType(Name, Description, Fields)-> ?OBJECT(Name, Description, Fields). 26 | 27 | %%%% execution %%%% 28 | exec(Schema, Document) -> exec(Schema, Document, #{}). 29 | exec(Schema, Document, Options)-> 30 | InitialValue = maps:get(initial, Options, #{}), 31 | VariableValues = maps:get(variable_values, Options, #{}), 32 | OperationName = maps:get(operation_name, Options, null), 33 | Context = maps:get(context, Options, #{}), 34 | ReturnMaps = maps:get(return_maps, Options, true), 35 | 36 | case graphql_parser:parse(Document) of 37 | {ok, DocumentParsed} -> 38 | case graphql_execution:execute(Schema, DocumentParsed, OperationName, VariableValues, InitialValue, Context) of 39 | #{data := [{_,_}|_] = Proplist} -> 40 | case ReturnMaps of 41 | true -> #{data => to_map_recursive(Proplist)} 42 | end; 43 | #{errors := _} = Resp -> Resp 44 | end; 45 | 46 | {error, {Line, graphql_parser_yecc, Reason}} -> {#{errors => [#{ 47 | line => Line, 48 | message => Reason 49 | }]}, Context} 50 | end. 51 | 52 | % execute is deprecated 53 | execute(Schema, Document, InitialValue)-> 54 | execute(Schema, Document, null, #{}, InitialValue, #{}). 55 | execute(Schema, Document, InitialValue, Context)-> 56 | execute(Schema, Document, null, #{}, InitialValue, Context). 57 | execute(Schema, Document, VariableValues, InitialValue, Context)-> 58 | execute(Schema, Document, null, VariableValues, InitialValue, Context). 59 | execute(Schema, Document, OperationName, VariableValues, InitialValue, Context)-> 60 | case graphql_parser:parse(Document) of 61 | {ok, DocumentParsed} -> 62 | case graphql_execution:execute(Schema, DocumentParsed, OperationName, VariableValues, InitialValue, Context) of 63 | #{data := _} = Res -> Res#{ errors => []}; 64 | Res -> Res 65 | end; 66 | {error, {_, _, Error}} -> 67 | #{error => Error} 68 | end. 69 | 70 | 71 | %%%% helpers %%%% 72 | 73 | -spec upmap(fun(), list(), integer()) -> list(). 74 | upmap(F, L, Timeout) -> 75 | Parent = self(), 76 | Ref = make_ref(), 77 | [receive {Ref, Result} -> Result after Timeout -> throw(timeout) end 78 | || _ <- [spawn(fun () -> Parent ! {Ref, F(X)} end) || X <- L]]. 79 | 80 | -spec pmap(fun(), list(), integer()) -> list(). 81 | pmap(F, L, Timeout) -> 82 | Parent = self(), 83 | L2 = lists:map(fun(El) -> 84 | Ref = make_ref(), 85 | spawn(fun() -> Parent ! {Ref, F(El)} end), 86 | Ref 87 | end, L), 88 | 89 | [receive {Ref, Result} -> Result after Timeout -> throw(timeout) end 90 | || Ref <- L2]. 91 | 92 | %%%------------------------------------------------------------------- 93 | %% @doc 94 | %% Convert proplist to map recursively 95 | %% @end 96 | %%%------------------------------------------------------------------- 97 | -spec to_map_recursive(list()) -> maps:map(). 98 | to_map_recursive(Proplist) -> 99 | recursive_apply(fun maps:from_list/1, Proplist). 100 | 101 | 102 | %%%------------------------------------------------------------------- 103 | %% @doc 104 | %% Recursively apply Fun to proplist (or list of proplists) 105 | %% @end 106 | %%%------------------------------------------------------------------- 107 | -spec recursive_apply(fun((term()) -> term()), list()) -> term(). 108 | recursive_apply(Fun, [{_,_}|_] = Proplist) -> 109 | InnerRec = lists:map(fun({K, V}) -> 110 | V2 = case V of 111 | [{_,_}|_] -> recursive_apply(Fun, V); 112 | V when is_list(V) -> lists:map(fun(X) -> recursive_apply(Fun, X) end, V); 113 | _ -> V 114 | end, 115 | {K, V2} 116 | end, Proplist), 117 | Fun(InnerRec); 118 | 119 | recursive_apply(Fun, List) when is_list(List) -> 120 | lists:map(fun(X) -> recursive_apply(Fun, X) end, List); 121 | 122 | recursive_apply(_Fun, Val) -> 123 | Val. -------------------------------------------------------------------------------- /src/graphql_execution.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_execution). 2 | -author("mrchex"). 3 | 4 | %% API 5 | -export([ 6 | execute/6, 7 | execute_operation/2 8 | ]). 9 | 10 | %%print(Text)-> print(Text, []). 11 | print(Text, Args) when is_list(Args) -> io:format(Text ++ "~n", Args). 12 | 13 | % Operation name can be null 14 | execute(Schema, Document, OperationName, VariableValues, InitialValue, Context0)-> 15 | Context = Context0#{ 16 | '__schema' => Schema, 17 | '__fragments' => collect_fragments(Document) 18 | }, 19 | try executor(Schema, Document, OperationName, VariableValues, InitialValue, Context) of 20 | Result -> #{data => Result} 21 | catch 22 | {error, Type, Msg} -> 23 | print("Error in ~p! Msg: ~p", [Type, Msg]), 24 | #{error => Msg, type => Type}; 25 | {field_error, Reason} -> 26 | #{errors => [Reason]} 27 | end. 28 | 29 | 30 | executor(Schema, Document, OperationName, VariableValues, InitialValue, Context0)-> 31 | Operation = get_operation(Document, OperationName), 32 | Context = Context0#{ 33 | '__variableValues' => coerceVariableValues(Schema, Operation, VariableValues), 34 | '__operation' => maps:get(operation, Operation), 35 | '__query' => Operation 36 | }, 37 | 38 | execute_operation(InitialValue, Context). 39 | 40 | % throw validation error when operation not found or document define multiple 41 | get_operation(Document, OperationName)-> 42 | Definitions = maps:get(definitions, Document, []), 43 | case get_operation_from_definitions(Definitions, OperationName) of 44 | {ok, Operation} -> Operation; 45 | {error, Error} -> throw({error, get_operation, Error}) 46 | end. 47 | 48 | get_operation_from_definitions(Definitions, OperationName) -> 49 | case get_operation_from_definitions(Definitions, OperationName, undefined) of 50 | undefined -> {error, <<"Operation not found">>}; 51 | Operation -> {ok, Operation} 52 | end. 53 | 54 | 55 | % TODO: has no spec result: when expented only one operation given first 56 | % FIXME: http://facebook.github.io/graphql/#sec-Executing-Requests 57 | get_operation_from_definitions([], _, Operation)-> Operation; 58 | % when we first meet operation - continue with what name 59 | get_operation_from_definitions([#{kind := 'OperationDefinition', operation := OperationName} = Operation|Tail], null, _)-> 60 | get_operation_from_definitions(Tail, OperationName, Operation); 61 | % when we meet another operation that named like we already founded 62 | get_operation_from_definitions([#{kind := 'OperationDefinition', operation := OperationName}|_], OperationName, _)-> 63 | {error, <<"Document defines multiple operations, otherwise the document is expected to only contain a single operation">>}; 64 | get_operation_from_definitions([_|Tail], OperationName, Operation)-> 65 | get_operation_from_definitions(Tail, OperationName, Operation). 66 | 67 | 68 | % TODO: implement me http://facebook.github.io/graphql/#CoerceVariableValues() 69 | coerceVariableValues(_, Operation, VariableValues)-> 70 | VariableDefinitions = maps:get(variableDefinitions, Operation, []), 71 | lists:foldl(fun(VariableDefinition, Acc) -> 72 | 73 | #{ 74 | kind := 'Variable', 75 | name := #{kind := 'Name', value := VarName} 76 | } = maps:get(variable, VariableDefinition), 77 | 78 | Value = maps:get(VarName, VariableValues, undefined), 79 | DefaultValue = maps:get(defaultValue, VariableDefinition, undefined), 80 | 81 | CoercedValue = case {Value, DefaultValue} of 82 | {undefined, undefined} -> 83 | throw({error, variable, <<"Value for variable: ", VarName/binary, " was not provided">>}); 84 | {undefined, _} -> DefaultValue; 85 | {_, _} -> 86 | case maps:get(type, VariableDefinition) of 87 | 88 | #{ kind := 'NamedType', name := #{ value := VarNamedType } } -> 89 | #{kind => <>, 90 | value => Value}; 91 | 92 | #{ kind := 'ListType' } when Value =:= null -> null; 93 | 94 | #{ kind := 'ListType', type := #{name := #{ value := VarNamedType } } } -> 95 | #{kind => 'ListValue', 96 | values => [#{ kind => <>, value => V} || V <- Value]}; 97 | 98 | #{ kind := 'NonNullType', type := #{ kind := 'ListType', type := #{name := #{ value := VarNamedType } } } } -> 99 | #{kind => 'NonNullValue', 100 | value => #{ 101 | kind => 'ListValue', 102 | values => [#{ kind => <>, value => V} || V <- Value] 103 | }}; 104 | 105 | #{ kind := 'NonNullType', type := #{name := #{ value := VarNamedType } } }-> 106 | #{kind => 'NonNullValue', 107 | value => #{ kind => <>, value => Value}} 108 | 109 | end 110 | end, 111 | 112 | Acc#{ VarName => CoercedValue } 113 | 114 | end, #{}, VariableDefinitions). 115 | 116 | % TODO: complete me http://facebook.github.io/graphql/#CoerceArgumentValues() 117 | coerceArgumentValues(ObjectType, Field, Context) -> 118 | FieldName = get_field_name(Field), 119 | ArgumentDefinitions = graphql_type_object:get_args(FieldName, ObjectType), 120 | VariableValues = maps:get('__variableValues', Context), 121 | 122 | maps:fold(fun(ArgumentName, ArgumentDefinition, CoercedValues) -> 123 | ArgumentType = graphql_type_object:get_type_from_definition(ArgumentDefinition), 124 | 125 | % 5 of http://facebook.github.io/graphql/#sec-Coercing-Field-Arguments 126 | CoercedValue = case get_field_argument_by_name(ArgumentName, Field) of 127 | 128 | #{ % h.Let coercedValue be the result of coercing value 129 | kind := 'Argument', 130 | name := #{kind := 'Name', value := ArgumentName}, 131 | value := Value 132 | } -> 133 | case Value of 134 | #{kind := 'Variable', name := #{ value := VariableName }} -> 135 | ParseValue = maps:get(parse_value, ArgumentType), 136 | ParseValue(maps:get(VariableName, VariableValues, null), ArgumentType); 137 | _ -> 138 | ParseLiteral = maps:get(parse_literal, ArgumentType), 139 | ParseLiteral(Value, ArgumentType) 140 | end; 141 | 142 | 143 | % f. Otherwise, if value does not exist (was not provided in argumentValues: 144 | % f.i. If defaultValue exists (including null): 145 | % f.i.1. Add an entry to coercedValues named argName with the value defaultValue. 146 | undefined -> 147 | case maps:get(default, ArgumentDefinition, undefined) of 148 | undefined -> 149 | ParseLiteral = maps:get(parse_literal, ArgumentType), 150 | ParseLiteral(null, ArgumentType); 151 | DefaultValue -> DefaultValue 152 | end 153 | 154 | % f.iii - Otherwise, continue to the next argument definition. 155 | end, 156 | 157 | CoercedValues#{ ArgumentName => CoercedValue} 158 | end, #{}, ArgumentDefinitions). 159 | 160 | 161 | % http://facebook.github.io/graphql/#sec-Executing-Operations 162 | execute_operation(InitialValue, #{'__operation' := Operation, '__schema' := Schema, '__query' := Query} = Context) -> 163 | QueryType = maps:get(Operation, Schema), 164 | SelectionSet = maps:get(selectionSet, Query), 165 | 166 | Parallel = false, % FIXME: enable parallel when we can 167 | 168 | execute_selection_set(SelectionSet, QueryType, InitialValue, Context, Parallel). 169 | 170 | % http://facebook.github.io/graphql/#sec-Executing-Selection-Sets 171 | execute_selection_set(SelectionSet, ObjectType, ObjectValue, Context)-> 172 | execute_selection_set(SelectionSet, ObjectType, ObjectValue, Context, false). 173 | 174 | execute_selection_set(SelectionSet, ObjectType, ObjectValue, Context, Parallel)-> 175 | Fragments = maps:get('__fragments', Context), 176 | VariableValues = maps:get('__variableValues', Context), 177 | GroupedFieldSet = collect_fields(ObjectType, SelectionSet, VariableValues, Fragments), 178 | 179 | MapFun = fun({ResponseKey, Fields})-> 180 | % 6.3 - 3.a. Let fieldName be the name of the first entry in fields. 181 | #{value := FieldName} = maps:get(name, lists:nth(1, Fields)), 182 | Field = case graphql_type_object:get_field(FieldName, ObjectType) of 183 | undefined -> 184 | ErrorMsg = << 185 | "Field `", FieldName/binary, 186 | "` does not exist in ObjectType `", 187 | (maps:get(name, ObjectType))/binary, "`" 188 | >>, 189 | throw({error, validation_error, ErrorMsg}); 190 | Field0 -> Field0 191 | end, 192 | 193 | % TODO: Must be implemented when we learn why its needed and what the point of use case 194 | % TODO: c.If fieldType is null: 195 | % TODO: i.Continue to the next iteration of groupedFieldSet. 196 | FieldType = graphql_type_object:get_type_from_definition(Field), 197 | 198 | ResponseValue = executeField(ObjectType, ObjectValue, Fields, FieldType, Context), 199 | {ResponseKey, ResponseValue} 200 | 201 | end, 202 | 203 | case Parallel of 204 | true -> graphql:upmap(MapFun, GroupedFieldSet, 5000); 205 | false -> lists:map(MapFun, GroupedFieldSet) 206 | end. 207 | 208 | collect_fragments(Document) -> 209 | lists:foldl(fun(Definition, Fragments) -> 210 | Kind = maps:get(kind, Definition), 211 | case Kind of 212 | 'FragmentDefinition' -> 213 | FragmentName = maps:get(value, maps:get(name, Definition)), 214 | Fragments#{FragmentName => Definition}; 215 | _ -> Fragments 216 | end 217 | end, #{}, maps:get(definitions, Document)). 218 | 219 | % TODO: does not support directives and inline fragments (3.a, 3.b, 3.e): http://facebook.github.io/graphql/#CollectFields() 220 | collect_fields(ObjectType, SelectionSet, VariableValues, Fragments) -> 221 | lists:reverse(collect_fields(ObjectType, SelectionSet, VariableValues, Fragments, [])). 222 | collect_fields(ObjectType, SelectionSet, VariableValues, Fragments, VisitedFragments0) -> 223 | Selections = maps:get(selections, SelectionSet), 224 | {CollectedFields, _} = lists:foldl(fun(Selection, {GroupedFields, VisitedFragments})-> 225 | case Selection of 226 | 227 | % 3.c 228 | #{kind := 'Field'} -> 229 | ResponseKey = get_response_key_from_selection(Selection), 230 | GroupForResponseKey = proplists:get_value(ResponseKey, GroupedFields, []), 231 | 232 | {[ 233 | {ResponseKey, [Selection|GroupForResponseKey]} 234 | | GroupedFields 235 | ], VisitedFragments}; 236 | 237 | % 3.d 238 | #{kind := 'FragmentSpread', name := #{kind := 'Name', value := FragmentSpreadName}} -> 239 | case lists:member(FragmentSpreadName, VisitedFragments) of 240 | true -> {GroupedFields, VisitedFragments}; 241 | false -> 242 | case maps:get(FragmentSpreadName, Fragments, undefined) of 243 | undefined -> {GroupedFields, VisitedFragments}; % 3.d.v 244 | Fragment -> 245 | #{ 246 | typeCondition := #{ 247 | kind := 'NamedType', 248 | name := #{kind := 'Name', value := FragmentType} 249 | } 250 | } = Fragment, 251 | case does_fragment_type_apply(ObjectType, FragmentType) of 252 | false -> {GroupedFields, VisitedFragments}; % 3.d.vii 253 | true -> 254 | FragmentSelectionSet = maps:get(selectionSet, Fragment), 255 | FragmentGroupedField = collect_fields(ObjectType, FragmentSelectionSet, VariableValues, Fragments, [FragmentSpreadName|VisitedFragments]), 256 | {GroupedFields ++ FragmentGroupedField, VisitedFragments} 257 | end 258 | end 259 | end; 260 | 261 | % 3.e 262 | 263 | #{kind := 'InlineFragment'} = Fragment -> 264 | #{ 265 | typeCondition := #{ 266 | kind := 'NamedType', 267 | name := #{ kind := 'Name', value := FragmentType } 268 | }, 269 | selectionSet := FragmentSelectionSet 270 | } = Fragment, 271 | case does_fragment_type_apply(ObjectType, FragmentType) of 272 | false -> {GroupedFields, VisitedFragments}; % 3.e.ii 273 | true -> 274 | FragmentGroupedField = collect_fields(ObjectType, FragmentSelectionSet, VariableValues, Fragments, VisitedFragments), 275 | {GroupedFields ++ FragmentGroupedField, VisitedFragments} 276 | end 277 | 278 | end 279 | end, {[], VisitedFragments0}, Selections), 280 | CollectedFields. 281 | 282 | does_fragment_type_apply(ObjectType, FragmentType)-> 283 | case ObjectType of 284 | #{ name := FragmentType } -> true; 285 | _ -> false 286 | end. 287 | 288 | get_response_key_from_selection(#{alias := #{value := Key}}) -> Key; 289 | get_response_key_from_selection(#{name := #{value := Key}}) -> Key. 290 | 291 | executeField(ObjectType, ObjectValue, [Field|_]=Fields, FieldType, Context)-> 292 | ArgumentValues = coerceArgumentValues(ObjectType, Field, Context), 293 | FieldName = get_field_name(Field), 294 | 295 | case resolveFieldValue(ObjectType, ObjectValue, FieldName, ArgumentValues, Context) of 296 | {overwrite_context, ResolvedValue, OverwritenContext} -> 297 | completeValue(FieldType, Fields, ResolvedValue, OverwritenContext); 298 | {ok, ResolvedValue} -> 299 | completeValue(FieldType, Fields, ResolvedValue, Context); 300 | 301 | {error, Reason} when is_list(Reason) -> throw({field_error, #{message => list_to_binary(Reason)}}); % {error, "msg"} 302 | {error, Reason} when is_binary(Reason) -> throw({field_error, #{message => Reason}}); % {error, <<"msg">>} 303 | {error, Reason} when is_map(Reason) -> throw({field_error, Reason}); % {error, #{message => <<"msg">>}} 304 | 305 | ResolvedValue -> 306 | completeValue(FieldType, Fields, ResolvedValue, Context) 307 | end. 308 | 309 | 310 | get_field_name(#{name := #{value := FieldName}}) -> FieldName. 311 | 312 | get_field_arguments(Field)-> 313 | case maps:get(arguments, Field, null) of 314 | null -> []; 315 | Args -> Args 316 | end. 317 | 318 | find_argument(_, []) -> undefined; 319 | find_argument(ArgumentName, [#{name := #{ value := ArgumentName }} = Arg|_])-> Arg; 320 | find_argument(ArgumentName, [_|Tail])-> find_argument(ArgumentName, Tail). 321 | 322 | get_field_argument_by_name(ArgumentName, Field)-> 323 | Arguments = get_field_arguments(Field), 324 | find_argument(ArgumentName, Arguments). 325 | 326 | resolveFieldValue(ObjectType, ObjectValue, FieldName, ArgumentValues, Context)-> 327 | Resolver = graphql_type_object:get_field_resolver(FieldName, ObjectType), 328 | case erlang:fun_info(Resolver, arity) of 329 | {arity, 0} -> Resolver(); 330 | {arity, 1} -> Resolver(ObjectValue); 331 | {arity, 2} -> Resolver(ObjectValue, ArgumentValues); 332 | {arity, 3} -> Resolver(ObjectValue, ArgumentValues, Context) 333 | end. 334 | 335 | % TODO: complete me http: //facebook.github.io/graphql/#CompleteValue() 336 | completeValue(FieldTypeDefinition, Fields, Result, Context) -> 337 | % unwrap type 338 | FieldType = graphql_type:unwrap_type(FieldTypeDefinition), 339 | 340 | % TODO: may be need move to some function in each type (serialize, for example) 341 | case FieldType of 342 | #{kind := Kind, serialize := Serialize} when 343 | Kind =:= 'SCALAR' orelse 344 | Kind =:= 'ENUM' -> 345 | Serialize(Result, FieldType, Context); 346 | 347 | #{kind := Kind, name := Name} when 348 | Kind =:= 'OBJECT' orelse 349 | Kind =:= 'UNION' -> 350 | case Result of 351 | null -> null; 352 | _ -> 353 | { AbstractFieldType, Result1 } = case Kind of 354 | 'OBJECT' -> {FieldType, Result}; 355 | 'UNION' -> 356 | ResolveType = maps:get(resolve_type, FieldType), 357 | { ResolvedType, UnwrappedResult } = ResolveType(Result, FieldType), 358 | {graphql_type:unwrap_type(ResolvedType), UnwrappedResult} 359 | end, 360 | 361 | case mergeSelectionSet(Fields) of 362 | [] -> 363 | throw({error, complete_value, <<"No sub selection provided for `", Name/binary ,"`">>}); 364 | SubSelectionSet -> 365 | execute_selection_set(#{selections => SubSelectionSet}, AbstractFieldType, Result1, Context) 366 | end 367 | end; 368 | 369 | #{kind := 'LIST', ofType := InnerTypeFun} -> 370 | case is_list(Result) of 371 | false when Result =:= null -> null; 372 | false when Result =/= null -> 373 | print("Actual result: ~p", [Result]), 374 | throw({error, result_validation, <<"Non list result for list field type">>}); 375 | true -> 376 | lists:map(fun(ResultItem) -> 377 | completeValue(InnerTypeFun, Fields, ResultItem, Context) 378 | end, Result) 379 | end; 380 | 381 | #{kind := 'NON_NULL', ofType := InnerTypeFun} -> 382 | case completeValue(InnerTypeFun, Fields, Result, Context) of 383 | % FIXME: make error message more readable 384 | null -> throw({error, complete_value, <<"Non null type cannot be null">>}); 385 | CompletedValue -> CompletedValue 386 | end; 387 | 388 | _ -> 389 | print("Provided type: ~p", [FieldType]), 390 | throw({error, complete_value, <<"Cannot complete value for provided type">>}) 391 | 392 | end. 393 | 394 | mergeSelectionSet(Fields)-> 395 | lists:foldl(fun(Field, SelectionSet) -> 396 | FieldSelectionSet = maps:get(selectionSet, Field, null), 397 | case FieldSelectionSet of 398 | null -> SelectionSet; 399 | #{selections := Selections} -> SelectionSet ++ Selections 400 | end 401 | end, [], Fields). 402 | -------------------------------------------------------------------------------- /src/graphql_introspection.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_introspection). 2 | -include("types.hrl"). 3 | 4 | %% API 5 | -export([ 6 | inject/1 7 | ]). 8 | 9 | inject(QueryRoot)-> 10 | Fields = maps:get(fields, QueryRoot), 11 | 12 | QueryRoot#{ 13 | fields => Fields#{ 14 | <<"__schema">> => #{ 15 | type => fun schema/0, 16 | resolver => fun(_,_, #{'__schema' := Schema}) -> Schema end 17 | } 18 | } 19 | }. 20 | 21 | schema() -> graphql:objectType(<<"__Schema">>, <<"Schema Introspection">>, #{ 22 | <<"queryType">> => #{ 23 | type => fun type/0, 24 | resolver => fun(Schema) -> maps:get(query, Schema, null) end 25 | }, 26 | <<"mutationType">> => #{ 27 | type => fun type/0, 28 | resolver => fun(Schema) -> maps:get(mutation, Schema, null) end 29 | }, 30 | <<"subscriptionType">> => #{ 31 | type => fun type/0, 32 | resolver => fun(Schema) -> maps:get(subscription, Schema, null) end 33 | }, 34 | <<"types">> => #{ 35 | type => ?LIST(fun type/0), 36 | resolver => fun(Schema) -> collect_types(Schema) end 37 | }, 38 | <<"directives">> => #{ % FIXME when directives implemented 39 | type => ?LIST(fun directive/0), 40 | resolver => fun() -> [] end 41 | } 42 | }). 43 | 44 | type() -> graphql:objectType(<<"__Type">>, <<"Type Introspection">>, #{ 45 | <<"kind">> => #{ 46 | type => ?STRING, 47 | resolver => fun(#{kind := Kind}) -> Kind end 48 | }, 49 | <<"ofType">> => #{ 50 | type => fun type/0, 51 | resolver => fun(Object) -> 52 | case maps:get(ofType, Object, null) of 53 | null -> null; 54 | Type -> graphql_type:unwrap_type(Type) 55 | end 56 | end 57 | }, 58 | <<"name">> => #{ 59 | type => ?STRING, 60 | resolver => fun(Object) -> maps:get(name, Object, null) end 61 | }, 62 | <<"description">> => #{ 63 | type => ?STRING, 64 | resolver => fun(Object) -> maps:get(description, Object, null) end 65 | }, 66 | <<"fields">> => #{ 67 | type => ?LIST(fun field/0), 68 | args => #{ 69 | <<"includeDeprecated">> => #{type => ?BOOLEAN, default => false} 70 | }, 71 | resolver => fun(Object, #{<<"includeDeprecated">> := IncludeDeprecated}) -> 72 | maps:fold(fun(Name, Field, Acc) -> 73 | case {maps:get(deprecated, Field, false), IncludeDeprecated} of 74 | {false, _} -> [Field#{name => Name}|Acc]; 75 | {true, true} -> [Field#{name => Name}|Acc]; 76 | {true, false} -> Acc 77 | end 78 | end, [], maps:get(fields, Object, #{})) 79 | end 80 | }, 81 | % FIXME: implement when inputTypes gonna be 82 | <<"inputFields">> => #{ 83 | type => ?LIST(?INT), 84 | resolver => fun() -> null end 85 | }, 86 | % FIXME: implement when interfaces gonna be 87 | <<"interfaces">> => #{ 88 | type => ?LIST(?INT), 89 | resolver => fun() -> [] end 90 | }, 91 | 92 | <<"enumValues">> => #{ 93 | type => ?LIST(fun enumValue/0), 94 | resolver => fun 95 | (#{kind := 'ENUM', enumValues := EnumValues}) -> EnumValues; 96 | (_) -> null 97 | end 98 | }, 99 | % FIXME: implement when interfaces gonna be 100 | <<"possibleTypes">> => #{ 101 | type => ?LIST(fun type/0), 102 | resolver => fun 103 | (#{kind := 'UNION', possibleTypes := PossibleTypes}) -> 104 | [graphql_type:unwrap_type(X) || X <- PossibleTypes]; 105 | (_) -> null 106 | end 107 | } 108 | }). 109 | 110 | directive() -> graphql:objectType(<<"__Directive">>, <<"Directive Introspection">>, #{ 111 | <<"name">> => #{type => ?STRING} 112 | }). 113 | 114 | field() -> graphql:objectType(<<"__Field">>, <<"Field Introspection">>, #{ 115 | <<"name">> => #{type => ?STRING, resolver => fun(Field) ->maps:get(name, Field) end}, 116 | <<"description">> => #{type => ?STRING, resolver => fun(Field) -> maps:get(description, Field, null) end}, 117 | <<"args">> => #{ 118 | type => ?LIST(fun inputValue/0), 119 | resolver => fun(Field) -> 120 | case maps:get(args, Field, undefined) of 121 | undefined -> []; 122 | Args -> maps:fold(fun(Name, Arg, Acc)-> 123 | [Arg#{name => Name}|Acc] 124 | end, [], Args) 125 | end 126 | end 127 | }, 128 | <<"type">> => #{type => fun type/0, resolver => fun(Field)-> graphql_type:unwrap_type(maps:get(type, Field)) end}, 129 | <<"isDeprecated">> => #{type => ?BOOLEAN, resolver => fun(Field) -> maps:get(isDeprecated, Field, false) end}, 130 | <<"deprecationReason">> => #{type => ?STRING, resolver => fun(Field) -> maps:get(deprecationReason, Field, null) end} 131 | }). 132 | 133 | inputValue() -> graphql:objectType(<<"__InputValue">>, <<"InputValue Introspection">>, #{ 134 | <<"name">> => #{type => ?STRING, resolver => fun(IV) -> maps:get(name, IV) end}, 135 | <<"description">> => #{type => ?STRING, resolver => fun(IV) -> maps:get(description, IV, null) end}, 136 | <<"type">> => #{ 137 | type => fun type/0, 138 | resolver => fun(#{type := Type}) -> graphql_type:unwrap_type(Type) end 139 | }, 140 | % fixme: type must be equal to object type 141 | <<"defaultValue">> => #{type => ?INT, resolver => fun(IV) -> maps:get(defaultFIXME, IV, null) end} 142 | }). 143 | 144 | enumValue() -> graphql:objectType(<<"EnumValue">>, <<"Enumerate value">>, #{ 145 | <<"name">> => #{type => ?STRING, resolver => fun(EV) -> maps:get(name, EV, null) end}, 146 | <<"description">> => #{type => ?STRING, resolver => fun(EV) -> maps:get(description, EV, null) end}, 147 | <<"isDeprecated">> => #{type => ?BOOLEAN, resolver => fun(EV) -> maps:get(isDeprecated, EV, null) end}, 148 | <<"deprecationReason">> => #{type => ?STRING, resolver => fun(EV) -> maps:get(deprecationReason, EV, null) end} 149 | }). 150 | 151 | extract_field_types(Object, IgnoreTypes) -> 152 | maps:fold(fun(_, #{type := FieldType} = Field, Acc)-> 153 | Type = graphql_type:unwrap_type(FieldType), 154 | ArgsTypes = case maps:get(args, Field, null) of 155 | null -> []; 156 | Args -> extract_field_types(#{fields => Args}, IgnoreTypes) 157 | end, 158 | 159 | case lists:member(maps:get(name, Type), IgnoreTypes) of 160 | true -> ArgsTypes ++ Acc; 161 | false -> ArgsTypes ++ [Type|Acc] 162 | end 163 | end, [], maps:get(fields, Object)). 164 | 165 | collect_types(Schema) -> 166 | QueryType = maps:get(query, Schema), 167 | TypeToCollect = case maps:get(mutation, Schema, null) of 168 | null -> [QueryType]; 169 | MutationType -> [QueryType, MutationType] 170 | end, 171 | 172 | {_, Types} = collect_types(TypeToCollect, [], []), 173 | 174 | Types. 175 | 176 | collect_types([], VisitedTypes, Acc) -> {VisitedTypes, Acc}; 177 | collect_types([#{kind := Kind, ofType := OfType}|TypesTail], V, A) when 178 | Kind =:= 'LIST' orelse 179 | Kind =:= 'NON_NULL' -> 180 | collect_types([graphql_type:unwrap_type(OfType)|TypesTail], V, A); 181 | collect_types([Edge|TypesTail], VisitedTypes, Acc)-> 182 | EdgeName = maps:get(name, Edge), 183 | 184 | % check is visited 185 | case lists:member(EdgeName, VisitedTypes) of 186 | % skip this edge, but add inner types for inspection 187 | true -> 188 | collect_types(TypesTail, VisitedTypes, Acc); 189 | 190 | % collect this edge and add to inner types for inspection 191 | false -> 192 | 193 | InnerTypes = case Edge of 194 | #{kind := 'OBJECT'} -> extract_field_types(Edge, [EdgeName|VisitedTypes]); 195 | #{kind := 'UNION', possibleTypes := PossibleTypes} -> 196 | [graphql_type:unwrap_type(X) || X <- PossibleTypes]; 197 | _ -> [] 198 | end, 199 | 200 | collect_types(InnerTypes ++ TypesTail, [EdgeName|VisitedTypes], [Edge|Acc]) 201 | end. 202 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /src/graphql_parser/graphql_lexer.xrl: -------------------------------------------------------------------------------- 1 | % GraphQL Lexer 2 | % 3 | % See the spec reference http://facebook.github.io/graphql/#sec-Appendix-Grammar-Summary 4 | % The relevant version is also copied into this repo 5 | 6 | Definitions. 7 | 8 | % Ignored tokens 9 | WhiteSpace = [\x{0009}\x{000B}\x{000C}\x{0020}\x{00A0}] 10 | _LineTerminator = \x{000A}\x{000D}\x{2028}\x{2029} 11 | LineTerminator = [{_LineTerminator}] 12 | Comment = #[^{_LineTerminator}]* 13 | Comma = , 14 | Ignored = {WhiteSpace}|{LineTerminator}|{Comment}|{Comma} 15 | 16 | % Lexical tokens 17 | Punctuator = [!$():=@\[\]{|}]|\.\.\. 18 | Name = [_A-Za-z][_0-9A-Za-z]* 19 | 20 | % Int Value 21 | Digit = [0-9] 22 | NonZeroDigit = [1-9] 23 | NegativeSign = - 24 | IntegerPart = {NegativeSign}?(0|{NonZeroDigit}{Digit}*) 25 | IntValue = {IntegerPart} 26 | 27 | % Float Value 28 | FractionalPart = \.{Digit}+ 29 | Sign = [+\-] 30 | ExponentIndicator = [eE] 31 | ExponentPart = {ExponentIndicator}{Sign}?{Digit}+ 32 | FloatValue = {IntegerPart}{FractionalPart}|{IntegerPart}{ExponentPart}|{IntegerPart}{FractionalPart}{ExponentPart} 33 | 34 | % String Value 35 | HexDigit = [0-9A-Fa-f] 36 | EscapedUnicode = u{HexDigit}{HexDigit}{HexDigit}{HexDigit} 37 | EscapedCharacter = ["\\\/bfnrt] 38 | StringCharacter = ([^\"{_LineTerminator}]|\\{EscapedUnicode}|\\{EscapedCharacter}) 39 | StringValue = "{StringCharacter}*" 40 | 41 | % Boolean Value 42 | BooleanValue = true|false 43 | 44 | % Reserved words 45 | ReservedWord = query|mutation|subscription|fragment|on|type|implements|interface|union|scalar|enum|input|extend|null 46 | 47 | Rules. 48 | 49 | {Ignored} : skip_token. 50 | {Punctuator} : {token, {list_to_atom(TokenChars), TokenLine}}. 51 | {ReservedWord} : {token, {list_to_atom(TokenChars), TokenLine}}. 52 | {IntValue} : {token, {int_value, TokenLine, TokenChars}}. 53 | {FloatValue} : {token, {float_value, TokenLine, TokenChars}}. 54 | {StringValue} : {token, {string_value, TokenLine, TokenChars}}. 55 | {BooleanValue} : {token, {boolean_value, TokenLine, TokenChars}}. 56 | {Name} : {token, {name, TokenLine, TokenChars}}. 57 | 58 | Erlang code. 59 | -------------------------------------------------------------------------------- /src/graphql_parser/graphql_parser.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_parser). 2 | -author("mrchex"). 3 | 4 | %% API 5 | -export([ 6 | parse/1 7 | ]). 8 | 9 | 10 | -spec parse(binary() | maybe_improper_list()) -> {'error', map()} | {'ok', map()}. 11 | parse(Q) when is_binary(Q) -> 12 | ListQ = binary_to_list(Q), 13 | parse_list(ListQ); 14 | parse(Q) when is_list(Q)-> 15 | parse_list(Q). 16 | 17 | -spec parse_list(maybe_improper_list()) -> {'error', map()} | {'ok', map()}. 18 | parse_list(Document)-> 19 | {ok, Tokens, _} = graphql_lexer:string(Document), 20 | graphql_parser_yecc:parse(Tokens). -------------------------------------------------------------------------------- /src/graphql_parser/graphql_parser_test.erl: -------------------------------------------------------------------------------- 1 | -include_lib("eunit/include/eunit.hrl"). 2 | 3 | -module(graphql_parser_test). 4 | -author("mrchex"). 5 | 6 | base_query1_test()-> 7 | Q = <<"{ base }">>, 8 | AST = graphql_parser:parse(Q), 9 | 10 | ExpectedAST = #{ 11 | definitions => [#{ 12 | kind => 'OperationDefinition', 13 | operation => query, 14 | selectionSet => #{ 15 | kind => 'SelectionSet', 16 | selections => [#{ 17 | kind => 'Field', 18 | name => #{ 19 | kind => 'Name', 20 | value => <<"base">> 21 | } 22 | }] 23 | } 24 | }], 25 | kind => 'Document'}, 26 | 27 | ?assertEqual({ok, ExpectedAST}, AST). 28 | 29 | base_query2_test()-> 30 | Q = <<"query { base }">>, 31 | AST = graphql_parser:parse(Q), 32 | 33 | ExpectedAST = #{ 34 | definitions => [#{ 35 | kind => 'OperationDefinition', 36 | operation => query, 37 | selectionSet => #{ 38 | kind => 'SelectionSet', 39 | selections => [#{ 40 | kind => 'Field', 41 | name => #{ 42 | kind => 'Name', 43 | value => <<"base">> 44 | } 45 | }] 46 | } 47 | }], 48 | kind => 'Document'}, 49 | 50 | ?assertEqual({ok, ExpectedAST}, AST). 51 | 52 | base_query_named_test()-> 53 | Q = <<"query QueryName { base }">>, 54 | AST = graphql_parser:parse(Q), 55 | 56 | ExpectedAST = #{ 57 | definitions => [#{ 58 | kind => 'OperationDefinition', 59 | operation => query, 60 | name => #{kind => 'Name',value => <<"QueryName">>}, 61 | selectionSet => #{ 62 | kind => 'SelectionSet', 63 | selections => [#{ 64 | kind => 'Field', 65 | name => #{ 66 | kind => 'Name', 67 | value => <<"base">> 68 | } 69 | }] 70 | } 71 | }], 72 | kind => 'Document'}, 73 | 74 | ?assertEqual({ok, ExpectedAST}, AST). 75 | 76 | argument_all_scalars_types_test()-> 77 | Q = <<"{ test(a:1 b: 1.2 c: ENUMVALUE d: \"string value\" d: true e: false ) }">>, 78 | AST = graphql_parser:parse(Q), 79 | 80 | ExpectedAST = #{definitions => [#{kind => 'OperationDefinition', 81 | operation => query, 82 | selectionSet => #{kind => 'SelectionSet', 83 | selections => [#{arguments => [#{kind => 'Argument', 84 | name => #{kind => 'Name', value => <<"a">>}, 85 | value => #{kind => 'IntValue', value => 1}}, 86 | #{kind => 'Argument', 87 | name => #{kind => 'Name', value => <<"b">>}, 88 | value => #{kind => 'FloatValue', value => 1.2}}, 89 | #{kind => 'Argument', 90 | name => #{kind => 'Name', value => <<"c">>}, 91 | value => #{kind => 'EnumValue', value => <<"ENUMVALUE">>}}, 92 | #{kind => 'Argument', 93 | name => #{kind => 'Name', value => <<"d">>}, 94 | value => #{kind => 'StringValue', value => <<"string value">>}}, 95 | #{kind => 'Argument', 96 | name => #{kind => 'Name', value => <<"d">>}, 97 | value => #{kind => 'BooleanValue', value => true}}, 98 | #{kind => 'Argument', 99 | name => #{kind => 'Name', value => <<"e">>}, 100 | value => #{kind => 'BooleanValue', value => false}}], 101 | kind => 'Field', 102 | name => #{kind => 'Name', value => <<"test">>}}]}}], 103 | kind => 'Document'}, 104 | 105 | ?assertEqual({ok, ExpectedAST}, AST). 106 | 107 | variable_definition_test() -> 108 | Q = <<"query($a: NullableType $b: NonNullType! = 1) { foo(z: $a w: $b) }">>, 109 | AST = graphql_parser:parse(Q), 110 | 111 | ExpectedAST = #{definitions => [#{kind => 'OperationDefinition', 112 | operation => query, 113 | selectionSet => #{kind => 'SelectionSet', 114 | selections => [#{arguments => [#{kind => 'Argument', 115 | name => #{kind => 'Name',value => <<"z">>}, 116 | value => #{kind => 'Variable',name => #{kind => 'Name',value => <<"a">>}}}, 117 | #{kind => 'Argument', 118 | name => #{kind => 'Name',value => <<"w">>}, 119 | value => #{kind => 'Variable',name => #{kind => 'Name',value => <<"b">>}}}], 120 | kind => 'Field', 121 | name => #{kind => 'Name',value => <<"foo">>}}]}, 122 | variableDefinitions => [#{kind => 'VariableDefinition', 123 | type => #{kind => 'NamedType',name => #{kind => 'Name',value => <<"NullableType">>}}, 124 | variable => #{kind => 'Variable',name => #{kind => 'Name',value => <<"a">>}}}, 125 | #{defaultValue => #{kind => 'IntValue',value => 1}, 126 | kind => 'VariableDefinition', 127 | type => #{kind => 'NonNullType', 128 | type => #{kind => 'NamedType',name => #{kind => 'Name',value => <<"NonNullType">>}}}, 129 | variable => #{kind => 'Variable',name => #{kind => 'Name',value => <<"b">>}}}]}], 130 | kind => 'Document'}, 131 | 132 | ?assertEqual({ok, ExpectedAST}, AST). 133 | 134 | 135 | directives_test() -> 136 | Q = <<"{foo @someDirective(when: false)}">>, 137 | AST = graphql_parser:parse(Q), 138 | 139 | ExpectedAST = #{definitions => [#{kind => 'OperationDefinition', 140 | operation => query, 141 | selectionSet => #{kind => 'SelectionSet', 142 | selections => [#{directives => [#{arguments => [#{kind => 'Argument', 143 | name => #{kind => 'Name',value => <<"when">>}, 144 | value => #{kind => 'BooleanValue',value => false}}], 145 | kind => 'Directive', 146 | name => #{kind => 'Name',value => <<"someDirective">>}}], 147 | kind => 'Field', 148 | name => #{kind => 'Name',value => <<"foo">>}}]}}], 149 | kind => 'Document'}, 150 | 151 | ?assertEqual({ok, ExpectedAST}, AST). 152 | 153 | 154 | fragments_test()-> 155 | Q = <<"query withFragments { 156 | user { 157 | friends { ...friendFields } 158 | mutualFriends { ...friendFields } 159 | } 160 | } 161 | 162 | fragment friendFields on User { 163 | id 164 | }">>, 165 | 166 | AST = graphql_parser:parse(Q), 167 | 168 | ExpectedAST = #{definitions => [#{kind => 'OperationDefinition', 169 | name => #{kind => 'Name',value => <<"withFragments">>}, 170 | operation => query, 171 | selectionSet => #{kind => 'SelectionSet', 172 | selections => [#{kind => 'Field', 173 | name => #{kind => 'Name',value => <<"user">>}, 174 | selectionSet => #{kind => 'SelectionSet', 175 | selections => [#{kind => 'Field', 176 | name => #{kind => 'Name',value => <<"friends">>}, 177 | selectionSet => #{kind => 'SelectionSet', 178 | selections => [#{kind => 'FragmentSpread', 179 | name => #{kind => 'Name',value => <<"friendFields">>}}]}}, 180 | #{kind => 'Field', 181 | name => #{kind => 'Name',value => <<"mutualFriends">>}, 182 | selectionSet => #{kind => 'SelectionSet', 183 | selections => [#{kind => 'FragmentSpread', 184 | name => #{kind => 'Name',value => <<"friendFields">>}}]}}]}}]}}, 185 | #{kind => 'FragmentDefinition', 186 | name => #{kind => 'Name',value => <<"friendFields">>}, 187 | selectionSet => #{kind => 'SelectionSet', 188 | selections => [#{kind => 'Field',name => #{kind => 'Name',value => <<"id">>}}]}, 189 | typeCondition => #{kind => 'NamedType',name => #{kind => 'Name',value => <<"User">>}}}], 190 | kind => 'Document'}, 191 | 192 | ?assertEqual({ok, ExpectedAST}, AST). 193 | -------------------------------------------------------------------------------- /src/graphql_parser/graphql_parser_yecc.yrl: -------------------------------------------------------------------------------- 1 | Nonterminals 2 | Document 3 | Definitions Definition OperationDefinition FragmentDefinition TypeDefinition 4 | ObjectTypeDefinition InterfaceTypeDefinition UnionTypeDefinition 5 | ScalarTypeDefinition EnumTypeDefinition InputObjectTypeDefinition TypeExtensionDefinition 6 | FieldDefinitionList FieldDefinition ImplementsInterfaces ArgumentsDefinition 7 | InputValueDefinitionList InputValueDefinition UnionMembers 8 | EnumValueDefinitionList EnumValueDefinition 9 | SelectionSet Selections Selection 10 | OperationType Name NameWithoutOn VariableDefinitions VariableDefinition Directives Directive 11 | Field Alias Arguments ArgumentList Argument 12 | FragmentSpread FragmentName InlineFragment 13 | VariableDefinitionList Variable DefaultValue 14 | Type TypeCondition NamedTypeList NamedType ListType NonNullType 15 | Value EnumValue ListValue Values ObjectValue ObjectFields ObjectField. 16 | 17 | Terminals 18 | 'query' 'mutation' 'subscription' 19 | '{' '}' '(' ')' '[' ']' '!' ':' '@' '$' '=' '|' '...' 20 | 'fragment' 'on' 'null' 21 | 'type' 'implements' 'interface' 'union' 'scalar' 'enum' 'input' 'extend' 22 | name int_value float_value string_value boolean_value. 23 | 24 | Rootsymbol Document. 25 | 26 | Document -> Definitions : build_ast_node('Document', #{'definitions' => '$1'}). 27 | 28 | Definitions -> Definition : ['$1']. 29 | Definitions -> Definition Definitions : ['$1'|'$2']. 30 | 31 | Definition -> OperationDefinition : '$1'. 32 | Definition -> FragmentDefinition : '$1'. 33 | Definition -> TypeDefinition : '$1'. 34 | 35 | OperationType -> 'query' : extract_atom('$1'). 36 | OperationType -> 'mutation' : extract_atom('$1'). 37 | OperationType -> 'subscription' : extract_atom('$1'). 38 | 39 | OperationDefinition -> SelectionSet : build_ast_node('OperationDefinition', #{'operation' => 'query', 'selectionSet' => '$1'}). 40 | OperationDefinition -> OperationType SelectionSet : build_ast_node('OperationDefinition', #{'operation' => '$1', 'selectionSet' => '$2'}). 41 | OperationDefinition -> OperationType VariableDefinitions SelectionSet : build_ast_node('OperationDefinition', #{'operation' => '$1', 'variableDefinitions' => '$2', 'selectionSet' => '$3'}). 42 | OperationDefinition -> OperationType Name SelectionSet : build_ast_node('OperationDefinition', #{'operation' => '$1', 'name' => extract_name('$2'), 'selectionSet' => '$3'}). 43 | OperationDefinition -> OperationType Name VariableDefinitions SelectionSet : build_ast_node('OperationDefinition', #{'operation' => '$1', 'name' => extract_name('$2'), 'variableDefinitions' => '$3', 'selectionSet' => '$4'}). 44 | OperationDefinition -> OperationType Name Directives SelectionSet : build_ast_node('OperationDefinition', #{'operation' => '$1', 'name' => extract_name('$2'), 'directives' => '$3', 'selectionSet' => '$4'}). 45 | OperationDefinition -> OperationType Name VariableDefinitions Directives SelectionSet : build_ast_node('OperationDefinition', #{'operation' => '$1', 'name' => extract_name('$2'), 'variableDefinitions' => '$3', 'directives' => '$4', 'selectionSet' => '$5'}). 46 | 47 | FragmentDefinition -> 'fragment' FragmentName 'on' TypeCondition SelectionSet : build_ast_node('FragmentDefinition', #{'name' => extract_name('$2'), 'typeCondition' => '$4', 'selectionSet' => '$5'}). 48 | FragmentDefinition -> 'fragment' FragmentName 'on' TypeCondition Directives SelectionSet : build_ast_node('FragmentDefinition', #{'name' => extract_name('$2'), 'typeCondition' => '$4', 'directives' => '$5', 'selectionSet' => '$6'}). 49 | 50 | TypeCondition -> NamedType : '$1'. 51 | 52 | VariableDefinitions -> '(' VariableDefinitionList ')' : '$2'. 53 | VariableDefinitionList -> VariableDefinition : ['$1']. 54 | VariableDefinitionList -> VariableDefinition VariableDefinitionList : ['$1'|'$2']. 55 | VariableDefinition -> Variable ':' Type : build_ast_node('VariableDefinition', #{'variable' => '$1', 'type' => '$3'}). 56 | VariableDefinition -> Variable ':' Type DefaultValue : build_ast_node('VariableDefinition', #{'variable' => '$1', 'type' => '$3', 'defaultValue' => '$4'}). 57 | Variable -> '$' Name : build_ast_node('Variable', #{'name' => extract_name('$2')}). 58 | 59 | DefaultValue -> '=' Value : '$2'. 60 | 61 | Type -> NamedType : '$1'. 62 | Type -> ListType : '$1'. 63 | Type -> NonNullType : '$1'. 64 | NamedType -> Name : build_ast_node('NamedType', #{'name' => extract_name('$1')}). 65 | ListType -> '[' Type ']' : build_ast_node('ListType', #{'type' => '$2'}). 66 | NonNullType -> NamedType '!' : build_ast_node('NonNullType', #{'type' => '$1'}). 67 | NonNullType -> ListType '!' : build_ast_node('NonNullType', #{'type' => '$1'}). 68 | 69 | SelectionSet -> '{' Selections '}' : build_ast_node('SelectionSet', #{'selections' => '$2'}). 70 | 71 | Selections -> Selection : ['$1']. 72 | Selections -> Selection Selections : ['$1'|'$2']. 73 | 74 | Selection -> Field : '$1'. 75 | Selection -> FragmentSpread : '$1'. 76 | Selection -> InlineFragment : '$1'. 77 | 78 | FragmentSpread -> '...' FragmentName : build_ast_node('FragmentSpread', #{'name' => extract_name('$2')}). 79 | FragmentSpread -> '...' FragmentName Directives : build_ast_node('FragmentSpread', #{'name' => extract_name('$2'), 'directives' => '$3'}). 80 | 81 | InlineFragment -> '...' SelectionSet : build_ast_node('InlineFragment', #{'selectionSet' => '$2'}). 82 | InlineFragment -> '...' Directives SelectionSet : build_ast_node('InlineFragment', #{'directives' => '$2', 'selectionSet' => '$3'}). 83 | 84 | InlineFragment -> '...' 'on' TypeCondition SelectionSet : build_ast_node('InlineFragment', #{'typeCondition' => '$3', 'selectionSet' => '$4'}). 85 | InlineFragment -> '...' 'on' TypeCondition Directives SelectionSet : build_ast_node('InlineFragment', #{'typeCondition' => '$3', 'directives' => '$4', 'selectionSet' => '$5'}). 86 | 87 | FragmentName -> NameWithoutOn : '$1'. 88 | 89 | Field -> Name : build_ast_node('Field', #{'name' => extract_name('$1')}). 90 | Field -> Name Arguments : build_ast_node('Field', #{'name' => extract_name('$1'), 'arguments' => '$2'}). 91 | Field -> Name Directives : build_ast_node('Field', #{'name' => extract_name('$1'), 'directives' => '$2'}). 92 | Field -> Name SelectionSet : build_ast_node('Field', #{'name' => extract_name('$1'), 'selectionSet' => '$2'}). 93 | Field -> Name Directives SelectionSet : build_ast_node('Field', #{'name' => extract_name('$1'), 'directives' => '$2', 'selectionSet' => '$3'}). 94 | Field -> Name Arguments SelectionSet : build_ast_node('Field', #{'name' => extract_name('$1'), 'arguments' => '$2', 'selectionSet' => '$3'}). 95 | Field -> Name Arguments Directives : build_ast_node('Field', #{'name' => extract_name('$1'), 'arguments' => '$2', 'directives' => '$3'}). 96 | Field -> Name Arguments Directives SelectionSet : build_ast_node('Field', #{'name' => extract_name('$1'), 'arguments' => '$2', 'directives' => '$3', 'selectionSet' => '$4'}). 97 | Field -> Alias Name : build_ast_node('Field', #{'alias' => extract_name('$1'), 'name' => extract_name('$2')}). 98 | Field -> Alias Name Arguments : build_ast_node('Field', #{'alias' => extract_name('$1'), 'name' => extract_name('$2'), 'arguments' => '$3'}). 99 | Field -> Alias Name SelectionSet : build_ast_node('Field', #{'alias' => extract_name('$1'), 'name' => extract_name('$2'), 'selectionSet' => '$3'}). 100 | Field -> Alias Name Arguments SelectionSet : build_ast_node('Field', #{'alias' => extract_name('$1'), 'name' => extract_name('$2'), 'arguments' => '$3', 'selectionSet' => '$4'}). 101 | Field -> Alias Name Directives : build_ast_node('Field', #{'alias' => extract_name('$1'), 'name' => extract_name('$2'), 'directives' => '$3'}). 102 | Field -> Alias Name Arguments Directives : build_ast_node('Field', #{'alias' => extract_name('$1'), 'name' => extract_name('$2'), 'arguments' => '$3', 'directives' => '$4'}). 103 | Field -> Alias Name Directives SelectionSet : build_ast_node('Field', #{'alias' => extract_name('$1'), 'name' => extract_name('$2'), 'directives' => '$3', 'selectionSet' => '$4'}). 104 | Field -> Alias Name Arguments Directives SelectionSet : build_ast_node('Field', #{'alias' => extract_name('$1'), 'name' => extract_name('$2'), 'arguments' => '$3', 'directives' => '$4', 'selectionSet' => '$5'}). 105 | 106 | Alias -> Name ':' : '$1'. 107 | 108 | Arguments -> '(' ArgumentList ')' : '$2'. 109 | ArgumentList -> Argument : ['$1']. 110 | ArgumentList -> Argument ArgumentList : ['$1'|'$2']. 111 | Argument -> Name ':' Value : build_ast_node('Argument', #{name => extract_name('$1'), value => '$3'}). 112 | 113 | Directives -> Directive : ['$1']. 114 | Directives -> Directive Directives : ['$1'|'$2']. 115 | Directive -> '@' Name : build_ast_node('Directive', #{name => extract_name('$2')}). 116 | Directive -> '@' Name Arguments : build_ast_node('Directive', #{name => extract_name('$2'), 'arguments' => '$3'}). 117 | 118 | NameWithoutOn -> name : extract_token('$1'). 119 | NameWithoutOn -> 'query' : extract_keyword('$1'). 120 | NameWithoutOn -> 'mutation' : extract_keyword('$1'). 121 | NameWithoutOn -> 'fragment' : extract_keyword('$1'). 122 | NameWithoutOn -> 'type' : extract_keyword('$1'). 123 | NameWithoutOn -> 'implements' : extract_keyword('$1'). 124 | NameWithoutOn -> 'interface' : extract_keyword('$1'). 125 | NameWithoutOn -> 'union' : extract_keyword('$1'). 126 | NameWithoutOn -> 'scalar' : extract_keyword('$1'). 127 | NameWithoutOn -> 'enum' : extract_keyword('$1'). 128 | NameWithoutOn -> 'input' : extract_keyword('$1'). 129 | NameWithoutOn -> 'extend' : extract_keyword('$1'). 130 | NameWithoutOn -> 'null' : extract_keyword('$1'). 131 | 132 | Name -> NameWithoutOn : '$1'. 133 | Name -> 'on' : extract_keyword('$1'). 134 | 135 | Value -> Variable : '$1'. 136 | Value -> int_value : build_ast_node('IntValue', #{'value' => extract_integer('$1')}). 137 | Value -> float_value : build_ast_node('FloatValue', #{'value' => extract_float('$1')}). 138 | Value -> string_value : build_ast_node('StringValue', #{'value' => extract_quoted_string_token('$1')}). 139 | Value -> boolean_value : build_ast_node('BooleanValue', #{'value' => extract_boolean('$1')}). 140 | Value -> EnumValue : build_ast_node('EnumValue', #{'value' => '$1'}). 141 | Value -> ListValue : build_ast_node('ListValue', #{'values' => '$1'}). 142 | Value -> ObjectValue : build_ast_node('ObjectValue', #{'fields' => '$1'}). 143 | 144 | EnumValue -> Name : '$1'. 145 | 146 | ListValue -> '[' ']' : []. 147 | ListValue -> '[' Values ']' : '$2'. 148 | Values -> Value : ['$1']. 149 | Values -> Value Values : ['$1'|'$2']. 150 | 151 | ObjectValue -> '{' '}' : []. 152 | ObjectValue -> '{' ObjectFields '}' : '$2'. 153 | ObjectFields -> ObjectField : ['$1']. 154 | ObjectFields -> ObjectField ObjectFields : ['$1'|'$2']. 155 | ObjectField -> Name ':' Value : build_ast_node('ObjectField', #{'name' => extract_name('$1'), 'value' => '$3'}). 156 | 157 | TypeDefinition -> ObjectTypeDefinition : '$1'. 158 | TypeDefinition -> InterfaceTypeDefinition : '$1'. 159 | TypeDefinition -> UnionTypeDefinition : '$1'. 160 | TypeDefinition -> ScalarTypeDefinition : '$1'. 161 | TypeDefinition -> EnumTypeDefinition : '$1'. 162 | TypeDefinition -> InputObjectTypeDefinition : '$1'. 163 | TypeDefinition -> TypeExtensionDefinition : '$1'. 164 | 165 | ObjectTypeDefinition -> 'type' Name '{' FieldDefinitionList '}' : 166 | build_ast_node('ObjectTypeDefinition', #{'name' => extract_name('$2'), 'fields' => '$4'}). 167 | ObjectTypeDefinition -> 'type' Name ImplementsInterfaces '{' FieldDefinitionList '}' : 168 | build_ast_node('ObjectTypeDefinition', #{'name' => extract_name('$2'), 'interfaces' => '$3', 'fields' => '$5'}). 169 | 170 | ImplementsInterfaces -> 'implements' NamedTypeList : '$2'. 171 | 172 | NamedTypeList -> NamedType : ['$1']. 173 | NamedTypeList -> NamedType NamedTypeList : ['$1'|'$2']. 174 | 175 | FieldDefinitionList -> FieldDefinition : ['$1']. 176 | FieldDefinitionList -> FieldDefinition FieldDefinitionList : ['$1'|'$2']. 177 | FieldDefinition -> Name ':' Type : build_ast_node('FieldDefinition', #{'name' => extract_name('$1'), 'type' => '$3'}). 178 | FieldDefinition -> Name ArgumentsDefinition ':' Type : build_ast_node('FieldDefinition', #{'name' => extract_name('$1'), 'arguments' => '$2', 'type' => '$4'}). 179 | 180 | ArgumentsDefinition -> '(' InputValueDefinitionList ')' : '$2'. 181 | 182 | InputValueDefinitionList -> InputValueDefinition : ['$1']. 183 | InputValueDefinitionList -> InputValueDefinition InputValueDefinitionList : ['$1'|'$2']. 184 | 185 | InputValueDefinition -> Name ':' Type : build_ast_node('InputValueDefinition', #{'name' => extract_name('$1'), 'type' => '$3'}). 186 | InputValueDefinition -> Name ':' Type DefaultValue : build_ast_node('InputValueDefinition', #{'name' => extract_name('$1'), 'type' => '$3', 'defaultValue' => '$4'}). 187 | 188 | InterfaceTypeDefinition -> 'interface' Name '{' FieldDefinitionList '}' : 189 | build_ast_node('InterfaceTypeDefinition', #{'name' => extract_name('$2'), 'fields' => '$4'}). 190 | 191 | UnionTypeDefinition -> 'union' Name '=' UnionMembers : 192 | build_ast_node('UnionTypeDefinition', #{'name' => extract_name('$2'), 'types' => '$4'}). 193 | 194 | UnionMembers -> NamedType : ['$1']. 195 | UnionMembers -> NamedType '|' UnionMembers : ['$1'|'$3']. 196 | 197 | ScalarTypeDefinition -> 'scalar' Name : build_ast_node('ScalarTypeDefinition', #{'name' => extract_name('$2')}). 198 | 199 | EnumTypeDefinition -> 'enum' Name '{' EnumValueDefinitionList '}': 200 | build_ast_node('EnumTypeDefinition', #{'name' => extract_name('$2'), 'values' => '$4'}). 201 | 202 | EnumValueDefinitionList -> EnumValueDefinition : ['$1']. 203 | EnumValueDefinitionList -> EnumValueDefinition EnumValueDefinitionList : ['$1'|'$2']. 204 | 205 | EnumValueDefinition -> EnumValue : '$1'. 206 | 207 | InputObjectTypeDefinition -> 'input' Name '{' InputValueDefinitionList '}' : 208 | build_ast_node('InputObjectTypeDefinition', #{'name' => extract_name('$2'), 'fields' => '$4'}). 209 | 210 | TypeExtensionDefinition -> 'extend' ObjectTypeDefinition : 211 | build_ast_node('TypeExtensionDefinition', #{'definition' => '$2'}). 212 | 213 | Erlang code. 214 | 215 | extract_atom({Value, _Line}) -> Value. 216 | extract_token({_Token, _Line, Value}) -> list_to_binary(Value). 217 | extract_quoted_string_token({_Token, _Line, Value}) -> unicode:characters_to_binary(lists:sublist(Value, 2, length(Value) - 2)). 218 | extract_integer({_Token, _Line, Value}) -> {Int, []} = string:to_integer(Value), Int. 219 | extract_float({_Token, _Line, Value}) -> {Float, []} = string:to_float(Value), Float. 220 | extract_boolean({_Token, _Line, "true"}) -> true; 221 | extract_boolean({_Token, _Line, "false"}) -> false. 222 | extract_keyword({Value, _Line}) -> list_to_binary(atom_to_list(Value)). 223 | extract_name(Value) -> build_ast_node('Name', #{'value' => Value}). 224 | 225 | build_ast_node(Type, Node) -> Node#{kind => Type}. -------------------------------------------------------------------------------- /src/test/graphql_context_test.erl: -------------------------------------------------------------------------------- 1 | % This test inspired by issue https://github.com/graphql-erlang/graphql/issues/32 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -include("types.hrl"). 5 | -module(graphql_context_test). 6 | 7 | schema() -> ?SCHEMA(#{ 8 | query => fun query/0 9 | }). 10 | 11 | db_resolver(_,_, #{db := DB}) -> DB. 12 | 13 | query() -> ?OBJECT("Query", "", #{ 14 | "a" => ?FIELD(?INT, "Db from context", fun db_resolver/3), 15 | "b" => ?FIELD(fun b/0, "Here context overwriting", fun(_,_,Context)-> {overwrite_context, #{}, Context#{db => 2}} end), 16 | "c" => ?FIELD(?INT, "Db from context", fun db_resolver/3), 17 | "d" => ?FIELD(?INT, "Db from context", fun db_resolver/3) 18 | }). 19 | 20 | b()-> ?OBJECT("B", "", #{ 21 | "b1" => ?FIELD(?INT, "Db from context", fun db_resolver/3), 22 | "b2" => ?FIELD(?INT, "Db from context", fun db_resolver/3) 23 | }). 24 | 25 | query_test() -> 26 | Document = <<"{ 27 | a 28 | b { 29 | b1 30 | b2 31 | } 32 | c 33 | d 34 | }">>, 35 | 36 | Context = #{db => 1}, 37 | 38 | Expect = #{ 39 | <<"a">> => 1, 40 | <<"b">> => #{ 41 | <<"b1">> => 2, 42 | <<"b2">> => 2 43 | }, 44 | <<"c">> => 1, 45 | <<"d">> => 1 46 | }, 47 | 48 | #{ data := Result } = graphql:exec(schema(), Document, #{ 49 | context => Context 50 | }), 51 | 52 | ?assertEqual(Expect, Result). -------------------------------------------------------------------------------- /src/test/graphql_introspection_test.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_introspection_test). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | -include("types.hrl"). 4 | 5 | query() -> <<" 6 | query IntrospectionQuery { 7 | __schema { 8 | queryType { name } 9 | mutationType { name } 10 | subscriptionType { name } 11 | types { 12 | ...FullType 13 | } 14 | directives { 15 | name 16 | description 17 | locations 18 | args { 19 | ...InputValue 20 | } 21 | } 22 | } 23 | } 24 | 25 | fragment FullType on __Type { 26 | kind 27 | name 28 | description 29 | fields(includeDeprecated: true) { 30 | name 31 | description 32 | args { 33 | ...InputValue 34 | } 35 | type { 36 | ...TypeRef 37 | } 38 | isDeprecated 39 | deprecationReason 40 | } 41 | inputFields { 42 | ...InputValue 43 | } 44 | interfaces { 45 | ...TypeRef 46 | } 47 | enumValues(includeDeprecated: true) { 48 | name 49 | description 50 | isDeprecated 51 | deprecationReason 52 | } 53 | possibleTypes { 54 | ...TypeRef 55 | } 56 | } 57 | 58 | fragment InputValue on __InputValue { 59 | name 60 | description 61 | type { ...TypeRef } 62 | defaultValue 63 | } 64 | 65 | fragment TypeRef on __Type { 66 | kind 67 | name 68 | ofType { 69 | kind 70 | name 71 | ofType { 72 | kind 73 | name 74 | ofType { 75 | kind 76 | name 77 | ofType { 78 | kind 79 | name 80 | ofType { 81 | kind 82 | name 83 | ofType { 84 | kind 85 | name 86 | ofType { 87 | kind 88 | name 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | ">>. 98 | 99 | expect() -> #{data => [{<<"__schema">>, 100 | [{<<"queryType">>,[{<<"name">>,<<"QueryRoot">>}]}, 101 | {<<"mutationType">>,null}, 102 | {<<"subscriptionType">>,null}, 103 | {<<"types">>, 104 | [[{<<"kind">>,<<"OBJECT">>}, 105 | {<<"name">>,<<"__Directive">>}, 106 | {<<"description">>,<<"Directive Introspection">>}, 107 | {<<"fields">>, 108 | [[{<<"name">>,<<"name">>}, 109 | {<<"description">>,null}, 110 | {<<"args">>,[]}, 111 | {<<"type">>, 112 | [{<<"kind">>,<<"SCALAR">>}, 113 | {<<"name">>,<<"String">>}, 114 | {<<"ofType">>,null}]}, 115 | {<<"isDeprecated">>,false}, 116 | {<<"deprecationReason">>,null}], 117 | [{<<"name">>,<<"__typename">>}, 118 | {<<"description">>,<<"Name of current type">>}, 119 | {<<"args">>,[]}, 120 | {<<"type">>, 121 | [{<<"kind">>,<<"SCALAR">>}, 122 | {<<"name">>,<<"String">>}, 123 | {<<"ofType">>,null}]}, 124 | {<<"isDeprecated">>,false}, 125 | {<<"deprecationReason">>,null}]]}, 126 | {<<"inputFields">>,null}, 127 | {<<"interfaces">>,[]}, 128 | {<<"enumValues">>,null}, 129 | {<<"possibleTypes">>,null}], 130 | [{<<"kind">>,<<"OBJECT">>}, 131 | {<<"name">>,<<"EnumValue">>}, 132 | {<<"description">>,<<"Enumerate value">>}, 133 | {<<"fields">>, 134 | [[{<<"name">>,<<"name">>}, 135 | {<<"description">>,null}, 136 | {<<"args">>,[]}, 137 | {<<"type">>, 138 | [{<<"kind">>,<<"SCALAR">>}, 139 | {<<"name">>,<<"String">>}, 140 | {<<"ofType">>,null}]}, 141 | {<<"isDeprecated">>,false}, 142 | {<<"deprecationReason">>,null}], 143 | [{<<"name">>,<<"isDeprecated">>}, 144 | {<<"description">>,null}, 145 | {<<"args">>,[]}, 146 | {<<"type">>, 147 | [{<<"kind">>,<<"SCALAR">>}, 148 | {<<"name">>,<<"Boolean">>}, 149 | {<<"ofType">>,null}]}, 150 | {<<"isDeprecated">>,false}, 151 | {<<"deprecationReason">>,null}], 152 | [{<<"name">>,<<"description">>}, 153 | {<<"description">>,null}, 154 | {<<"args">>,[]}, 155 | {<<"type">>, 156 | [{<<"kind">>,<<"SCALAR">>}, 157 | {<<"name">>,<<"String">>}, 158 | {<<"ofType">>,null}]}, 159 | {<<"isDeprecated">>,false}, 160 | {<<"deprecationReason">>,null}], 161 | [{<<"name">>,<<"deprecationReason">>}, 162 | {<<"description">>,null}, 163 | {<<"args">>,[]}, 164 | {<<"type">>, 165 | [{<<"kind">>,<<"SCALAR">>}, 166 | {<<"name">>,<<"String">>}, 167 | {<<"ofType">>,null}]}, 168 | {<<"isDeprecated">>,false}, 169 | {<<"deprecationReason">>,null}], 170 | [{<<"name">>,<<"__typename">>}, 171 | {<<"description">>,<<"Name of current type">>}, 172 | {<<"args">>,[]}, 173 | {<<"type">>, 174 | [{<<"kind">>,<<"SCALAR">>}, 175 | {<<"name">>,<<"String">>}, 176 | {<<"ofType">>,null}]}, 177 | {<<"isDeprecated">>,false}, 178 | {<<"deprecationReason">>,null}]]}, 179 | {<<"inputFields">>,null}, 180 | {<<"interfaces">>,[]}, 181 | {<<"enumValues">>,null}, 182 | {<<"possibleTypes">>,null}], 183 | [{<<"kind">>,<<"OBJECT">>}, 184 | {<<"name">>,<<"__InputValue">>}, 185 | {<<"description">>,<<"InputValue Introspection">>}, 186 | {<<"fields">>, 187 | [[{<<"name">>,<<"type">>}, 188 | {<<"description">>,null}, 189 | {<<"args">>,[]}, 190 | {<<"type">>, 191 | [{<<"kind">>,<<"OBJECT">>}, 192 | {<<"name">>,<<"__Type">>}, 193 | {<<"ofType">>,null}]}, 194 | {<<"isDeprecated">>,false}, 195 | {<<"deprecationReason">>,null}], 196 | [{<<"name">>,<<"name">>}, 197 | {<<"description">>,null}, 198 | {<<"args">>,[]}, 199 | {<<"type">>, 200 | [{<<"kind">>,<<"SCALAR">>}, 201 | {<<"name">>,<<"String">>}, 202 | {<<"ofType">>,null}]}, 203 | {<<"isDeprecated">>,false}, 204 | {<<"deprecationReason">>,null}], 205 | [{<<"name">>,<<"description">>}, 206 | {<<"description">>,null}, 207 | {<<"args">>,[]}, 208 | {<<"type">>, 209 | [{<<"kind">>,<<"SCALAR">>}, 210 | {<<"name">>,<<"String">>}, 211 | {<<"ofType">>,null}]}, 212 | {<<"isDeprecated">>,false}, 213 | {<<"deprecationReason">>,null}], 214 | [{<<"name">>,<<"defaultValue">>}, 215 | {<<"description">>,null}, 216 | {<<"args">>,[]}, 217 | {<<"type">>, 218 | [{<<"kind">>,<<"SCALAR">>}, 219 | {<<"name">>,<<"Int">>}, 220 | {<<"ofType">>,null}]}, 221 | {<<"isDeprecated">>,false}, 222 | {<<"deprecationReason">>,null}], 223 | [{<<"name">>,<<"__typename">>}, 224 | {<<"description">>,<<"Name of current type">>}, 225 | {<<"args">>,[]}, 226 | {<<"type">>, 227 | [{<<"kind">>,<<"SCALAR">>}, 228 | {<<"name">>,<<"String">>}, 229 | {<<"ofType">>,null}]}, 230 | {<<"isDeprecated">>,false}, 231 | {<<"deprecationReason">>,null}]]}, 232 | {<<"inputFields">>,null}, 233 | {<<"interfaces">>,[]}, 234 | {<<"enumValues">>,null}, 235 | {<<"possibleTypes">>,null}], 236 | [{<<"kind">>,<<"OBJECT">>}, 237 | {<<"name">>,<<"__Field">>}, 238 | {<<"description">>,<<"Field Introspection">>}, 239 | {<<"fields">>, 240 | [[{<<"name">>,<<"type">>}, 241 | {<<"description">>,null}, 242 | {<<"args">>,[]}, 243 | {<<"type">>, 244 | [{<<"kind">>,<<"OBJECT">>}, 245 | {<<"name">>,<<"__Type">>}, 246 | {<<"ofType">>,null}]}, 247 | {<<"isDeprecated">>,false}, 248 | {<<"deprecationReason">>,null}], 249 | [{<<"name">>,<<"name">>}, 250 | {<<"description">>,null}, 251 | {<<"args">>,[]}, 252 | {<<"type">>, 253 | [{<<"kind">>,<<"SCALAR">>}, 254 | {<<"name">>,<<"String">>}, 255 | {<<"ofType">>,null}]}, 256 | {<<"isDeprecated">>,false}, 257 | {<<"deprecationReason">>,null}], 258 | [{<<"name">>,<<"isDeprecated">>}, 259 | {<<"description">>,null}, 260 | {<<"args">>,[]}, 261 | {<<"type">>, 262 | [{<<"kind">>,<<"SCALAR">>}, 263 | {<<"name">>,<<"Boolean">>}, 264 | {<<"ofType">>,null}]}, 265 | {<<"isDeprecated">>,false}, 266 | {<<"deprecationReason">>,null}], 267 | [{<<"name">>,<<"description">>}, 268 | {<<"description">>,null}, 269 | {<<"args">>,[]}, 270 | {<<"type">>, 271 | [{<<"kind">>,<<"SCALAR">>}, 272 | {<<"name">>,<<"String">>}, 273 | {<<"ofType">>,null}]}, 274 | {<<"isDeprecated">>,false}, 275 | {<<"deprecationReason">>,null}], 276 | [{<<"name">>,<<"deprecationReason">>}, 277 | {<<"description">>,null}, 278 | {<<"args">>,[]}, 279 | {<<"type">>, 280 | [{<<"kind">>,<<"SCALAR">>}, 281 | {<<"name">>,<<"String">>}, 282 | {<<"ofType">>,null}]}, 283 | {<<"isDeprecated">>,false}, 284 | {<<"deprecationReason">>,null}], 285 | [{<<"name">>,<<"args">>}, 286 | {<<"description">>,null}, 287 | {<<"args">>,[]}, 288 | {<<"type">>, 289 | [{<<"kind">>,<<"LIST">>}, 290 | {<<"name">>,null}, 291 | {<<"ofType">>, 292 | [{<<"kind">>,<<"OBJECT">>}, 293 | {<<"name">>,<<"__InputValue">>}, 294 | {<<"ofType">>,null}]}]}, 295 | {<<"isDeprecated">>,false}, 296 | {<<"deprecationReason">>,null}], 297 | [{<<"name">>,<<"__typename">>}, 298 | {<<"description">>,<<"Name of current type">>}, 299 | {<<"args">>,[]}, 300 | {<<"type">>, 301 | [{<<"kind">>,<<"SCALAR">>}, 302 | {<<"name">>,<<"String">>}, 303 | {<<"ofType">>,null}]}, 304 | {<<"isDeprecated">>,false}, 305 | {<<"deprecationReason">>,null}]]}, 306 | {<<"inputFields">>,null}, 307 | {<<"interfaces">>,[]}, 308 | {<<"enumValues">>,null}, 309 | {<<"possibleTypes">>,null}], 310 | [{<<"kind">>,<<"OBJECT">>}, 311 | {<<"name">>,<<"__Type">>}, 312 | {<<"description">>,<<"Type Introspection">>}, 313 | {<<"fields">>, 314 | [[{<<"name">>,<<"possibleTypes">>}, 315 | {<<"description">>,null}, 316 | {<<"args">>,[]}, 317 | {<<"type">>, 318 | [{<<"kind">>,<<"LIST">>}, 319 | {<<"name">>,null}, 320 | {<<"ofType">>, 321 | [{<<"kind">>,<<"OBJECT">>}, 322 | {<<"name">>,<<"__Type">>}, 323 | {<<"ofType">>,null}]}]}, 324 | {<<"isDeprecated">>,false}, 325 | {<<"deprecationReason">>,null}], 326 | [{<<"name">>,<<"ofType">>}, 327 | {<<"description">>,null}, 328 | {<<"args">>,[]}, 329 | {<<"type">>, 330 | [{<<"kind">>,<<"OBJECT">>}, 331 | {<<"name">>,<<"__Type">>}, 332 | {<<"ofType">>,null}]}, 333 | {<<"isDeprecated">>,false}, 334 | {<<"deprecationReason">>,null}], 335 | [{<<"name">>,<<"name">>}, 336 | {<<"description">>,null}, 337 | {<<"args">>,[]}, 338 | {<<"type">>, 339 | [{<<"kind">>,<<"SCALAR">>}, 340 | {<<"name">>,<<"String">>}, 341 | {<<"ofType">>,null}]}, 342 | {<<"isDeprecated">>,false}, 343 | {<<"deprecationReason">>,null}], 344 | [{<<"name">>,<<"kind">>}, 345 | {<<"description">>,null}, 346 | {<<"args">>,[]}, 347 | {<<"type">>, 348 | [{<<"kind">>,<<"SCALAR">>}, 349 | {<<"name">>,<<"String">>}, 350 | {<<"ofType">>,null}]}, 351 | {<<"isDeprecated">>,false}, 352 | {<<"deprecationReason">>,null}], 353 | [{<<"name">>,<<"interfaces">>}, 354 | {<<"description">>,null}, 355 | {<<"args">>,[]}, 356 | {<<"type">>, 357 | [{<<"kind">>,<<"LIST">>}, 358 | {<<"name">>,null}, 359 | {<<"ofType">>, 360 | [{<<"kind">>,<<"SCALAR">>}, 361 | {<<"name">>,<<"Int">>}, 362 | {<<"ofType">>,null}]}]}, 363 | {<<"isDeprecated">>,false}, 364 | {<<"deprecationReason">>,null}], 365 | [{<<"name">>,<<"inputFields">>}, 366 | {<<"description">>,null}, 367 | {<<"args">>,[]}, 368 | {<<"type">>, 369 | [{<<"kind">>,<<"LIST">>}, 370 | {<<"name">>,null}, 371 | {<<"ofType">>, 372 | [{<<"kind">>,<<"SCALAR">>}, 373 | {<<"name">>,<<"Int">>}, 374 | {<<"ofType">>,null}]}]}, 375 | {<<"isDeprecated">>,false}, 376 | {<<"deprecationReason">>,null}], 377 | [{<<"name">>,<<"fields">>}, 378 | {<<"description">>,null}, 379 | {<<"args">>, 380 | [[{<<"name">>,<<"includeDeprecated">>}, 381 | {<<"description">>,null}, 382 | {<<"type">>, 383 | [{<<"kind">>,<<"SCALAR">>}, 384 | {<<"name">>,<<"Boolean">>}, 385 | {<<"ofType">>,null}]}, 386 | {<<"defaultValue">>,null}]]}, 387 | {<<"type">>, 388 | [{<<"kind">>,<<"LIST">>}, 389 | {<<"name">>,null}, 390 | {<<"ofType">>, 391 | [{<<"kind">>,<<"OBJECT">>}, 392 | {<<"name">>,<<"__Field">>}, 393 | {<<"ofType">>,null}]}]}, 394 | {<<"isDeprecated">>,false}, 395 | {<<"deprecationReason">>,null}], 396 | [{<<"name">>,<<"enumValues">>}, 397 | {<<"description">>,null}, 398 | {<<"args">>,[]}, 399 | {<<"type">>, 400 | [{<<"kind">>,<<"LIST">>}, 401 | {<<"name">>,null}, 402 | {<<"ofType">>, 403 | [{<<"kind">>,<<"OBJECT">>}, 404 | {<<"name">>,<<"EnumValue">>}, 405 | {<<"ofType">>,null}]}]}, 406 | {<<"isDeprecated">>,false}, 407 | {<<"deprecationReason">>,null}], 408 | [{<<"name">>,<<"description">>}, 409 | {<<"description">>,null}, 410 | {<<"args">>,[]}, 411 | {<<"type">>, 412 | [{<<"kind">>,<<"SCALAR">>}, 413 | {<<"name">>,<<"String">>}, 414 | {<<"ofType">>,null}]}, 415 | {<<"isDeprecated">>,false}, 416 | {<<"deprecationReason">>,null}], 417 | [{<<"name">>,<<"__typename">>}, 418 | {<<"description">>,<<"Name of current type">>}, 419 | {<<"args">>,[]}, 420 | {<<"type">>, 421 | [{<<"kind">>,<<"SCALAR">>}, 422 | {<<"name">>,<<"String">>}, 423 | {<<"ofType">>,null}]}, 424 | {<<"isDeprecated">>,false}, 425 | {<<"deprecationReason">>,null}]]}, 426 | {<<"inputFields">>,null}, 427 | {<<"interfaces">>,[]}, 428 | {<<"enumValues">>,null}, 429 | {<<"possibleTypes">>,null}], 430 | [{<<"kind">>,<<"OBJECT">>}, 431 | {<<"name">>,<<"__Schema">>}, 432 | {<<"description">>,<<"Schema Introspection">>}, 433 | {<<"fields">>, 434 | [[{<<"name">>,<<"types">>}, 435 | {<<"description">>,null}, 436 | {<<"args">>,[]}, 437 | {<<"type">>, 438 | [{<<"kind">>,<<"LIST">>}, 439 | {<<"name">>,null}, 440 | {<<"ofType">>, 441 | [{<<"kind">>,<<"OBJECT">>}, 442 | {<<"name">>,<<"__Type">>}, 443 | {<<"ofType">>,null}]}]}, 444 | {<<"isDeprecated">>,false}, 445 | {<<"deprecationReason">>,null}], 446 | [{<<"name">>,<<"subscriptionType">>}, 447 | {<<"description">>,null}, 448 | {<<"args">>,[]}, 449 | {<<"type">>, 450 | [{<<"kind">>,<<"OBJECT">>}, 451 | {<<"name">>,<<"__Type">>}, 452 | {<<"ofType">>,null}]}, 453 | {<<"isDeprecated">>,false}, 454 | {<<"deprecationReason">>,null}], 455 | [{<<"name">>,<<"queryType">>}, 456 | {<<"description">>,null}, 457 | {<<"args">>,[]}, 458 | {<<"type">>, 459 | [{<<"kind">>,<<"OBJECT">>}, 460 | {<<"name">>,<<"__Type">>}, 461 | {<<"ofType">>,null}]}, 462 | {<<"isDeprecated">>,false}, 463 | {<<"deprecationReason">>,null}], 464 | [{<<"name">>,<<"mutationType">>}, 465 | {<<"description">>,null}, 466 | {<<"args">>,[]}, 467 | {<<"type">>, 468 | [{<<"kind">>,<<"OBJECT">>}, 469 | {<<"name">>,<<"__Type">>}, 470 | {<<"ofType">>,null}]}, 471 | {<<"isDeprecated">>,false}, 472 | {<<"deprecationReason">>,null}], 473 | [{<<"name">>,<<"directives">>}, 474 | {<<"description">>,null}, 475 | {<<"args">>,[]}, 476 | {<<"type">>, 477 | [{<<"kind">>,<<"LIST">>}, 478 | {<<"name">>,null}, 479 | {<<"ofType">>, 480 | [{<<"kind">>,<<"OBJECT">>}, 481 | {<<"name">>,<<"__Directive">>}, 482 | {<<"ofType">>,null}]}]}, 483 | {<<"isDeprecated">>,false}, 484 | {<<"deprecationReason">>,null}], 485 | [{<<"name">>,<<"__typename">>}, 486 | {<<"description">>,<<"Name of current type">>}, 487 | {<<"args">>,[]}, 488 | {<<"type">>, 489 | [{<<"kind">>,<<"SCALAR">>}, 490 | {<<"name">>,<<"String">>}, 491 | {<<"ofType">>,null}]}, 492 | {<<"isDeprecated">>,false}, 493 | {<<"deprecationReason">>,null}]]}, 494 | {<<"inputFields">>,null}, 495 | {<<"interfaces">>,[]}, 496 | {<<"enumValues">>,null}, 497 | {<<"possibleTypes">>,null}], 498 | [{<<"kind">>,<<"ENUM">>}, 499 | {<<"name">>,<<"Test">>}, 500 | {<<"description">>,<<"Test description">>}, 501 | {<<"fields">>,[]}, 502 | {<<"inputFields">>,null}, 503 | {<<"interfaces">>,[]}, 504 | {<<"enumValues">>, 505 | [[{<<"name">>,<<"ONE">>}, 506 | {<<"description">>, 507 | <<"This is 1 represent as text">>}, 508 | {<<"isDeprecated">>,false}, 509 | {<<"deprecationReason">>,null}], 510 | [{<<"name">>,<<"TWO">>}, 511 | {<<"description">>, 512 | <<"This is 2 represent as text">>}, 513 | {<<"isDeprecated">>,false}, 514 | {<<"deprecationReason">>,null}]]}, 515 | {<<"possibleTypes">>,null}], 516 | [{<<"kind">>,<<"SCALAR">>}, 517 | {<<"name">>,<<"Float">>}, 518 | {<<"description">>, 519 | <<"The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).">>}, 520 | {<<"fields">>,[]}, 521 | {<<"inputFields">>,null}, 522 | {<<"interfaces">>,[]}, 523 | {<<"enumValues">>,null}, 524 | {<<"possibleTypes">>,null}], 525 | [{<<"kind">>,<<"SCALAR">>}, 526 | {<<"name">>,<<"Int">>}, 527 | {<<"description">>, 528 | <<"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^53 - 1) and 2^53 - 1 since represented in JSON as double-precision floating point numbers specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).">>}, 529 | {<<"fields">>,[]}, 530 | {<<"inputFields">>,null}, 531 | {<<"interfaces">>,[]}, 532 | {<<"enumValues">>,null}, 533 | {<<"possibleTypes">>,null}], 534 | [{<<"kind">>,<<"OBJECT">>}, 535 | {<<"name">>,<<"CustomObject3">>}, 536 | {<<"description">>, 537 | <<"Test Object introspection in list">>}, 538 | {<<"fields">>, 539 | [[{<<"name">>,<<"boolean">>}, 540 | {<<"description">>,null}, 541 | {<<"args">>,[]}, 542 | {<<"type">>, 543 | [{<<"kind">>,<<"SCALAR">>}, 544 | {<<"name">>,<<"Boolean">>}, 545 | {<<"ofType">>,null}]}, 546 | {<<"isDeprecated">>,false}, 547 | {<<"deprecationReason">>,null}], 548 | [{<<"name">>,<<"__typename">>}, 549 | {<<"description">>,<<"Name of current type">>}, 550 | {<<"args">>,[]}, 551 | {<<"type">>, 552 | [{<<"kind">>,<<"SCALAR">>}, 553 | {<<"name">>,<<"String">>}, 554 | {<<"ofType">>,null}]}, 555 | {<<"isDeprecated">>,false}, 556 | {<<"deprecationReason">>,null}]]}, 557 | {<<"inputFields">>,null}, 558 | {<<"interfaces">>,[]}, 559 | {<<"enumValues">>,null}, 560 | {<<"possibleTypes">>,null}], 561 | [{<<"kind">>,<<"OBJECT">>}, 562 | {<<"name">>,<<"CustomObject2">>}, 563 | {<<"description">>, 564 | <<"Test Object introspection in list">>}, 565 | {<<"fields">>, 566 | [[{<<"name">>,<<"boolean">>}, 567 | {<<"description">>,null}, 568 | {<<"args">>,[]}, 569 | {<<"type">>, 570 | [{<<"kind">>,<<"SCALAR">>}, 571 | {<<"name">>,<<"Boolean">>}, 572 | {<<"ofType">>,null}]}, 573 | {<<"isDeprecated">>,false}, 574 | {<<"deprecationReason">>,null}], 575 | [{<<"name">>,<<"__typename">>}, 576 | {<<"description">>,<<"Name of current type">>}, 577 | {<<"args">>,[]}, 578 | {<<"type">>, 579 | [{<<"kind">>,<<"SCALAR">>}, 580 | {<<"name">>,<<"String">>}, 581 | {<<"ofType">>,null}]}, 582 | {<<"isDeprecated">>,false}, 583 | {<<"deprecationReason">>,null}]]}, 584 | {<<"inputFields">>,null}, 585 | {<<"interfaces">>,[]}, 586 | {<<"enumValues">>,null}, 587 | {<<"possibleTypes">>,null}], 588 | [{<<"kind">>,<<"SCALAR">>}, 589 | {<<"name">>,<<"String">>}, 590 | {<<"description">>, 591 | <<"The `String` scalar type represents textual data, represented as UTF-8character sequences. The String type is most often used by GraphQL torepresent free-form human-readable text.">>}, 592 | {<<"fields">>,[]}, 593 | {<<"inputFields">>,null}, 594 | {<<"interfaces">>,[]}, 595 | {<<"enumValues">>,null}, 596 | {<<"possibleTypes">>,null}], 597 | [{<<"kind">>,<<"SCALAR">>}, 598 | {<<"name">>,<<"Boolean">>}, 599 | {<<"description">>, 600 | <<"The `Boolean` scalar type represents `true` or `false`.">>}, 601 | {<<"fields">>,[]}, 602 | {<<"inputFields">>,null}, 603 | {<<"interfaces">>,[]}, 604 | {<<"enumValues">>,null}, 605 | {<<"possibleTypes">>,null}], 606 | [{<<"kind">>,<<"OBJECT">>}, 607 | {<<"name">>,<<"CustomObject">>}, 608 | {<<"description">>, 609 | <<"Test Object introspection in list">>}, 610 | {<<"fields">>, 611 | [[{<<"name">>,<<"boolean">>}, 612 | {<<"description">>,null}, 613 | {<<"args">>,[]}, 614 | {<<"type">>, 615 | [{<<"kind">>,<<"SCALAR">>}, 616 | {<<"name">>,<<"Boolean">>}, 617 | {<<"ofType">>,null}]}, 618 | {<<"isDeprecated">>,true}, 619 | {<<"deprecationReason">>,<<"Deprecation test">>}], 620 | [{<<"name">>,<<"__typename">>}, 621 | {<<"description">>,<<"Name of current type">>}, 622 | {<<"args">>,[]}, 623 | {<<"type">>, 624 | [{<<"kind">>,<<"SCALAR">>}, 625 | {<<"name">>,<<"String">>}, 626 | {<<"ofType">>,null}]}, 627 | {<<"isDeprecated">>,false}, 628 | {<<"deprecationReason">>,null}]]}, 629 | {<<"inputFields">>,null}, 630 | {<<"interfaces">>,[]}, 631 | {<<"enumValues">>,null}, 632 | {<<"possibleTypes">>,null}], 633 | [{<<"kind">>,<<"UNION">>}, 634 | {<<"name">>,<<"UnionTest">>}, 635 | {<<"description">>,<<"Test union">>}, 636 | {<<"fields">>,[]}, 637 | {<<"inputFields">>,null}, 638 | {<<"interfaces">>,[]}, 639 | {<<"enumValues">>,null}, 640 | {<<"possibleTypes">>, 641 | [[{<<"kind">>,<<"OBJECT">>}, 642 | {<<"name">>,<<"CustomObject">>}, 643 | {<<"ofType">>,null}], 644 | [{<<"kind">>,<<"OBJECT">>}, 645 | {<<"name">>,<<"CustomObject2">>}, 646 | {<<"ofType">>,null}], 647 | [{<<"kind">>,<<"OBJECT">>}, 648 | {<<"name">>,<<"CustomObject3">>}, 649 | {<<"ofType">>,null}]]}], 650 | [{<<"kind">>,<<"OBJECT">>}, 651 | {<<"name">>,<<"QueryRoot">>}, 652 | {<<"description">>,<<"Test Query">>}, 653 | {<<"fields">>, 654 | [[{<<"name">>,<<"union">>}, 655 | {<<"description">>,null}, 656 | {<<"args">>,[]}, 657 | {<<"type">>, 658 | [{<<"kind">>,<<"UNION">>}, 659 | {<<"name">>,<<"UnionTest">>}, 660 | {<<"ofType">>,null}]}, 661 | {<<"isDeprecated">>,false}, 662 | {<<"deprecationReason">>,null}], 663 | [{<<"name">>,<<"string">>}, 664 | {<<"description">>,null}, 665 | {<<"args">>,[]}, 666 | {<<"type">>, 667 | [{<<"kind">>,<<"SCALAR">>}, 668 | {<<"name">>,<<"String">>}, 669 | {<<"ofType">>,null}]}, 670 | {<<"isDeprecated">>,false}, 671 | {<<"deprecationReason">>,null}], 672 | [{<<"name">>,<<"object_list">>}, 673 | {<<"description">>,null}, 674 | {<<"args">>,[]}, 675 | {<<"type">>, 676 | [{<<"kind">>,<<"LIST">>}, 677 | {<<"name">>,null}, 678 | {<<"ofType">>, 679 | [{<<"kind">>,<<"OBJECT">>}, 680 | {<<"name">>,<<"CustomObject">>}, 681 | {<<"ofType">>,null}]}]}, 682 | {<<"isDeprecated">>,false}, 683 | {<<"deprecationReason">>,null}], 684 | [{<<"name">>,<<"non_null">>}, 685 | {<<"description">>,null}, 686 | {<<"args">>,[]}, 687 | {<<"type">>, 688 | [{<<"kind">>,<<"NON_NULL">>}, 689 | {<<"name">>,null}, 690 | {<<"ofType">>, 691 | [{<<"kind">>,<<"SCALAR">>}, 692 | {<<"name">>,<<"Int">>}, 693 | {<<"ofType">>,null}]}]}, 694 | {<<"isDeprecated">>,false}, 695 | {<<"deprecationReason">>,null}], 696 | [{<<"name">>,<<"integer">>}, 697 | {<<"description">>,null}, 698 | {<<"args">>,[]}, 699 | {<<"type">>, 700 | [{<<"kind">>,<<"SCALAR">>}, 701 | {<<"name">>,<<"Int">>}, 702 | {<<"ofType">>,null}]}, 703 | {<<"isDeprecated">>,false}, 704 | {<<"deprecationReason">>,null}], 705 | [{<<"name">>,<<"float">>}, 706 | {<<"description">>,null}, 707 | {<<"args">>,[]}, 708 | {<<"type">>, 709 | [{<<"kind">>,<<"SCALAR">>}, 710 | {<<"name">>,<<"Float">>}, 711 | {<<"ofType">>,null}]}, 712 | {<<"isDeprecated">>,false}, 713 | {<<"deprecationReason">>,null}], 714 | [{<<"name">>,<<"enums_args">>}, 715 | {<<"description">>,null}, 716 | {<<"args">>, 717 | [[{<<"name">>,<<"int">>}, 718 | {<<"description">>,null}, 719 | {<<"type">>, 720 | [{<<"kind">>,<<"ENUM">>}, 721 | {<<"name">>,<<"Test">>}, 722 | {<<"ofType">>,null}]}, 723 | {<<"defaultValue">>,null}]]}, 724 | {<<"type">>, 725 | [{<<"kind">>,<<"SCALAR">>}, 726 | {<<"name">>,<<"Int">>}, 727 | {<<"ofType">>,null}]}, 728 | {<<"isDeprecated">>,false}, 729 | {<<"deprecationReason">>,null}], 730 | [{<<"name">>,<<"boolean">>}, 731 | {<<"description">>,null}, 732 | {<<"args">>,[]}, 733 | {<<"type">>, 734 | [{<<"kind">>,<<"SCALAR">>}, 735 | {<<"name">>,<<"Boolean">>}, 736 | {<<"ofType">>,null}]}, 737 | {<<"isDeprecated">>,false}, 738 | {<<"deprecationReason">>,null}], 739 | [{<<"name">>,<<"__typename">>}, 740 | {<<"description">>,<<"Name of current type">>}, 741 | {<<"args">>,[]}, 742 | {<<"type">>, 743 | [{<<"kind">>,<<"SCALAR">>}, 744 | {<<"name">>,<<"String">>}, 745 | {<<"ofType">>,null}]}, 746 | {<<"isDeprecated">>,false}, 747 | {<<"deprecationReason">>,null}], 748 | [{<<"name">>,<<"__schema">>}, 749 | {<<"description">>,null}, 750 | {<<"args">>,[]}, 751 | {<<"type">>, 752 | [{<<"kind">>,<<"OBJECT">>}, 753 | {<<"name">>,<<"__Schema">>}, 754 | {<<"ofType">>,null}]}, 755 | {<<"isDeprecated">>,false}, 756 | {<<"deprecationReason">>,null}]]}, 757 | {<<"inputFields">>,null}, 758 | {<<"interfaces">>,[]}, 759 | {<<"enumValues">>,null}, 760 | {<<"possibleTypes">>,null}]]}, 761 | {<<"directives">>,[]}]}], 762 | errors => []}. 763 | 764 | schema() -> ?SCHEMA(#{ 765 | query => fun queryRoot/0 766 | }). 767 | 768 | queryRoot() -> ?OBJECT("QueryRoot", "Test Query", #{ 769 | "boolean" => ?FIELD(?BOOLEAN), 770 | "string" => ?FIELD(?STRING), 771 | "integer" => ?FIELD(?INT), 772 | "float" => ?FIELD(?FLOAT), 773 | "non_null" => ?FIELD(?NON_NULL(?INT)), 774 | "object_list" => ?FIELD(?LIST(fun object/0)), 775 | "enums_args" => #{ 776 | type => ?INT, 777 | args => #{ 778 | <<"int">> => #{ type => fun enum_two/0} 779 | } 780 | }, 781 | "union" => #{ 782 | type => ?UNION(<<"UnionTest">>, <<"Test union">>, [ 783 | fun object/0, 784 | fun object2/0, 785 | fun object3/0 786 | ]) 787 | } 788 | }). 789 | 790 | object() -> ?OBJECT("CustomObject", "Test Object introspection in list", #{ 791 | "boolean" => ?DEPRECATED("Deprecation test", ?FIELD(?BOOLEAN)) 792 | }). 793 | 794 | object2() -> ?OBJECT("CustomObject2", "Test Object introspection in list", #{ 795 | "boolean" => ?FIELD(?BOOLEAN) 796 | }). 797 | 798 | object3() -> ?OBJECT("CustomObject3", "Test Object introspection in list", #{ 799 | "boolean" => ?FIELD(?BOOLEAN) 800 | }). 801 | 802 | 803 | enum_two() -> 804 | ?ENUM(<<"Test">>, <<"Test description">>, [ 805 | ?ENUM_VAL(1, <<"ONE">>, <<"This is 1 represent as text">>), 806 | ?ENUM_VAL(1, <<"TWO">>, <<"This is 2 represent as text">>) 807 | ]). 808 | 809 | introspection_test()-> 810 | ?assertEqual( 811 | expect(), 812 | graphql:execute(schema(), query(), #{}) 813 | ). 814 | -------------------------------------------------------------------------------- /src/test/graphql_mutation_test.erl: -------------------------------------------------------------------------------- 1 | -include_lib("eunit/include/eunit.hrl"). 2 | -include("types.hrl"). 3 | -module(graphql_mutation_test). 4 | 5 | 6 | schema() -> ?SCHEMA(#{ 7 | mutation => fun mutation/0 8 | }). 9 | 10 | increment_resolver(Obj) -> {ok, Obj + 1}. 11 | 12 | mutation()-> ?OBJECT("Mutation", "", #{ 13 | "a" => ?FIELD(?INT, "", fun increment_resolver/1) 14 | }). 15 | 16 | first_test() -> 17 | Document = <<"mutation { a }">>, 18 | Expect = #{ 19 | <<"a">> => 1 20 | }, 21 | 22 | #{data := Result} = graphql:exec(schema(), Document, #{ 23 | initial => 0 24 | }), 25 | 26 | ?assertEqual(Expect, Result). -------------------------------------------------------------------------------- /src/test/graphql_old_test.erl: -------------------------------------------------------------------------------- 1 | -include_lib("eunit/include/eunit.hrl"). 2 | -module(graphql_old_test). 3 | 4 | 5 | recursion_nesting_test()-> 6 | Document = <<"{ 7 | nest { 8 | info 9 | nest { 10 | info 11 | nest { 12 | nest { 13 | info 14 | } 15 | } 16 | } 17 | } 18 | }">>, 19 | 20 | ?assertEqual( #{ 21 | % TODO: fix sorting? 22 | data => [{<<"nest">>, 23 | [{<<"info">>,<<"information does not availiable">>}, 24 | {<<"nest">>, 25 | [{<<"info">>,<<"information does not availiable">>}, 26 | {<<"nest">>, 27 | [{<<"nest">>, 28 | [{<<"info">>, 29 | <<"information does not availiable">>}]}]}]}]}], errors => [] 30 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{}) ). 31 | 32 | arguments_valid_passing_test() -> 33 | Document = <<"{ 34 | arg(hello: \"world\") { 35 | greatings_for 36 | } 37 | }">>, 38 | 39 | ?assertEqual( #{ 40 | data => [ 41 | {<<"arg">>, [ 42 | {<<"greatings_for">>, <<"world">>} 43 | ]} 44 | ], 45 | errors => [] 46 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{}) ). 47 | 48 | 49 | default_argument_passing_test() -> 50 | Document = <<"{ arg { greatings_for } }">>, 51 | ?assertEqual(#{ 52 | data => [ 53 | {<<"arg">>, [ 54 | {<<"greatings_for">>, <<"default value">>} 55 | ]} 56 | ], 57 | errors => [] 58 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 59 | 60 | % is correnct this test? If in schema arguments defined - need it pass to the resolver or not? 61 | no_arguments_passing_test() -> 62 | Document = <<"{ arg_without_defaults { arguments_count } }">>, 63 | ?assertEqual(#{ 64 | data => [ 65 | {<<"arg_without_defaults">>, [ 66 | {<<"arguments_count">>, 1} 67 | ]} 68 | ], 69 | errors => [] 70 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 71 | 72 | map_support_default_resolver_test() -> 73 | Document = <<"{ hello }">>, 74 | ?assertEqual(#{ 75 | data => [ 76 | {<<"hello">>, <<"world">>} 77 | ], 78 | errors => [] 79 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{<<"hello">> => <<"world">>})). 80 | 81 | proplists_support_default_resolver_test() -> 82 | Document = <<"{ hello }">>, 83 | ?assertEqual(#{ 84 | data => [ 85 | {<<"hello">>, <<"proplists">>} 86 | ], 87 | errors => [] 88 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, [{<<"hello">>, <<"proplists">>}])). 89 | 90 | fragment_test()-> 91 | Document = "{ nest { ...NestFragmentTest } } fragment NestFragmentTest on Nest { info }", 92 | ?assertEqual(#{ 93 | data => [ 94 | {<<"nest">>, [{<<"info">>,<<"information does not availiable">>}]} 95 | ], 96 | errors => [] 97 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 98 | 99 | fragment_inline_test()-> 100 | Document = "{ nest { ... on Nest { info } } }", 101 | ?assertEqual(#{ 102 | data => [ 103 | {<<"nest">>, [{<<"info">>,<<"information does not availiable">>}]} 104 | ], 105 | errors => [] 106 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 107 | 108 | subselection_not_provided_error_test() -> 109 | Document = <<"{ range_objects(seq: 2) }">>, 110 | ?assertEqual(#{ 111 | error => <<"No sub selection provided for `ValueObject`">>, 112 | type => complete_value 113 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 114 | 115 | 116 | %%%%% 117 | % Types 118 | 119 | boolean_type_test() -> 120 | Document = <<"{ arg_bool(bool: true) }">>, 121 | ?assertEqual(#{ 122 | data => [ 123 | {<<"arg_bool">>, true} 124 | ], 125 | errors => [] 126 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 127 | 128 | boolean_type_validation_test() -> 129 | Document = <<"{ arg_bool(bool: \"invalid boolean type\") }">>, 130 | ?assertEqual(#{ 131 | error => <<"Unexpected type StringValue, expected BooleanValue">>, 132 | type => type_validation 133 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 134 | 135 | 136 | integer_type_test() -> 137 | Document = <<"{ arg(int: 10) { int } }">>, 138 | ?assertEqual(#{ 139 | data => [ 140 | {<<"arg">>, [ 141 | {<<"int">>, 10} 142 | ]} 143 | ], 144 | errors => [] 145 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 146 | 147 | 148 | list_type_test() -> 149 | Document = <<"{ range(seq: 10) }">>, 150 | ?assertEqual(#{ 151 | data => [ 152 | {<<"range">>, [0,1,2,3,4,5,6,7,8,9,10]} 153 | ], 154 | errors => [] 155 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 156 | 157 | list_of_object_with_list_of_int_test() -> 158 | Document = <<"{ range_objects(seq: 2) { value } }">>, 159 | ?assertEqual(#{data => [ 160 | {<<"range_objects">>, [ 161 | [{<<"value">>,[0]}], 162 | [{<<"value">>,[0,1]}], 163 | [{<<"value">>,[0,1,2]}] 164 | ]} 165 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 166 | 167 | list_in_args_test() -> 168 | Document = <<"{ arg(list: [1,2,3]) { list } }">>, 169 | ?assertEqual(#{data => [ 170 | {<<"arg">>, [ 171 | {<<"list">>, [1,2,3]} 172 | ]} 173 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 174 | 175 | non_null_valid_test()-> 176 | Document = <<"{ non_null(int: 10) }">>, 177 | ?assertEqual(#{data => [ 178 | {<<"non_null">>, 10} 179 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 180 | 181 | non_null_invalid_arguments_test()-> 182 | Document = <<"{ non_null }">>, 183 | ?assertEqual(#{ 184 | error => <<"Null value provided to non null type">>, 185 | type => non_null 186 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 187 | 188 | non_null_invalid_result_test()-> 189 | Document = <<"{ non_null_invalid }">>, 190 | ?assertEqual(#{ 191 | error => <<"Non null type cannot be null">>, 192 | type => complete_value 193 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 194 | 195 | %%% Enum 196 | 197 | enum_arg_test() -> 198 | Document = <<"{ enum(e: ONE) }">>, 199 | ?assertEqual(#{data => [ 200 | {<<"enum">>, 1} 201 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 202 | 203 | enum_arg_null_test() -> 204 | Document = <<"{ enum }">>, 205 | ?assertEqual(#{data => [ 206 | {<<"enum">>, null} 207 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 208 | 209 | enum_field_test() -> 210 | Document = <<"{ enum_value(e: ONE) }">>, 211 | ?assertEqual(#{data => [ 212 | {<<"enum_value">>, <<"ONE">>} 213 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 214 | 215 | enum_list_field_test() -> 216 | Document = <<"{ enum_list_value(e: [ONE, TWO]) }">>, 217 | ?assertEqual(#{data => [ 218 | {<<"enum_list_value">>, [<<"ONE">>, <<"TWO">>]} 219 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 220 | 221 | enum_field_null_test() -> 222 | Document = <<"{ enum_value }">>, 223 | ?assertEqual(#{data => [ 224 | {<<"enum_value">>, null} 225 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 226 | 227 | enum_non_null_test() -> 228 | Document = <<"{ enum_non_null(e: ONE) }">>, 229 | ?assertEqual(#{data => [ 230 | {<<"enum_non_null">>, 1} 231 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 232 | 233 | enum_non_null_invalid_test() -> 234 | Document = <<"{ enum_non_null }">>, 235 | ?assertEqual(#{ 236 | error => <<"Null value provided to non null type">>, 237 | type => non_null 238 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 239 | 240 | enum_error_test() -> 241 | Document = <<"{ enum(e: MANY) }">>, 242 | ?assertEqual(#{ 243 | error => <<"Cannot find enum: MANY">>, 244 | type => enum 245 | }, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 246 | 247 | union_1_test() -> 248 | Document = <<"{ union(type: HELLO) { 249 | ... on Hello { 250 | name 251 | } 252 | ... on Nest { 253 | info 254 | } 255 | } }">>, 256 | ?assertEqual(#{data => [ 257 | {<<"union">>, [{<<"name">>, <<"Union">>}]} 258 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 259 | 260 | union_2_test() -> 261 | Document = <<"{ union(type: NEST) { 262 | ... on Hello { 263 | name 264 | } 265 | ... on Nest { 266 | info 267 | } 268 | } }">>, 269 | ?assertEqual(#{data => [ 270 | {<<"union">>, [{<<"info">>, <<"information does not availiable">>}]} 271 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 272 | 273 | union_1_default_resolve_type_test() -> 274 | Document = <<"{ union_default_resolve_type(type: HELLO) { 275 | ... on Hello { 276 | name 277 | } 278 | ... on Nest { 279 | info 280 | } 281 | } }">>, 282 | ?assertEqual(#{data => [ 283 | {<<"union_default_resolve_type">>, [{<<"name">>, <<"Union">>}]} 284 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 285 | 286 | union_2_default_resolve_type_test() -> 287 | Document = <<"{ union_default_resolve_type(type: NEST) { 288 | ... on Hello { 289 | name 290 | } 291 | ... on Nest { 292 | info 293 | } 294 | } }">>, 295 | ?assertEqual(#{data => [ 296 | {<<"union_default_resolve_type">>, [{<<"info">>, <<"information does not availiable">>}]} 297 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 298 | 299 | %%% Variables 300 | 301 | variable_bool_test() -> 302 | Document = <<"query($var: Boolean) { arg(bool: $var) { bool } }">>, 303 | VariableValues = #{ 304 | <<"var">> => true 305 | }, 306 | ?assertEqual(#{data => [ 307 | {<<"arg">>, [{<<"bool">>, true}]} 308 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, VariableValues, #{}, #{})). 309 | 310 | variable_default_bool_test() -> 311 | Document = <<"query($var: Boolean = true) { arg(bool: $var) { bool } }">>, 312 | ?assertEqual(#{data => [ 313 | {<<"arg">>, [{<<"bool">>, true}]} 314 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 315 | 316 | variable_enum_one_test() -> 317 | Document = <<"query($var: EnumOneTwo) { arg(enum: $var) { enum } }">>, 318 | VariableValues = #{ 319 | <<"var">> => <<"ONE">> 320 | }, 321 | ?assertEqual(#{data => [ 322 | {<<"arg">>, [{<<"enum">>, <<"ONE">>}]} 323 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, VariableValues, #{}, #{})). 324 | 325 | variable_enum_two_test() -> 326 | Document = <<"query($var: EnumOneTwo) { arg(enum: $var) { enum } }">>, 327 | VariableValues = #{ 328 | <<"var">> => <<"TWO">> 329 | }, 330 | ?assertEqual(#{data => [ 331 | {<<"arg">>, [{<<"enum">>, <<"TWO">>}]} 332 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, VariableValues, #{}, #{})). 333 | 334 | variable_default_enum_test() -> 335 | Document = <<"query($var: EnumOneTwo = ONE) { arg(enum: $var) { enum } }">>, 336 | ?assertEqual(#{data => [ 337 | {<<"arg">>, [{<<"enum">>, <<"ONE">>}]} 338 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 339 | 340 | variable_float_test() -> 341 | Document = <<"query($var: Float) { arg(float: $var) { float } }">>, 342 | VariableValues = #{ 343 | <<"var">> => 100.500 344 | }, 345 | ?assertEqual(#{data => [ 346 | {<<"arg">>, [{<<"float">>, 100.5}]} 347 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, VariableValues, #{}, #{})). 348 | 349 | variable_default_float_test() -> 350 | Document = <<"query($var: FLOAT = 1.5) { arg(float: $var) { float } }">>, 351 | ?assertEqual(#{data => [ 352 | {<<"arg">>, [{<<"float">>, 1.5}]} 353 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 354 | 355 | variable_int_test() -> 356 | Document = <<"query($var: Int) { arg(int: $var) { int } }">>, 357 | VariableValues = #{ 358 | <<"var">> => 100500 359 | }, 360 | ?assertEqual(#{data => [ 361 | {<<"arg">>, [{<<"int">>, 100500}]} 362 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, VariableValues, #{}, #{})). 363 | 364 | variable_default_int_test() -> 365 | Document = <<"query($var: Int = 100500) { arg(int: $var) { int } }">>, 366 | ?assertEqual(#{data => [ 367 | {<<"arg">>, [{<<"int">>, 100500}]} 368 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 369 | 370 | variable_list_test() -> 371 | Document = <<"query($var: [Int]) { arg(list: $var) { list } }">>, 372 | VariableValues = #{ 373 | <<"var">> => [1,0,0,5,0,0] 374 | }, 375 | ?assertEqual(#{data => [ 376 | {<<"arg">>, [{<<"list">>, [1,0,0,5,0,0]}]} 377 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, VariableValues, #{}, #{})). 378 | 379 | variable_null_list_test() -> 380 | Document = <<"query($var: [Int]) { arg(list: $var) { list } }">>, 381 | VariableValues = #{ 382 | <<"var">> => null 383 | }, 384 | ?assertEqual(#{data => [ 385 | {<<"arg">>, [{<<"list">>, null}]} 386 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, VariableValues, #{}, #{})). 387 | 388 | variable_default_list_test() -> 389 | Document = <<"query($var: [Int] = [1,0,0,5,0,0]) { arg(list: $var) { list } }">>, 390 | ?assertEqual(#{data => [ 391 | {<<"arg">>, [{<<"list">>, [1,0,0,5,0,0]}]} 392 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, #{})). 393 | 394 | variable_non_null_test() -> 395 | Document = <<"query($var: Int!) { arg_non_null(int: $var) { int } }">>, 396 | VariableValues = #{ 397 | <<"var">> => 100500 398 | }, 399 | ?assertEqual(#{data => [ 400 | {<<"arg_non_null">>, [{<<"int">>, 100500}]} 401 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, VariableValues, #{}, #{})). 402 | 403 | variable_non_null_string_test() -> 404 | Document = <<"query($var: String!) { arg_non_null_string(str: $var) { str } }">>, 405 | VariableValues = #{ 406 | <<"var">> => <<"test">> 407 | }, 408 | ?assertEqual(#{data => [ 409 | {<<"arg_non_null_string">>, [{<<"str">>, <<"test">>}]} 410 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, VariableValues, #{}, #{})). 411 | 412 | variable_list_non_null_test() -> 413 | Document = <<"query($var: [Int]!) { arg_non_null_list(list: $var) { list } }">>, 414 | VariableValues = #{ 415 | <<"var">> => [1,2,3] 416 | }, 417 | ?assertEqual(#{data => [ 418 | {<<"arg_non_null_list">>, [{<<"list">>, [1,2,3]}]} 419 | ], errors => []}, graphql:execute(graphql_old_test_schema:schema_root(), Document, VariableValues, #{}, #{})). 420 | -------------------------------------------------------------------------------- /src/test/graphql_old_test_schema.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_old_test_schema). 2 | -include("types.hrl"). 3 | 4 | %% API 5 | -export([schema_root/0]). 6 | 7 | print(Text, Args) -> io:format(Text ++ "~n", Args). 8 | 9 | schema_root()-> ?SCHEMA(#{ 10 | query => fun query/0 11 | }). 12 | 13 | enumOneTwo() -> ?ENUM("EnumOneTwo", "Test description", [ 14 | ?ENUM_VAL(1, <<"ONE">>, "This is 1 represent as text"), 15 | ?ENUM_VAL(2, <<"TWO">>, "This is 2 represent as text") 16 | ]). 17 | 18 | query() -> ?OBJECT("QueryRoot", "This is Root Query Type", #{ 19 | "hello" => ?FIELD(?STRING, "This is hello world field"), 20 | "range" => ?FIELD(?LIST(?INT), "Sequence range", #{ 21 | <<"seq">> => #{type => ?INT} 22 | }, 23 | fun(_, #{<<"seq">> := Seq}) -> lists:seq(0, Seq) end 24 | ), 25 | <<"range_objects">> => #{ 26 | type => ?LIST(fun valueObject/0), 27 | args => #{ 28 | <<"seq">> => #{type => ?INT} 29 | }, 30 | resolver => fun(_, #{<<"seq">> := Seq}) -> lists:seq(0, Seq) end 31 | }, 32 | <<"arg">> => ?FIELD(fun arg/0, "Argument schema", #{ 33 | "hello" => ?ARG(?STRING, <<"default value">>, "Hello desction"), 34 | "argument" => #{ type => ?STRING, default => <<"Default argument value">>}, 35 | % scalars 36 | "bool" => ?ARG(?BOOLEAN), 37 | "enum" => ?ARG(fun enumOneTwo/0), 38 | "float" => #{ type => ?FLOAT }, 39 | "int" => #{ type => ?INT }, 40 | "list" => #{ type => ?LIST(?INT) }, 41 | "str" => #{ type => ?STRING } 42 | }, 43 | fun(_, Args) -> Args end 44 | ), 45 | <<"arg_non_null">> => #{ 46 | type => fun arg/0, 47 | args => #{ 48 | <<"int">> => #{type => ?NON_NULL(?INT) } 49 | }, 50 | resolver => fun(_, Args) -> Args end 51 | }, 52 | <<"arg_non_null_string">> => #{ 53 | type => fun arg/0, 54 | args => #{ 55 | <<"str">> => #{type => ?NON_NULL(?STRING) } 56 | }, 57 | resolver => fun(_, Args) -> Args end 58 | }, 59 | <<"arg_non_null_list">> => #{ 60 | type => fun arg/0, 61 | args => #{ 62 | <<"list">> => #{type => ?NON_NULL(?LIST(?INT)) } 63 | }, 64 | resolver => fun(_, Args) -> Args end 65 | }, 66 | <<"arg_bool">> => #{ 67 | type => ?BOOLEAN, 68 | args => #{ 69 | <<"bool">> => #{ type => ?BOOLEAN, description => <<"Proxied argument to result">> } 70 | }, 71 | resolver => fun(_, #{<<"bool">> := Value}) -> Value end 72 | }, 73 | <<"arg_without_resolver">> => #{ 74 | type => fun arg/0, 75 | args => #{ 76 | <<"argument">> => #{ type => ?STRING, default => <<"Default argument value">>} 77 | }, 78 | description => <<"Argument schema">> 79 | }, 80 | <<"arg_without_defaults">> => #{ 81 | type => fun arg/0, 82 | description => <<"Pass arguments count down to three">>, 83 | args => #{ 84 | <<"argument">> => #{ type => ?STRING } 85 | }, 86 | resolver => fun(_, Args) -> 87 | print("RESOLVER FOR arg_without_defaults", []), 88 | #{<<"arguments_count">> => length(maps:keys(Args))} 89 | end 90 | }, 91 | <<"nest">> => #{ 92 | type => fun nest/0, 93 | description => <<"go deeper inside">>, 94 | resolver => fun() -> #{} end 95 | }, 96 | <<"non_null">> => #{ 97 | type => ?NON_NULL(?INT), 98 | args => #{ 99 | <<"int">> => #{type => ?NON_NULL(?INT)} 100 | }, 101 | resolver => fun(_, #{<<"int">> := Int}) -> Int end 102 | }, 103 | <<"non_null_invalid">> => #{ 104 | type => ?NON_NULL(?INT), 105 | resolver => fun() -> null end 106 | }, 107 | <<"enum">> => #{ 108 | type => ?INT, 109 | args => #{ 110 | <<"e">> => #{ 111 | type => ?ENUM(<<"Test">>, <<"Test description">>, [ 112 | ?ENUM_VAL(1, <<"ONE">>, <<"This is 1 represent as text">>), 113 | ?ENUM_VAL(2, <<"TWO">>, <<"This is 2 represent as text">>) 114 | ]) 115 | } 116 | }, 117 | resolver => fun(_, #{<<"e">> := E}) -> E end 118 | }, 119 | <<"enum_value">> => #{ 120 | type => fun enumOneTwo/0, 121 | args => #{ 122 | <<"e">> => #{ 123 | type => fun enumOneTwo/0 124 | } 125 | }, 126 | resolver => fun(_, #{<<"e">> := E}) -> E end 127 | }, 128 | 129 | "enum_list_value" => ?FIELD(?LIST(fun enumOneTwo/0), "Test enum in list", 130 | #{ 131 | "e" => ?ARG(?LIST(fun enumOneTwo/0)) 132 | }, 133 | fun(_, #{<<"e">> := E}) -> E end 134 | ), 135 | 136 | <<"enum_non_null">> => #{ 137 | type => ?INT, 138 | args => #{ 139 | <<"e">> => #{ 140 | type => ?NON_NULL(?ENUM(<<"Test">>, <<"Test description">>, [ 141 | ?ENUM_VAL(1, <<"ONE">>, <<"This is 1 represent as text">>), 142 | ?ENUM_VAL(1, <<"TWO">>, <<"This is 2 represent as text">>) 143 | ])) 144 | } 145 | }, 146 | resolver => fun(_, #{<<"e">> := E}) -> E end 147 | }, 148 | <<"union">> => #{ 149 | type => ?UNION(<<"TestUnionType">>, <<"Many types in one type :)">>, [ 150 | fun nest/0, 151 | fun hello/0 152 | ], fun 153 | ({nest, V}, _)-> {fun nest/0, V}; 154 | ({hello, V}, _) -> {fun hello/0, V} 155 | end), 156 | args => #{ 157 | <<"type">> => #{ 158 | type => ?ENUM(<<"EnumUnionTest">>, <<>>, [ 159 | ?ENUM_VAL(nest, <<"NEST">>, <<>>), 160 | ?ENUM_VAL(hello, <<"HELLO">>, <<>>) 161 | ]) 162 | } 163 | }, 164 | resolver => fun 165 | (_, #{<<"type">> := nest}) -> {nest, #{ }}; 166 | (_, #{<<"type">> := hello}) -> {hello, #{ <<"name">> => <<"Union">>}} 167 | end 168 | }, 169 | <<"union_default_resolve_type">> => #{ 170 | type => ?UNION(<<"TestUnionTypeDefaultRosolve">>, <<"Many types in one type :)">>, [ 171 | fun nest/0, 172 | fun hello/0 173 | ]), 174 | args => #{ 175 | <<"type">> => #{ 176 | type => ?ENUM(<<"EnumUnionTest">>, <<>>, [ 177 | ?ENUM_VAL(nest, <<"NEST">>, <<>>), 178 | ?ENUM_VAL(hello, <<"HELLO">>, <<>>) 179 | ]) 180 | } 181 | }, 182 | resolver => fun 183 | (_, #{<<"type">> := nest}) -> {fun nest/0, #{ }}; 184 | (_, #{<<"type">> := hello}) -> {fun hello/0, #{ <<"name">> => <<"Union">>}} 185 | end 186 | }, 187 | 188 | "newNotation" => ?FIELD(fun newNotation/0, null, fun newNotation_resolver/0) 189 | }). 190 | 191 | hello()-> graphql:objectType(<<"Hello">>, <<>>, #{ 192 | <<"name">> => #{ 193 | type => ?STRING 194 | } 195 | }). 196 | 197 | nest()-> 198 | graphql:objectType(<<"Nest">>, <<"Test schema for nesting">>, #{ 199 | <<"info">> => #{ 200 | type => ?STRING, 201 | description => <<"Information">>, 202 | resolver => fun(_,_) -> <<"information does not availiable">> end 203 | }, 204 | <<"nest">> => #{ 205 | type => fun nest/0, 206 | description => <<"go deeper inside">>, 207 | resolver => fun() -> #{} end 208 | } 209 | }). 210 | 211 | arg()-> ?OBJECT("Arg", "when you pass argument - that return in specified field", #{ 212 | "greatings_for" => ?FIELD(?STRING, "Proxy hello argument to response", 213 | fun(Obj, _) -> maps:get(<<"hello">>, Obj, undefined) end 214 | ), 215 | <<"argument">> => #{ 216 | type => ?STRING, 217 | description => <<"This is argument passed to the parrent. It must be authomaticly resolved">> 218 | }, 219 | 220 | <<"arguments_count">> => #{ 221 | type => ?INT, 222 | description => <<"Passed from parrent - count of arguments">> 223 | }, 224 | 225 | % scalars 226 | <<"bool">> => #{ type => ?BOOLEAN }, 227 | <<"enum">> => #{ type => fun enumOneTwo/0 }, 228 | <<"float">> => #{ type => ?FLOAT }, 229 | <<"int">> => #{ type => ?INT }, 230 | <<"list">> => #{ type => ?LIST(?INT) }, 231 | <<"str">> => #{ type => ?STRING } 232 | }). 233 | 234 | valueObject() -> graphql:objectType(<<"ValueObject">>, <<"">>, #{ 235 | <<"value">> => #{ 236 | type => ?LIST(?INT), 237 | description => <<"range of object value">>, 238 | resolver => fun(Value) -> lists:seq(0, Value) end 239 | } 240 | }). 241 | 242 | newNotation() -> ?OBJECT("NewNotation", "Test macros for new notation style", #{ 243 | "string" => ?FIELD(?STRING, "Field description"), 244 | "deprecated" => ?FIELD(?STRING, "Test deprecation"), 245 | "enum" => ?ENUM("EnumNewNotation", "String description", [ 246 | ?ENUM_VAL(1, "One", "String one description") 247 | ]) 248 | }). 249 | 250 | newNotation_resolver() -> #{ 251 | <<"field">> => <<"field">>, 252 | <<"deprecated">> => <<"okay">>, 253 | <<"enum">> => 1 254 | }. 255 | -------------------------------------------------------------------------------- /src/test/graphql_resolver_test.erl: -------------------------------------------------------------------------------- 1 | -include_lib("eunit/include/eunit.hrl"). 2 | -include("types.hrl"). 3 | -module(graphql_resolver_test). 4 | 5 | %% prepare schema 6 | 7 | schema()-> ?SCHEMA(#{ 8 | query => fun query/0 9 | }). 10 | 11 | query()-> ?OBJECT("Query", "", #{ 12 | "ok" => ?FIELD(?BOOLEAN, "Resolver return {ok, true}", fun()-> {ok, true} end), 13 | "error" => ?FIELD(?BOOLEAN, "{error, Reason}", fun()-> {error, "Because we can"} end), 14 | "error_custom" => ?FIELD(?BOOLEAN, "Custom error", fun() -> 15 | {error, #{ 16 | message => <<"Binary - because whole Reason serialized to json">>, 17 | line => ?LINE, 18 | module => ?MODULE 19 | }} 20 | end) 21 | }). 22 | 23 | %% tests 24 | 25 | ok_test()-> 26 | Document = <<"{ ok }">>, 27 | Expect = #{ 28 | data => #{ 29 | <<"ok">> => true 30 | } 31 | }, 32 | assert(Document, Expect). 33 | 34 | error_test()-> 35 | Document = <<"{ error }">>, 36 | Expect = #{ 37 | errors => [#{message => <<"Because we can">>}] 38 | }, 39 | assert(Document, Expect). 40 | 41 | error_custom_test()-> 42 | Document = <<"{ error_custom }">>, 43 | Expect = #{ 44 | errors => [#{ 45 | message => <<"Binary - because whole Reason serialized to json">>, 46 | line => 17, 47 | module => ?MODULE 48 | }] 49 | }, 50 | assert(Document, Expect). 51 | 52 | 53 | %% helper 54 | assert(Document, Expect)-> 55 | Result = graphql:exec(schema(), Document), 56 | ?assertEqual(Expect, Result). 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/types/graphql_type.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type). 2 | 3 | %% API 4 | -export([ 5 | unwrap_type/1, 6 | optional_string/1, 7 | deprecated/2 8 | ]). 9 | 10 | -export_type([ 11 | optional_string/0, 12 | type/0 13 | ]). 14 | 15 | -type type() :: map() | fun() | {map() | fun(), binary() | string()}. 16 | -type optional_string() :: string() | binary(). 17 | 18 | -spec unwrap_type(type()) -> map(). 19 | unwrap_type(Type) -> 20 | case Type of 21 | #{kind := _} -> Type; 22 | _ when is_function(Type) -> Type(); 23 | _ -> 24 | io:format("Cannot unwrap type: ~p~n", [Type]), 25 | throw({error, field_type, <<"Unexpected type">>}) 26 | end. 27 | 28 | -spec optional_string(null | binary() | string()) -> binary() | null. 29 | optional_string(null) -> null; 30 | optional_string(V) when is_list(V) -> list_to_binary(V); 31 | optional_string(V) when is_binary(V) -> V. 32 | 33 | 34 | -spec deprecated(DeprecationReason::optional_string(), map()) -> map(). 35 | deprecated(DeprecationReason, #{kind := Kind} = FieldOrEnumVal) 36 | when Kind =:= 'FIELD' 37 | orelse Kind =:= 'ENUM_VALUE' -> 38 | FieldOrEnumVal#{ 39 | isDeprecated => true, 40 | deprecationReason => graphql_type:optional_string(DeprecationReason) 41 | }. -------------------------------------------------------------------------------- /src/types/graphql_type_boolean.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type_boolean). 2 | 3 | -export([ 4 | type/0 5 | ]). 6 | 7 | type()-> #{ 8 | kind => 'SCALAR', 9 | name => 'Boolean', 10 | ofType => null, 11 | description => <<"The `Boolean` scalar type represents `true` or `false`.">>, 12 | 13 | serialize => fun serialize/3, 14 | parse_value => fun parse_value/2, 15 | parse_literal => fun parse_literal/2 16 | }. 17 | 18 | serialize(Value,_,_) -> coerce(Value). 19 | 20 | parse_value(null, _) -> coerce(null); 21 | parse_value(#{kind := <<"BooleanValue">>, value := Value}, _) -> coerce(Value); 22 | parse_value(#{kind := 'BooleanValue', value := Value}, _) -> coerce(Value). 23 | 24 | parse_literal(null, _) -> null; 25 | parse_literal(#{kind := 'BooleanValue', value := Value}, _) -> Value; 26 | parse_literal(#{kind := Kind}, _) -> 27 | throw({error, type_validation, <<"Unexpected type ", (atom_to_binary(Kind, utf8))/binary, ", expected BooleanValue">>}). 28 | 29 | 30 | %% TODO: add binary and string representation 31 | -spec coerce(null | integer() | boolean() | binary() | list()) -> boolean() | null. 32 | coerce(null) -> null; 33 | coerce(Value) when is_boolean(Value) -> Value; 34 | coerce(_) -> throw({error, type_validation, <<"Cannot coerce boolean type">>}). 35 | -------------------------------------------------------------------------------- /src/types/graphql_type_enum.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type_enum). 2 | 3 | %% API 4 | -export([ 5 | type/3 6 | ]). 7 | 8 | -spec type(Name::graphql_type:optional_string(), Description::graphql_type:optional_string() | null, list(graphql_type:type())) -> graphql_type:type(). 9 | type(Name, Description, EnumValues)-> 10 | #{ 11 | kind => 'ENUM', 12 | name => graphql_type:optional_string(Name), 13 | description => graphql_type:optional_string(Description), 14 | enumValues => EnumValues, 15 | serialize => fun serialize/3, 16 | parse_value => fun parse_value/2, 17 | parse_literal => fun parse_literal/2 18 | }. 19 | 20 | 21 | serialize(null, _, _) -> null; 22 | serialize(EnumValue, #{enumValues := Values}, _) -> 23 | find_enum_by_val(EnumValue, Values). 24 | 25 | parse_value(null, _) -> null; 26 | parse_value( 27 | #{kind := KindEnum, value := EnumName}, 28 | #{name := ObjectName, enumValues := EnumValues} 29 | ) when <> =:= KindEnum -> 30 | find_enum(EnumName, EnumValues); 31 | parse_value(#{kind := 'EnumValue', value := EnumValue}, #{enumValues := Values})-> 32 | find_enum(EnumValue, Values). 33 | 34 | parse_literal(null, _) -> null; 35 | parse_literal(#{kind := 'EnumValue', value := EnumValue}, #{enumValues := Values})-> 36 | find_enum(EnumValue, Values). 37 | 38 | find_enum(EnumName, [])-> 39 | throw({error, enum, <<"Cannot find enum: ", EnumName/binary>>}); 40 | find_enum(EnumName, [#{name := EnumName, value := Value}|_]) -> Value; 41 | find_enum(EnumName, [_|Tail]) -> find_enum(EnumName, Tail). 42 | 43 | find_enum_by_val(Value, [])-> 44 | throw({error, enum, <<"Cannot find enum name by value: ", Value/binary>>}); 45 | find_enum_by_val(Value, [#{name := EnumName, value := Value}|_]) -> EnumName; 46 | find_enum_by_val(Value, [_|Tail]) -> find_enum_by_val(Value, Tail). 47 | -------------------------------------------------------------------------------- /src/types/graphql_type_enum_value.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type_enum_value). 2 | 3 | %% API 4 | -export([ 5 | type/3 6 | ]). 7 | 8 | -spec type(Value::any(), Name::graphql_type:optional_string(), Description::graphql_type:optional_string()|null) -> graphql_type:type(). 9 | type(Value, Name, Description)-> 10 | #{ 11 | kind => 'ENUM_VALUE', 12 | value => Value, 13 | name => graphql_type:optional_string(Name), 14 | description => graphql_type:optional_string(Description), 15 | isDeprecated => false, 16 | deprecationReason => null 17 | }. 18 | 19 | -------------------------------------------------------------------------------- /src/types/graphql_type_float.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type_float). 2 | 3 | -export([ 4 | type/0 5 | ]). 6 | 7 | type()-> #{ 8 | kind => 'SCALAR', 9 | name => 'Float', 10 | ofType => null, 11 | description => << 12 | "The `Float` scalar type represents signed double-precision fractional ", 13 | "values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point)." 14 | >>, 15 | 16 | serialize => fun serialize/3, 17 | parse_value => fun parse_value/2, 18 | parse_literal => fun parse_literal/2 19 | }. 20 | 21 | serialize(Value,_,_) -> coerce(Value). 22 | 23 | 24 | parse_value(null, _) -> coerce(null); 25 | parse_value(#{kind := <<"FloatValue">>, value := Value}, _) -> coerce(Value); 26 | parse_value(#{kind := 'FloatValue', value := Value}, _) -> coerce(Value). 27 | 28 | -spec parse_literal(map(), map()) -> float(). 29 | parse_literal(null, _) -> null; 30 | parse_literal(#{kind := Kind, value := Value}, _) when 31 | Kind =:= 'IntValue' orelse 32 | Kind =:= 'FloatValue' -> 33 | coerce(Value); 34 | parse_literal(#{kind := Kind}, _) -> 35 | throw({error, type_validation, <<"Unexpected value type. Got: ", (atom_to_binary(Kind, utf8))/binary, ", expected: FloatValue">>}). 36 | 37 | 38 | %% TODO: add binary, string and float representation 39 | -spec coerce(null | float() | integer()) -> float(). 40 | coerce(null) -> null; 41 | coerce(Value) when is_integer(Value) -> Value * 1.0; 42 | coerce(Value) when is_float(Value) -> Value; 43 | coerce(_) -> throw({error, type_validation, <<"Cannot coerce Float type.">>}). 44 | -------------------------------------------------------------------------------- /src/types/graphql_type_int.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type_int). 2 | 3 | -export([ 4 | type/0 5 | ]). 6 | 7 | type()-> #{ 8 | kind => 'SCALAR', 9 | name => 'Int', 10 | ofType => null, 11 | description => << 12 | "The `Int` scalar type represents non-fractional signed whole numeric ", 13 | "values. Int can represent values between -(2^53 - 1) and 2^53 - 1 since ", 14 | "represented in JSON as double-precision floating point numbers specified ", 15 | "by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point)." 16 | >>, 17 | 18 | serialize => fun serialize/3, 19 | parse_value => fun parse_value/2, 20 | parse_literal => fun parse_literal/2 21 | }. 22 | 23 | serialize(Value,_,_) -> coerce(Value). 24 | 25 | parse_value(null,_) -> null; 26 | parse_value(#{kind := <<"IntValue">>, value := Value}, _) -> coerce(Value); 27 | parse_value(#{kind := 'IntValue', value := Value}, _) -> coerce(Value). 28 | 29 | parse_literal(null, _) -> null; 30 | parse_literal(#{kind := Kind, value := Value}, _) when 31 | Kind =:= 'IntValue' -> 32 | coerce(Value); 33 | parse_literal(#{kind := Kind}, _) -> 34 | throw({error, type_validation, <<"Unexpected value type. Got: ", (atom_to_binary(Kind, utf8))/binary, ", expected: IntValue">>}). 35 | 36 | 37 | %% TODO: add binary, string and float representation 38 | -spec coerce(null | float() | integer()) -> integer(). 39 | coerce(null) -> null; 40 | coerce(Value) when is_float(Value)-> 41 | IntValue = round(Value), 42 | case Value > IntValue of 43 | true -> throw({error, type_validation, <<"Cannot coerce float into Int type.">>}); 44 | false -> IntValue 45 | end; 46 | coerce(Value) when is_integer(Value) -> Value; 47 | coerce(_) -> throw({error, type_validation, <<"Cannot coerce Int type.">>}). 48 | -------------------------------------------------------------------------------- /src/types/graphql_type_list.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type_list). 2 | 3 | -export([ 4 | type/1 5 | ]). 6 | 7 | type(InnerType)-> #{ 8 | kind => 'LIST', 9 | name => null, 10 | ofType => InnerType, 11 | 12 | parse_value => fun parse_value/2, 13 | parse_literal => fun parse_literal/2 14 | }. 15 | 16 | parse_value(null, _) -> null; 17 | parse_value( 18 | #{kind := Kind, values := Values}, 19 | #{ofType := InnerType} 20 | ) when Kind =:= 'ListValue' orelse Kind =:= <<"ListValue">> -> 21 | ParseValue = maps:get(parse_value, graphql_type:unwrap_type(InnerType)), 22 | lists:map(fun(Value) -> 23 | ParseValue(Value, InnerType) 24 | end, Values). 25 | 26 | parse_literal(null, _) -> null; 27 | parse_literal(#{kind := 'ListValue', values := Values}, #{ofType := InnerType}) -> 28 | InnerTypeUnwrapped = graphql_type:unwrap_type(InnerType), 29 | ParseLiteral = maps:get(parse_literal, InnerTypeUnwrapped), 30 | lists:map(fun(Value) -> 31 | ParseLiteral(Value, InnerTypeUnwrapped) 32 | end, Values). 33 | -------------------------------------------------------------------------------- /src/types/graphql_type_non_null.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type_non_null). 2 | 3 | -export([ 4 | type/1 5 | ]). 6 | 7 | type(InnerType)-> #{ 8 | kind => 'NON_NULL', 9 | name => null, 10 | ofType => InnerType, 11 | 12 | parse_value => fun parse_value/2, 13 | parse_literal => fun parse_literal/2 14 | }. 15 | 16 | 17 | 18 | parse_value(null, _) -> throw({error, non_null, <<"Null value provided to non null type">>}); 19 | parse_value(#{kind := 'NonNullValue', value := Value}, #{ofType := InnerType}) -> 20 | Type = graphql_type:unwrap_type(InnerType), 21 | ParseValue = maps:get(parse_value, Type), 22 | case ParseValue(Value, Type) of 23 | null -> throw({error, non_null, <<"Null provided to non null type">>}); 24 | Result -> Result 25 | end. 26 | 27 | parse_literal(null, _) -> throw({error, non_null, <<"Null value provided to non null type">>}); 28 | parse_literal(Literal, #{ofType := InnerType})-> 29 | Type = graphql_type:unwrap_type(InnerType), 30 | ParseLiteral = maps:get(parse_literal, Type), 31 | case ParseLiteral(Literal, Type) of 32 | null -> throw({error, non_null, <<"Null provided to non null type">>}); 33 | Result -> Result 34 | end. 35 | -------------------------------------------------------------------------------- /src/types/graphql_type_object.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type_object). 2 | -include("types.hrl"). 3 | 4 | %% API 5 | -export([ 6 | type/2, type/3, 7 | 8 | field/1, field/2, field/3, field/4, 9 | arg/1, arg/2, arg/3, 10 | 11 | get_field/2, 12 | get_args/2, 13 | get_type_from_definition/1, 14 | get_field_resolver/2 15 | ]). 16 | 17 | -type field() :: map(). 18 | -type fields() :: #{ 19 | string() | binary() => field() 20 | } | null. 21 | 22 | -type arg() :: map(). 23 | -type args() :: #{ binary() | string() => arg() }. 24 | 25 | 26 | -type type() :: graphql_type:type() | {graphql_type:type(), graphql_type:optional_string() | null}. 27 | -type name() :: graphql_type:optional_string(). 28 | -type description() :: graphql_type:optional_string() | null. 29 | -type resolver() :: function() | null. 30 | 31 | -spec type(name(), fields())-> graphql_type:type(). 32 | -spec type(name(), description(), fields())-> graphql_type:type(). 33 | type(Name, Fields) -> type(Name, null, Fields). 34 | type(Name0, Description, Fields)-> 35 | Name = graphql_type:optional_string(Name0), 36 | 37 | #{ 38 | kind => 'OBJECT', 39 | name => Name, 40 | ofType => null, 41 | description => graphql_type:optional_string(Description), 42 | fields => maps:fold(fun(FieldName, Field, Acc) -> 43 | 44 | Key = case is_binary(FieldName) of 45 | true -> FieldName; 46 | false -> list_to_binary(FieldName) 47 | end, 48 | 49 | Acc#{ Key => Field#{ 50 | resolver => coerce_field_resolver(Key, Field) 51 | }} 52 | end, #{ <<"__typename">> => field(?STRING, "Name of current type", fun() -> Name end) }, Fields) 53 | }. 54 | 55 | -spec field(Type::type()) -> field(). 56 | field(Type) -> field(Type, null). 57 | 58 | -spec field(Type::type(), Description::description()) -> field(). 59 | field(Type, Description) -> field(Type, Description, null). 60 | 61 | -spec field(Type::type(), Description::description(), Resolver::resolver()) -> field(). 62 | field(Type, Description, Resolver) -> field(Type, Description, #{}, Resolver). 63 | 64 | -spec field(Type::type(), Description::description(), Args::args(), Resolver::resolver()) -> field(). 65 | field(Type, Description, Args, Resolver) -> 66 | 67 | % like elm notation ^__^ 68 | #{ kind => 'FIELD' 69 | , type => Type 70 | , description => graphql_type:optional_string(Description) 71 | , args => maps:fold(fun(ArgName, Arg, Acc) -> 72 | Key = case is_binary(ArgName) of 73 | true -> ArgName; 74 | false -> list_to_binary(ArgName) 75 | end, 76 | Acc#{ Key => Arg#{ name => Key } } 77 | end, #{}, Args) 78 | , resolver => Resolver 79 | , isDeprecated => false 80 | , deprecationReason => null 81 | }. 82 | 83 | -spec arg(Type::type()) -> arg(). 84 | arg(Type) -> arg(Type, null). 85 | 86 | -spec arg(Type::type(), Description::description()) -> arg(). 87 | arg(Type, Description) -> arg(Type, null, Description). 88 | 89 | -spec arg(Type::type(), DefaultValue::any(), Description::description()) -> arg(). 90 | arg(Type, DefaultValue, Description) -> 91 | #{ kind => 'INPUT_VALUE' 92 | , type => Type 93 | , description => graphql_type:optional_string(Description) 94 | , default => DefaultValue 95 | }. 96 | 97 | 98 | get_field(FieldName, ObjectType)-> 99 | Fields = maps:get(fields, ObjectType), 100 | maps:get(FieldName, Fields, undefined). 101 | 102 | get_args(FieldName, ObjectType)-> 103 | FieldDefinition = get_field(FieldName, ObjectType), 104 | maps:get(args, FieldDefinition, #{}). 105 | 106 | 107 | get_field_resolver(FieldName, ObjectType)-> 108 | maps:get(resolver, get_field(FieldName, ObjectType)). 109 | 110 | coerce_field_resolver(_, #{resolver := Resolver}) 111 | when is_function(Resolver)-> 112 | Resolver; 113 | coerce_field_resolver(FieldName, Field) -> 114 | case maps:get(resolver, Field, null) of 115 | null -> fun(ObjectValue) -> default_resolver(FieldName, ObjectValue) end; 116 | Resolver -> Resolver 117 | end. 118 | 119 | % if this field is scalar type - 120 | default_resolver(FieldName, ObjectValue)-> 121 | case is_map(ObjectValue) of 122 | true -> maps:get(FieldName, ObjectValue, null); 123 | false -> proplists:get_value(FieldName, ObjectValue, null) 124 | end. 125 | 126 | get_type_from_definition(Definition) -> 127 | graphql_type:unwrap_type(maps:get(type, Definition)). 128 | -------------------------------------------------------------------------------- /src/types/graphql_type_schema.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type_schema). 2 | 3 | %% API 4 | -export([ 5 | new/1, new/2 6 | ]). 7 | 8 | -type t() :: #{ 9 | query => map(), 10 | mutation => map() | null 11 | }. 12 | 13 | -spec new(map()) -> t(). 14 | new(Schema) -> new(Schema, true). 15 | 16 | -spec new(map(), boolean()) -> t(). 17 | new(Schema, InjectIntrospection) -> 18 | 19 | Query = case {maps:get(query, Schema, null), InjectIntrospection} of 20 | {null, _} -> null; 21 | {Query1, false} -> graphql_type:unwrap_type(Query1); 22 | {Query1, true} -> graphql_introspection:inject(graphql_type:unwrap_type(Query1)) 23 | end, 24 | 25 | 26 | Mutation = case maps:get(mutation, Schema, null) of 27 | null -> null; 28 | Mutation0 -> graphql_type:unwrap_type(Mutation0) 29 | end, 30 | 31 | Schema#{ 32 | query => Query, 33 | mutation => Mutation 34 | }. -------------------------------------------------------------------------------- /src/types/graphql_type_string.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type_string). 2 | 3 | -export([ 4 | type/0 5 | ]). 6 | 7 | type()-> #{ 8 | kind => 'SCALAR', 9 | name => 'String', 10 | ofType => null, 11 | description => << 12 | "The `String` scalar type represents textual data, represented as UTF-8", 13 | "character sequences. The String type is most often used by GraphQL to", 14 | "represent free-form human-readable text." 15 | >>, 16 | 17 | serialize => fun serialize/3, 18 | parse_value => fun parse_value/2, 19 | parse_literal => fun parse_literal/2 20 | }. 21 | 22 | serialize(Value,_,_) -> coerce(Value). 23 | parse_value(null,_) -> null; 24 | parse_value(#{kind := <<"StringValue">>, value := Value}, _) -> coerce(Value); 25 | parse_value(#{kind := 'StringValue', value := Value}, _) -> coerce(Value). 26 | 27 | -spec parse_literal(map(), map()) -> binary(). 28 | parse_literal(null, _) -> null; 29 | parse_literal(#{kind := 'StringValue', value := Value}, _) -> Value; 30 | parse_literal(#{kind := Kind}, _) -> 31 | throw({error, type_validation, <<"Unexpected type ", (atom_to_binary(Kind, utf8))/binary, ", expected StringValue">>}). 32 | 33 | 34 | -spec coerce(atom() | binary() | list()) -> binary(). 35 | coerce(null) -> null; 36 | coerce(Value) when is_atom(Value) -> atom_to_binary(Value, utf8); 37 | coerce(Value) when is_binary(Value) -> Value; 38 | coerce(Value) when is_list(Value) -> list_to_binary(Value); 39 | coerce(_) -> throw({error, type_validation, <<"Cannot coerce string type">>}). 40 | -------------------------------------------------------------------------------- /src/types/graphql_type_union.erl: -------------------------------------------------------------------------------- 1 | -module(graphql_type_union). 2 | 3 | %% API 4 | -export([ 5 | type/3, type/4 6 | ]). 7 | 8 | -spec type(binary(), binary(), list( function() )) -> map(). 9 | type(Name, Description, PossibleTypes)-> 10 | type(Name, Description, PossibleTypes, fun({Type, Value}, _) -> 11 | #{ name := TypeName } = graphql_type:unwrap_type(Type), 12 | {find_type(PossibleTypes, TypeName), Value} 13 | end). 14 | 15 | -spec type(binary(), binary(), list(map() | function()), function()) -> map(). 16 | type(Name, Description, PossibleTypes, ResolveType)-> 17 | 18 | #{ 19 | kind => 'UNION', 20 | name => graphql_type:optional_string(Name), 21 | description => graphql_type:optional_string(Description), 22 | 23 | possibleTypes => PossibleTypes, 24 | 25 | resolve_type => ResolveType 26 | }. 27 | 28 | 29 | find_type([], _) -> throw({error, union, <<"Cannon find union type">>}); 30 | find_type([Type|Tail], ExpectedTypeName) -> 31 | case graphql_type:unwrap_type(Type) of 32 | #{ name := ExpectedTypeName} = ExpectedType -> ExpectedType; 33 | _ -> find_type(Tail, ExpectedTypeName) 34 | end. --------------------------------------------------------------------------------