├── logo.png ├── .gitignore ├── .travis.yml ├── test ├── graphql │ ├── error_test.exs │ ├── util │ │ └── array_map_test.exs │ ├── lang │ │ ├── ast │ │ │ ├── reducer_test.exs │ │ │ └── composite_visitor_test.exs │ │ └── lexer_test.exs │ ├── type │ │ ├── serialization_test.exs │ │ ├── introspection_test.exs │ │ ├── enum_test.exs │ │ └── union_interface_test.exs │ ├── validation │ │ └── rules │ │ │ ├── unique_operation_names_test.exs │ │ │ ├── provided_non_null_arguments_test.exs │ │ │ ├── no_fragment_cycles_test.exs │ │ │ └── fields_on_correct_type_test.exs │ └── execution │ │ ├── mutations_test.exs │ │ ├── executor_blog_schema_test.exs │ │ └── directive_test.exs ├── graphql_test.exs ├── test_helper.exs ├── support │ └── star_wars │ │ ├── data.exs │ │ └── schema.exs └── star_wars │ └── query_test.exs ├── lib ├── graphql │ ├── execution │ │ ├── types.ex │ │ ├── completion.ex │ │ ├── completion_any.ex │ │ ├── completion_atom.ex │ │ ├── directives.ex │ │ ├── arguments.ex │ │ ├── variables.ex │ │ ├── resolvable.ex │ │ ├── executor.ex │ │ ├── field_resolver.ex │ │ ├── ast_value.ex │ │ ├── execution_context.ex │ │ └── selection.ex │ ├── util │ │ ├── text.ex │ │ ├── stack.ex │ │ └── array_map.ex │ ├── type │ │ ├── enum_value.ex │ │ ├── input.ex │ │ ├── boolean.ex │ │ ├── json.ex │ │ ├── abstract_type.ex │ │ ├── non_null.ex │ │ ├── object.ex │ │ ├── id.ex │ │ ├── string.ex │ │ ├── float.ex │ │ ├── list.ex │ │ ├── int.ex │ │ ├── definition.ex │ │ ├── enum.ex │ │ ├── composite_type.ex │ │ ├── union.ex │ │ ├── directive.ex │ │ ├── interface.ex │ │ └── schema.ex │ ├── error │ │ └── syntax_error.ex │ ├── validation │ │ ├── rules │ │ │ ├── noop.ex │ │ │ ├── unique_operation_names.ex │ │ │ ├── provided_non_null_arguments.ex │ │ │ ├── fields_on_correct_type.ex │ │ │ └── no_fragment_cycles.ex │ │ ├── rules.ex │ │ └── validator.ex │ ├── validation.ex │ ├── lang │ │ ├── lexer.ex │ │ ├── ast │ │ │ ├── document_info.ex │ │ │ ├── reducer.ex │ │ │ ├── nodes.ex │ │ │ ├── parallel_visitor.ex │ │ │ ├── type_info.ex │ │ │ ├── visitor.ex │ │ │ ├── composite_visitor.ex │ │ │ └── type_info_visitor.ex │ │ └── parser.ex │ ├── error.ex │ ├── test_support │ │ └── visitor_implementations.ex │ └── schema │ │ └── generator.ex ├── mix │ └── tasks │ │ └── compile.graphql.ex └── graphql.ex ├── config ├── dogma.exs └── config.exs ├── mix.lock ├── RELEASE.md ├── CHANGELOG.md ├── mix.exs ├── LICENSE ├── src └── graphql_lexer.xrl ├── CODE_OF_CONDUCT.md ├── docs ├── graphql_grammar_summary.md └── graphql-js_ast.js └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-elixir/graphql/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /doc 4 | /tmp 5 | erl_crash.dump 6 | *.ez 7 | *.beam 8 | src/*.erl 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.2.6 4 | - 1.3.2 5 | otp_release: 6 | - 18.3 7 | - 19.0 8 | -------------------------------------------------------------------------------- /test/graphql/error_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Error.Test do 2 | use ExUnit.Case, async: true 3 | doctest GraphQL.Error 4 | doctest GraphQL.Errors 5 | 6 | end 7 | -------------------------------------------------------------------------------- /lib/graphql/execution/types.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Execution.Types do 3 | def unwrap_type(type) when is_atom(type), do: type.type 4 | def unwrap_type(type), do: type 5 | end 6 | -------------------------------------------------------------------------------- /lib/graphql/util/text.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Util.Text do 2 | def normalize(text) do 3 | text |> String.replace(~r/\n/, " ", global: true) |> String.strip() 4 | end 5 | end 6 | 7 | -------------------------------------------------------------------------------- /lib/graphql/type/enum_value.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.EnumValue do 2 | @type t :: %GraphQL.Type.EnumValue{ 3 | name: binary, 4 | description: binary, 5 | value: any 6 | } 7 | defstruct name: "", value: "", description: "" 8 | end 9 | -------------------------------------------------------------------------------- /lib/graphql/type/input.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.Input do 2 | @type t :: %GraphQL.Type.Input{ 3 | name: binary, 4 | description: binary, 5 | fields: Map.t | function 6 | } 7 | 8 | defstruct name: "Input", description: "", fields: %{} 9 | end 10 | -------------------------------------------------------------------------------- /lib/graphql/execution/completion.ex: -------------------------------------------------------------------------------- 1 | 2 | defprotocol GraphQL.Execution.Completion do 3 | @fallback_to_any true 4 | 5 | @spec complete_value(any, ExecutionContext.t, GraphQL.Document.t, map, map) :: {ExecutionContext.t, any} 6 | def complete_value(type, context, field_asts, info, result) 7 | end 8 | 9 | -------------------------------------------------------------------------------- /lib/graphql/execution/completion_any.ex: -------------------------------------------------------------------------------- 1 | 2 | defimpl GraphQL.Execution.Completion, for: Any do 3 | alias GraphQL.Execution.Types 4 | 5 | def complete_value(return_type, context, _field_asts, _info, result) do 6 | {context, GraphQL.Types.serialize(Types.unwrap_type(return_type), result)} 7 | end 8 | end 9 | 10 | -------------------------------------------------------------------------------- /lib/graphql/error/syntax_error.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.SyntaxError do 2 | @moduledoc """ 3 | An error raised when the syntax in a GraphQL query is incorrect. 4 | """ 5 | defexception line: nil, errors: "Syntax error" 6 | 7 | def message(exception) do 8 | "#{exception.errors} on line #{exception.line}" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/graphql/validation/rules/noop.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Validation.Rules.Noop do 3 | 4 | alias GraphQL.Lang.AST.Visitor 5 | 6 | defstruct name: "Noop" 7 | 8 | defimpl Visitor do 9 | def enter(_visitor, _node, accumulator), do: {:continue, accumulator} 10 | def leave(_visitor, _node, accumulator), do: accumulator 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/dogma.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | alias Dogma.Rule 3 | 4 | config :dogma, 5 | 6 | # Select a set of rules as a base 7 | rule_set: Dogma.RuleSet.All, 8 | 9 | # Pick paths not to lint 10 | exclude: [ 11 | ~r(\Atest/), 12 | ], 13 | 14 | # Override an existing rule configuration 15 | override: [ 16 | %Rule.LineLength{max_length: 120}, 17 | ] 18 | -------------------------------------------------------------------------------- /lib/graphql/execution/completion_atom.ex: -------------------------------------------------------------------------------- 1 | defimpl GraphQL.Execution.Completion, for: Atom do 2 | alias GraphQL.Execution.Completion 3 | alias GraphQL.Execution.Types 4 | 5 | def complete_value(return_type, context, field_asts, info, result) do 6 | Completion.complete_value(Types.unwrap_type(return_type), context, field_asts, info, result) 7 | end 8 | end 9 | 10 | -------------------------------------------------------------------------------- /lib/graphql/validation.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Validation do 3 | @moduledoc ~S""" 4 | Helpers that are useful within validation rules. 5 | """ 6 | 7 | @doc """ 8 | Returns an updated accumulator containing the validation error. 9 | """ 10 | def report_error(acc, error) do 11 | %{acc | validation_errors: [error] ++ acc[:validation_errors]} 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/graphql/validation/rules.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Validation.Rules do 3 | 4 | # All of the known validation rules. 5 | @rules [ 6 | #%GraphQL.Validation.Rules.Noop{}, 7 | %GraphQL.Validation.Rules.UniqueOperationNames{}, 8 | %GraphQL.Validation.Rules.FieldsOnCorrectType{}, 9 | %GraphQL.Validation.Rules.ProvidedNonNullArguments{}, 10 | %GraphQL.Validation.Rules.NoFragmentCycles{} 11 | ] 12 | 13 | def all(), do: @rules 14 | end 15 | -------------------------------------------------------------------------------- /lib/graphql/type/boolean.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.Boolean do 2 | defstruct name: "Boolean", description: "The `Boolean` scalar type represents `true` or `false`." 3 | 4 | def coerce(""), do: false 5 | def coerce(0), do: false 6 | def coerce(value), do: !!value 7 | end 8 | 9 | defimpl GraphQL.Types, for: GraphQL.Type.Boolean do 10 | def parse_value(_, value), do: GraphQL.Type.Boolean.coerce(value) 11 | def serialize(_, value), do: GraphQL.Type.Boolean.coerce(value) 12 | def parse_literal(_, v), do: v.value 13 | end 14 | -------------------------------------------------------------------------------- /lib/graphql/type/json.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.JSON do 2 | defstruct name: "JSON", description: 3 | """ 4 | The `JSON` type represents dynamic objects in JSON. 5 | """ |> GraphQL.Util.Text.normalize 6 | 7 | def coerce(value), do: value 8 | end 9 | 10 | defimpl GraphQL.Types, for: GraphQL.Type.JSON do 11 | def parse_value(_, value), do: GraphQL.Type.JSON.coerce(value) 12 | def serialize(_, value), do: GraphQL.Type.JSON.coerce(value) 13 | def parse_literal(_, v), do: GraphQL.Type.JSON.coerce(v.value) 14 | end 15 | -------------------------------------------------------------------------------- /lib/graphql/execution/directives.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Execution.Directives do 3 | alias GraphQL.Execution.Arguments 4 | 5 | def resolve_directive(context, directives, directive_name) do 6 | ast = Enum.find(directives, fn(d) -> d.name.value == Atom.to_string(directive_name) end) 7 | if ast do 8 | directive = apply(GraphQL.Type.Directives, directive_name, []) 9 | %{if: val} = Arguments.argument_values(directive.args, ast.arguments, context.variable_values) 10 | val 11 | else 12 | directive_name == :include 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/graphql/type/abstract_type.ex: -------------------------------------------------------------------------------- 1 | defprotocol GraphQL.Type.AbstractType do 2 | @type t :: GraphQL.Type.Union.t | GraphQL.Type.Interface.t 3 | 4 | @spec possible_type?(GraphQL.AbstractType.t, GraphQL.Type.ObjectType.t) :: boolean 5 | def possible_type?(abstract_type, object) 6 | 7 | @spec possible_types(GraphQL.AbstractType.t, GraphQL.Schema.t) :: [GraphQL.Type.ObjectType.t] 8 | def possible_types(abstract_type, schema) 9 | 10 | @spec get_object_type(GraphQL.AbstractType.t, %{}, GraphQL.Schema.t) :: GraphQL.Type.ObjectType.t 11 | def get_object_type(abstract_type, object, schema) 12 | end 13 | -------------------------------------------------------------------------------- /lib/graphql/util/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Util.Stack do 2 | @moduledoc ~S""" 3 | A Stack implementation. *push* and *pop* return a new Stack. 4 | *peek* returns the top element. 5 | """ 6 | 7 | alias GraphQL.Util.Stack 8 | 9 | defstruct elements: [] 10 | 11 | def push(stack, node) do 12 | %Stack{stack | elements: [node] ++ stack.elements} 13 | end 14 | 15 | def pop(stack) do 16 | case stack.elements do 17 | [_|rest] -> %Stack{stack | elements: rest} 18 | [] -> nil 19 | end 20 | end 21 | 22 | def peek(stack) do 23 | case stack.elements do 24 | [node|_] -> node 25 | [] -> nil 26 | end 27 | end 28 | 29 | def length(stack), do: Kernel.length(stack.elements) 30 | end 31 | -------------------------------------------------------------------------------- /lib/graphql/lang/lexer.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Lang.Lexer do 2 | @moduledoc ~S""" 3 | GraphQL lexer implemented with leex. 4 | 5 | Tokenise a GraphQL query 6 | 7 | iex> GraphQL.tokenize("{ hello }") 8 | [{ :"{", 1 }, { :name, 1, 'hello' }, { :"}", 1 }] 9 | """ 10 | 11 | @doc """ 12 | Tokenize the input string into a stream of tokens. 13 | 14 | iex> GraphQL.tokenize("{ hello }") 15 | [{ :"{", 1 }, { :name, 1, 'hello' }, { :"}", 1 }] 16 | 17 | """ 18 | def tokenize(input_string) when is_binary(input_string) do 19 | input_string |> to_char_list |> tokenize 20 | end 21 | 22 | def tokenize(input_string) do 23 | {:ok, tokens, _} = :graphql_lexer.string input_string 24 | tokens 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/graphql/type/non_null.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.NonNull do 2 | @type t :: %{ofType: map} 3 | defstruct ofType: nil 4 | 5 | defimpl String.Chars do 6 | def to_string(non_null), do: "#{non_null.ofType}!" 7 | end 8 | end 9 | 10 | defimpl GraphQL.Execution.Completion, for: GraphQL.Type.NonNull do 11 | alias GraphQL.Execution.Completion 12 | alias GraphQL.Execution.Types 13 | 14 | @spec complete_value(%GraphQL.Type.NonNull{}, ExecutionContext.t, GraphQL.Document.t, any, any) :: {ExecutionContext.t, map} 15 | def complete_value(%GraphQL.Type.NonNull{ofType: inner_type}, context, field_asts, info, result) do 16 | Completion.complete_value(Types.unwrap_type(inner_type), context, field_asts, info, result) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/graphql/type/object.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.ObjectType do 2 | @type t :: %GraphQL.Type.ObjectType{ 3 | name: binary, 4 | description: binary | nil, 5 | fields: Map.t | function, 6 | interfaces: [GraphQL.Interface.t] | nil, 7 | isTypeOf: ((any) -> boolean) 8 | } 9 | defstruct name: "", description: "", fields: %{}, interfaces: [], isTypeOf: nil 10 | 11 | defimpl String.Chars do 12 | def to_string(obj), do: obj.name 13 | end 14 | 15 | defimpl GraphQL.Execution.Completion do 16 | alias GraphQL.Execution.Selection 17 | 18 | def complete_value(return_type, context, field_asts, _info, result) do 19 | Selection.complete_sub_fields(return_type, context, field_asts, result) 20 | end 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /lib/graphql/lang/ast/document_info.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Lang.AST.DocumentInfo do 3 | 4 | defstruct schema: nil, 5 | document: nil, 6 | lookups: %{} 7 | 8 | def new(schema, document) do 9 | %GraphQL.Lang.AST.DocumentInfo{schema: schema, lookups: precompute_lookups(schema, document)} 10 | end 11 | 12 | def get_fragment_definition(document_info, name) do 13 | document_info.lookups[:fragment_definitions][name] 14 | end 15 | 16 | defp precompute_lookups(_schema, document) do 17 | %{ 18 | fragment_definitions: Enum.reduce(document.definitions, %{}, fn(definition, acc) -> 19 | if definition[:kind] == :FragmentDefinition do 20 | put_in(acc[definition.name.value], definition) 21 | else 22 | acc 23 | end 24 | end) 25 | } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/graphql/util/array_map_test.exs: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Util.ArrayMapTest do 3 | use ExUnit.Case, async: true 4 | 5 | alias GraphQL.Util.ArrayMap 6 | 7 | test "implements Access" do 8 | version_1 = ArrayMap.new(%{0 => :zero, 1 => :one, 2 => :two}) 9 | version_2 = ArrayMap.new(%{0 => :zero, 1 => :ONE, 2 => :two}) 10 | 11 | version_1_updated = put_in(version_1, [1], :ONE) 12 | 13 | assert version_2 == version_1_updated 14 | end 15 | 16 | test "Access works with mix of nested Maps and ArrayMaps" do 17 | nested = %{:foo => ArrayMap.new(%{0 => %{:baz => ArrayMap.new(%{0 => %{quux: 123}})}})} 18 | expected = %{:foo => ArrayMap.new(%{0 => %{:baz => ArrayMap.new(%{0 => %{quux: 456}})}})} 19 | 20 | updated = put_in(nested, [:foo, 0, :baz, 0, :quux], 456) 21 | 22 | assert updated == expected 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/graphql/execution/arguments.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Execution.Arguments do 3 | alias GraphQL.Execution.ASTValue 4 | 5 | def argument_values(arg_defs, arg_asts, variable_values) do 6 | arg_ast_map = Enum.reduce arg_asts, %{}, fn(arg_ast, result) -> 7 | Map.put(result, String.to_atom(arg_ast.name.value), arg_ast) 8 | end 9 | Enum.reduce(arg_defs, %{}, fn(arg_def, result) -> 10 | {arg_def_name, arg_def_type} = arg_def 11 | value_ast = Map.get(arg_ast_map, arg_def_name) 12 | 13 | value = ASTValue.value_from_ast(value_ast, arg_def_type.type, variable_values) 14 | value = if is_nil(value) do 15 | Map.get(arg_def_type, :defaultValue) 16 | else 17 | value 18 | end 19 | if is_nil(value) do 20 | result 21 | else 22 | Map.put(result, arg_def_name, value) 23 | end 24 | end) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/graphql/type/id.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.ID do 2 | defstruct name: "ID", description: 3 | """ 4 | The `ID` scalar type represents a unique identifier, often used to 5 | refetch an object or as key for a cache. The ID type appears in a JSON 6 | response as a String; however, it is not intended to be human-readable. 7 | When expected as an input type, any string (such as `"4"`) or integer 8 | (such as `4`) input value will be accepted as an ID. 9 | """ |> GraphQL.Util.Text.normalize 10 | 11 | def coerce(value), do: to_string(value) 12 | 13 | defimpl String.Chars do 14 | def to_string(_), do: "ID" 15 | end 16 | end 17 | 18 | defimpl GraphQL.Types, for: GraphQL.Type.ID do 19 | def parse_value(_, value), do: GraphQL.Type.String.coerce(value) 20 | def serialize(_, value), do: GraphQL.Type.String.coerce(value) 21 | def parse_literal(_, v), do: v.value 22 | end 23 | -------------------------------------------------------------------------------- /test/graphql_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQLTest do 2 | use ExUnit.Case, async: true 3 | doctest GraphQL 4 | 5 | import ExUnit.TestHelpers 6 | 7 | alias GraphQL.Type.ObjectType 8 | alias GraphQL.Type.String 9 | 10 | def schema do 11 | GraphQL.Schema.new(%{ 12 | query: %ObjectType{ 13 | fields: %{ 14 | a: %{type: %String{}} 15 | } 16 | } 17 | } 18 | ) 19 | end 20 | 21 | test "Execute simple query" do 22 | {:ok, result} = execute(schema, "{ a }", root_value: %{a: "A"}) 23 | assert_data(result, %{a: "A"}) 24 | end 25 | 26 | test "Report parse error with message" do 27 | {_, result} = execute(schema, "{") 28 | assert_has_error(result, %{message: "GraphQL: syntax error before: on line 1", line_number: 1}) 29 | 30 | {_, result} = execute(schema, "a") 31 | assert_has_error(result, %{message: "GraphQL: syntax error before: \"a\" on line 1", line_number: 1}) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/graphql/type/string.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.String do 2 | defstruct name: "String", description: 3 | """ 4 | The `String` scalar type represents textual data, represented as UTF-8 5 | character sequences. The String type is most often used by GraphQL to 6 | represent free-form human-readable text. 7 | """ |> GraphQL.Util.Text.normalize 8 | 9 | def coerce(nil), do: nil 10 | def coerce(value) when is_map(value) do 11 | for {k, v} <- value, into: %{}, do: {to_string(k), v} 12 | end 13 | def coerce(value), do: to_string(value) 14 | 15 | defimpl String.Chars do 16 | def to_string(_), do: "String" 17 | end 18 | end 19 | 20 | defimpl GraphQL.Types, for: GraphQL.Type.String do 21 | def parse_value(_, value), do: GraphQL.Type.String.coerce(value) 22 | def serialize(_, value), do: GraphQL.Type.String.coerce(value) 23 | def parse_literal(_, %{kind: :StringValue, value: value}), do: value 24 | def parse_literal(_, _), do: nil 25 | end 26 | -------------------------------------------------------------------------------- /lib/graphql/type/float.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.Float do 2 | @type t :: %GraphQL.Type.Float{name: binary, description: binary} 3 | defstruct name: "Float", description: 4 | """ 5 | The `Float` scalar type represents signed double-precision fractional 6 | values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). 7 | """ |> GraphQL.Util.Text.normalize 8 | 9 | def coerce(false), do: 0 10 | def coerce(true), do: 1 11 | def coerce(value) when is_binary(value) do 12 | case Float.parse(value) do 13 | :error -> nil 14 | {v, _} -> coerce(v) 15 | end 16 | end 17 | def coerce(value) do 18 | value * 1.0 19 | end 20 | 21 | defimpl String.Chars do 22 | def to_string(_), do: "Float" 23 | end 24 | end 25 | 26 | defimpl GraphQL.Types, for: GraphQL.Type.Float do 27 | def parse_value(_, value), do: GraphQL.Type.Float.coerce(value) 28 | def serialize(_, value), do: GraphQL.Type.Float.coerce(value) 29 | def parse_literal(_, v), do: v.value 30 | end 31 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /lib/graphql/type/list.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.List do 2 | alias GraphQL.Execution.Completion 3 | 4 | @type t :: %{ofType: map} 5 | defstruct ofType: nil 6 | 7 | defimpl String.Chars do 8 | def to_string(list), do: "[#{list.ofType}]" 9 | end 10 | 11 | defimpl Completion do 12 | alias GraphQL.Util.ArrayMap 13 | alias GraphQL.Execution.Types 14 | 15 | def complete_value(%GraphQL.Type.List{ofType: list_type}, context, field_asts, info, result) do 16 | {context, value, _} = Enum.reduce result, {context, %ArrayMap{}, 0}, fn(item, {context, acc, count}) -> 17 | {context, value} = Completion.complete_value(Types.unwrap_type(list_type), context, field_asts, info, item) 18 | {context, ArrayMap.put(acc, count, value), count + 1} 19 | end 20 | {context, value} 21 | end 22 | end 23 | 24 | defimpl GraphQL.Types do 25 | def parse_value(_, nil), do: nil 26 | def parse_value(_, value) when is_list(value), do: value 27 | def parse_value(_, value), do: List.wrap(value) 28 | def serialize(_, value), do: value 29 | def parse_literal(_, v), do: v.value 30 | end 31 | end 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(exclude: [:skip]) 2 | 3 | defmodule ExUnit.TestHelpers do 4 | import ExUnit.Assertions 5 | 6 | alias GraphQL 7 | alias GraphQL.Lang.Parser 8 | 9 | def stringify_keys(map) when is_map(map) do 10 | Enum.reduce(map, %{}, fn({k, v}, acc) -> Map.put(acc, stringify_key(k), stringify_keys(v)) end) 11 | end 12 | def stringify_keys(list) when is_list(list) do 13 | Enum.map(list, &stringify_keys/1) 14 | end 15 | def stringify_keys(x), do: x 16 | 17 | def stringify_key(key) when is_atom(key), do: to_string(key) 18 | def stringify_key(key), do: key 19 | 20 | def execute(schema, query, opts \\ []) do 21 | GraphQL.execute_with_opts(schema, query, opts) 22 | end 23 | 24 | def assert_data(result, expected) do 25 | assert result[:data] == stringify_keys(expected) 26 | end 27 | 28 | def assert_has_error(result, expected) do 29 | assert( 30 | Enum.member?(result[:errors], stringify_keys(expected)), 31 | message: "Expected result[:errors] to contain #{inspect expected}" 32 | ) 33 | end 34 | 35 | def assert_parse(input_string, expected_output, type \\ :ok) do 36 | assert Parser.parse(input_string) == {type, expected_output} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/graphql/execution/variables.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Execution.Variables do 2 | alias GraphQL.Execution.ASTValue 3 | 4 | @spec extract(map) :: map 5 | def extract(context) do 6 | schema = context.schema 7 | variable_definition_asts = Map.get(context.operation, :variableDefinitions, []) 8 | input_map = context.variable_values 9 | 10 | reduce_values(schema, variable_definition_asts, input_map) 11 | end 12 | 13 | @spec reduce_values(GraphQL.Schema.t, map, map) :: map 14 | defp reduce_values(schema, definition_asts, inputs) do 15 | Enum.reduce(definition_asts, %{}, fn(ast, result) -> 16 | key = ast.variable.name.value 17 | value = get_variable_value(schema, ast, inputs[key]) 18 | Map.put(result, key, value) 19 | end) 20 | end 21 | 22 | defp get_variable_value(schema, ast, input_value) do 23 | type = GraphQL.Schema.type_from_ast(ast.type, schema) 24 | value_for(ast, type, input_value) 25 | end 26 | 27 | @spec value_for(map, map, map | nil) :: map | nil 28 | defp value_for(%{defaultValue: default}, type, nil) do 29 | ASTValue.value_from_ast(%{value: default}, type, nil) 30 | end 31 | defp value_for(_, _, nil), do: nil 32 | defp value_for(_, type, input) do 33 | GraphQL.Types.serialize(%{type: type}, input) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/graphql/error.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Errors do 2 | @moduledoc """ 3 | Represents a set of errors that have occured. 4 | """ 5 | @type t :: %{errors: list(GraphQL.Error.t)} 6 | defstruct errors: [] 7 | 8 | @doc """ 9 | Generates a new Errors structure using the passed in errors as the contents 10 | 11 | ## Examples 12 | 13 | iex> GraphQL.Errors.new([%GraphQL.Error{message: "GraphQL: syntax error before: '}' on line 1", line_number: 1}]) 14 | %GraphQL.Errors{errors: [%GraphQL.Error{line_number: 1, 15 | message: "GraphQL: syntax error before: '}' on line 1"}]} 16 | """ 17 | @spec new(list(GraphQL.Error.t)) :: GraphQL.Errors.t 18 | def new(errors) when is_list(errors) do 19 | %GraphQL.Errors{errors: errors} 20 | end 21 | 22 | @spec new(GraphQL.Error.t) :: GraphQL.Errors.t 23 | def new(error) do 24 | %GraphQL.Errors{errors: [error]} 25 | end 26 | end 27 | 28 | defmodule GraphQL.Error do 29 | @moduledoc """ 30 | Represents the data structure for a single error. 31 | 32 | ## Examples 33 | 34 | iex> %GraphQL.Error{message: "GraphQL: syntax error before: '}' on line 1", line_number: 1} 35 | %GraphQL.Error{line_number: 1, 36 | message: "GraphQL: syntax error before: '}' on line 1"} 37 | """ 38 | @type t :: %{message: String.t} 39 | defstruct message: "", line_number: 0 40 | end 41 | -------------------------------------------------------------------------------- /lib/graphql/validation/rules/unique_operation_names.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Validation.Rules.UniqueOperationNames do 3 | 4 | alias GraphQL.Lang.AST.{Visitor, InitialisingVisitor} 5 | import GraphQL.Validation 6 | 7 | defstruct name: "UniqueOperationNames" 8 | 9 | defimpl InitialisingVisitor do 10 | def init(_visitor, accumulator) do 11 | Map.merge(%{operation_names: %{}}, accumulator) 12 | end 13 | end 14 | 15 | defimpl Visitor do 16 | def enter(_visitor, %{kind: :OperationDefinition, name: %{value: _} = op_name}, accumulator) do 17 | accumulator = if seen_operation?(accumulator, op_name) do 18 | report_error(accumulator, duplicate_operation_message(op_name)) 19 | else 20 | mark_as_seen(accumulator, op_name) 21 | end 22 | {:continue, accumulator} 23 | end 24 | 25 | def enter(_visitor, _node, accumulator), do: {:continue, accumulator} 26 | 27 | def leave(_visitor, _node, accumulator), do: accumulator 28 | 29 | defp duplicate_operation_message(op_name) do 30 | "There can only be one operation named '#{op_name.value}'." 31 | end 32 | 33 | defp seen_operation?(accumulator, op_name) do 34 | Map.has_key?(accumulator[:operation_names], op_name.value) 35 | end 36 | 37 | defp mark_as_seen(accumulator, op_name) do 38 | put_in(accumulator[:operation_names][op_name.value], true) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/graphql/type/int.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.Int do 2 | @max_int 2147483647 3 | @min_int -2147483648 4 | 5 | @type t :: %GraphQL.Type.Int{name: binary, description: binary} 6 | defstruct name: "Int", description: 7 | """ 8 | The `Int` scalar type represents non-fractional signed whole numeric 9 | values. Int can represent values between -(2^53 - 1) and 2^53 - 1 since 10 | represented in JSON as double-precision floating point numbers specified 11 | by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). 12 | """ |> GraphQL.Util.Text.normalize 13 | 14 | def coerce(false), do: 0 15 | def coerce(true), do: 1 16 | def coerce(value) when is_binary(value) do 17 | case Float.parse(value) do 18 | :error -> nil 19 | {v, _} -> coerce(v) 20 | end 21 | end 22 | def coerce(value) do 23 | if value <= @max_int && value >= @min_int do 24 | if value < 0 do 25 | round(Float.ceil(value * 1.0, 0)) 26 | else 27 | round(Float.floor(value * 1.0, 0)) 28 | end 29 | else 30 | nil 31 | end 32 | end 33 | 34 | defimpl String.Chars do 35 | def to_string(_), do: "Int" 36 | end 37 | end 38 | 39 | defimpl GraphQL.Types, for: GraphQL.Type.Int do 40 | def parse_value(_, value), do: GraphQL.Type.Int.coerce(value) 41 | def serialize(_, value), do: GraphQL.Type.Int.coerce(value) 42 | def parse_literal(_, v), do: v.value 43 | end 44 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, 2 | "credo": {:hex, :credo, "0.4.11", "03a64e9d53309b7132556284dda0be57ba1013885725124cfea7748d740c6170", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, 3 | "dialyxir": {:hex, :dialyxir, "0.3.5", "eaba092549e044c76f83165978979f60110dc58dd5b92fd952bf2312f64e9b14", [:mix], []}, 4 | "dogma": {:hex, :dogma, "0.1.7", "927f76a89a809db96e0983b922fc899f601352690aefa123529b8aa0c45123b2", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, optional: false]}]}, 5 | "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []}, 6 | "ex_doc": {:hex, :ex_doc, "0.12.0", "b774aabfede4af31c0301aece12371cbd25995a21bb3d71d66f5c2fe074c603f", [:mix], [{:earmark, "~> 0.2", [hex: :earmark, optional: false]}]}, 7 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, 8 | "inch_ex": {:hex, :inch_ex, "0.5.3", "39f11e96181ab7edc9c508a836b33b5d9a8ec0859f56886852db3d5708889ae7", [:mix], [{:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, 9 | "mix_test_watch": {:hex, :mix_test_watch, "0.2.6", "9fcc2b1b89d1594c4a8300959c19d50da2f0ff13642c8f681692a6e507f92cab", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}]}, 10 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}} 11 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | This document simply outlines the release process: 4 | 5 | 1. Ensure you are running on the oldest supported Elixir version (check `.travis.yml`) 6 | 7 | 2. Ensure `CHANGELOG.md` is updated and add current date 8 | 9 | 3. Change the version number in `mix.exs` and `README.md` 10 | 11 | 4. Run `mix test` to ensure all tests pass 12 | 13 | 5. Commit changes above with title "Release vVERSION" and push to GitHub 14 | 15 | git add . 16 | git commit -m"Release vX.Y.Z" 17 | git push origin master 18 | 19 | 6. Check CI is green 20 | 21 | 7. Create a release on GitHub and add the CHANGELOG from step #2 (https://github.com/graphql-elixir/graphql/releases/new) using VERSION as the tag and title 22 | 23 | 8. Publish new hex release with `mix hex.publish` 24 | 25 | 9. Publish hex docs with `mix hex.docs` 26 | 27 | 10. Update upstream repos `plug_graphql` and `graphql_relay` and release as appropriate 28 | 29 | ## Deprecation policy 30 | 31 | GraphQL deprecations happen in 3 steps: 32 | 33 | 1. The feature is soft-deprecated. It means both CHANGELOG and documentation must list the feature as deprecated but no warning is effectively emitted by running the code. There is no requirement to soft-deprecate a feature. 34 | 35 | 2. The feature is effectively deprecated by emitting warnings on usage. In order to deprecate a feature, the proposed alternative MUST exist for AT LEAST two versions. 36 | 37 | 3. The feature is removed. This can only happen on major releases. 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.2 (2016-06-09) 4 | 5 | * Enhancements 6 | * Future support for deferred resolvers 7 | * GraphQL IDL compiler 8 | 9 | * Bugfixes 10 | * Validate operation name matches an operation 11 | * Resolve no longer fails when it cannot find a matching function 12 | * Fix 1.3 warnings 13 | 14 | 15 | ## 0.3.1 (2016-06-09) 16 | 17 | * Bugfixes 18 | * Fix introspection to include Input types when input types are arguments 19 | to fields. 20 | 21 | 22 | ## 0.3.0 (2016-05-29) 23 | 24 | * Enhancements 25 | * Directive support (@skip and @include) 26 | * Validations now run on queries 27 | * Rule: Fields on correct type 28 | * Rule: No fragment cycles 29 | * Rule: Validate mandatory arguments 30 | * Rule: Unique operation names 31 | 32 | * Bugfixes 33 | * Allow default values to get assigned correctly when a query defines 34 | an enum variable with a default 35 | * Query can take an optional Enum argument and correctly fall back if 36 | that value is not specified 37 | 38 | * Note: the `execute/5` signature will be changed to the `execute_with_opts/3` 39 | in a future version 40 | 41 | 42 | ## 0.2.0 (2016-03-19) 43 | 44 | * Enhancements 45 | * Interface, Union and Input type support 46 | * Types can be referenced in schemas using modules or atoms 47 | * Require Elixir 1.2 and above 48 | 49 | * Bugfixes 50 | * Resolve now accepts a map with string keys 51 | * Duplicate field definitions handled correctly (required for Relay support) 52 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.3.2" 5 | 6 | @description "GraphQL Elixir implementation" 7 | @repo_url "https://github.com/graphql-elixir/graphql" 8 | 9 | def project do 10 | [app: :graphql, 11 | version: @version, 12 | elixir: "~> 1.2", 13 | description: @description, 14 | deps: deps, 15 | package: package, 16 | source_url: @repo_url, 17 | homepage_url: @repo_url, 18 | build_embedded: Mix.env == :prod, 19 | start_permanent: Mix.env == :prod, 20 | consolidate_protocols: true, 21 | name: "GraphQL", 22 | docs: [main: "GraphQL", logo: "logo.png", extras: ["README.md"]]] 23 | end 24 | 25 | def application do 26 | [applications: [:logger]] 27 | end 28 | 29 | defp deps do 30 | [ 31 | {:mix_test_watch, "~> 0.2", only: :dev}, 32 | {:credo, "~> 0.3", only: :dev}, 33 | {:dogma, "~> 0.1", only: :dev}, 34 | 35 | # Doc dependencies 36 | {:earmark, "~> 0.2", only: :dev}, 37 | {:ex_doc, "~> 0.11", only: :dev}, 38 | {:inch_ex, "~> 0.5", only: :dev}, 39 | {:dialyxir, "~> 0.3", only: [:dev]}, 40 | {:poison, "~> 1.5 or ~> 2.0", only: [:dev, :test]}, 41 | ] 42 | end 43 | 44 | defp package do 45 | [maintainers: ["Josh Price", "James Sadler", "Mark Olson", "Aaron Weiker", "Sean Abrahams"], 46 | licenses: ["BSD"], 47 | links: %{"GitHub" => @repo_url}, 48 | files: ~w(lib src/*.xrl src/*.yrl mix.exs *.md LICENSE)] 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/graphql/test_support/visitor_implementations.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.TestSupport.VisitorImplementations do 3 | 4 | alias GraphQL.Lang.AST.Visitor 5 | alias GraphQL.Lang.AST.PostprocessingVisitor 6 | 7 | 8 | defmodule TracingVisitor do 9 | defstruct name: nil 10 | end 11 | 12 | defimpl Visitor, for: TracingVisitor do 13 | def enter(visitor, node, accumulator) do 14 | {:continue, %{accumulator | calls: ["#{visitor.name} entering #{node[:kind]}"] ++ accumulator[:calls]}} 15 | end 16 | 17 | def leave(visitor, node, accumulator) do 18 | %{accumulator | calls: ["#{visitor.name} leaving #{node[:kind]}"] ++ accumulator[:calls]} 19 | end 20 | end 21 | 22 | defmodule CallReverser do 23 | defstruct name: "call reverser" 24 | end 25 | 26 | defimpl PostprocessingVisitor, for: CallReverser do 27 | def finish(_visitor, accumulator) do 28 | Enum.reverse(accumulator[:calls]) 29 | end 30 | end 31 | 32 | defmodule BalancedCallsVisitor do 33 | defstruct name: "balanced calls visitor" 34 | end 35 | 36 | defimpl Visitor, for: BalancedCallsVisitor do 37 | def enter(_visitor, _node, accumulator) do 38 | {:continue, %{accumulator | count: accumulator[:count] + 1}} 39 | end 40 | 41 | def leave(_visitor, _node, accumulator) do 42 | %{accumulator | count: accumulator[:count] - 1} 43 | end 44 | end 45 | 46 | defimpl PostprocessingVisitor, for: BalancedCallsVisitor do 47 | def finish(_visitor, accumulator), do: accumulator[:count] 48 | end 49 | end 50 | 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | Copyright (c) Josh Price All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name Facebook nor the names of its contributors may be used to 16 | endorse or promote products derived from this software without specific 17 | prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /test/graphql/lang/ast/reducer_test.exs: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Lang.AST.ReducerTest do 3 | use ExUnit.Case, async: true 4 | 5 | alias GraphQL.Lang.Parser 6 | alias GraphQL.Lang.AST.Reducer 7 | alias GraphQL.Lang.AST.CompositeVisitor 8 | 9 | alias GraphQL.TestSupport.VisitorImplementations.{ 10 | CallReverser, 11 | TracingVisitor, 12 | BalancedCallsVisitor 13 | } 14 | 15 | test "Enter and leave calls should be balanced" do 16 | {:ok, ast} = Parser.parse "type Person {name: String}" 17 | count = Reducer.reduce(ast, %BalancedCallsVisitor{}, %{count: 0}) 18 | assert count == 0 19 | end 20 | 21 | test "All nodes are visited" do 22 | {:ok, ast} = Parser.parse "type Person {name: String}" 23 | 24 | v0 = %CallReverser{} 25 | v1 = %TracingVisitor{name: "Tracing Visitor"} 26 | composite_visitor = CompositeVisitor.compose([v0, v1]) 27 | 28 | log = Reducer.reduce(ast, composite_visitor, %{calls: []}) 29 | assert log == [ 30 | "Tracing Visitor entering Document", 31 | "Tracing Visitor entering ObjectTypeDefinition", 32 | "Tracing Visitor entering Name", 33 | "Tracing Visitor leaving Name", 34 | "Tracing Visitor entering FieldDefinition", 35 | "Tracing Visitor entering Name", 36 | "Tracing Visitor leaving Name", 37 | "Tracing Visitor entering NamedType", 38 | "Tracing Visitor entering Name", 39 | "Tracing Visitor leaving Name", 40 | "Tracing Visitor leaving NamedType", 41 | "Tracing Visitor leaving FieldDefinition", 42 | "Tracing Visitor leaving ObjectTypeDefinition", 43 | "Tracing Visitor leaving Document" 44 | ] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/graphql/type/definition.ex: -------------------------------------------------------------------------------- 1 | # TODO think of a better name for this protocol: 2 | # Typed, TypeProtocol 3 | defprotocol GraphQL.Types do 4 | @fallback_to_any true 5 | def parse_value(type, value) 6 | def parse_literal(type, value) 7 | def serialize(type, value) 8 | end 9 | 10 | defimpl GraphQL.Types, for: Any do 11 | def parse_value(_, v), do: v 12 | def parse_literal(_, v), do: v.value 13 | def serialize(_, v), do: v 14 | end 15 | 16 | defmodule GraphQL.Type do 17 | @doc """ 18 | Converts a module type (`StarWars.Schema.Character`) to a String (`"Character"`) 19 | """ 20 | @spec module_to_string(atom) :: String.t 21 | def module_to_string(module_type) do 22 | module_type |> Atom.to_string |> String.split(".") |> Enum.reverse |> hd 23 | end 24 | 25 | @spec implements?(GraphQL.Type.ObjectType.t, GraphQL.Type.Interface.t) :: boolean 26 | def implements?(object, interface) do 27 | Map.get(object, :interfaces, []) 28 | |> Enum.map(fn 29 | (iface) when is_atom(iface) -> module_to_string(iface) 30 | (iface) -> iface.name 31 | end) 32 | |> Enum.member?(interface.name) 33 | end 34 | 35 | def is_abstract?(%GraphQL.Type.Union{}), do: true 36 | def is_abstract?(%GraphQL.Type.Interface{}), do: true 37 | def is_abstract?(_), do: false 38 | 39 | def is_named?(%GraphQL.Type.ObjectType{}), do: true 40 | def is_named?(_), do: false 41 | 42 | def is_composite_type?(%GraphQL.Type.ObjectType{}), do: true 43 | def is_composite_type?(%GraphQL.Type.Interface{}), do: true 44 | def is_composite_type?(%GraphQL.Type.Union{}), do: true 45 | def is_composite_type?(%GraphQL.Type.Input{}), do: true 46 | def is_composite_type?(_), do: false 47 | end 48 | -------------------------------------------------------------------------------- /lib/graphql/type/enum.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.Enum do 2 | @type t :: %GraphQL.Type.Enum{ 3 | name: binary, 4 | description: binary, 5 | values: %{binary => GraphQL.Type.EnumValue.t} 6 | } 7 | defstruct name: "", values: %{}, description: "" 8 | 9 | def new(map) do 10 | map = %{map | values: define_values(map.values)} 11 | struct(GraphQL.Type.Enum, map) 12 | end 13 | 14 | def values(map) do 15 | Enum.reduce(map.values, %{}, fn(%{name: name, value: value}, acc) -> 16 | Map.put(acc, name, value) 17 | end) 18 | end 19 | 20 | defp define_values(values) do 21 | Enum.map(values, fn {name, v} -> 22 | val = Map.get(v, :value, name) 23 | desc = Map.get(v, :description, "") 24 | %GraphQL.Type.EnumValue{name: name, value: val, description: desc} 25 | end) 26 | end 27 | 28 | defimpl String.Chars do 29 | def to_string(_), do: "Enum" 30 | end 31 | end 32 | 33 | defimpl GraphQL.Types, for: GraphQL.Type.Enum do 34 | def parse_value(_struct, value) when is_integer(value) do 35 | value 36 | end 37 | def parse_value(struct, value) do 38 | GraphQL.Type.Enum.values(struct) |> Map.get(String.to_atom(value)) 39 | end 40 | 41 | def parse_literal(struct, value) do 42 | values = GraphQL.Type.Enum.values(struct) 43 | key = String.to_atom(value.value) 44 | case Map.has_key?(values, key) do 45 | true -> Map.get(values, key) 46 | false -> nil 47 | end 48 | end 49 | 50 | def serialize(struct, wanted) do 51 | values = GraphQL.Type.Enum.values(struct) 52 | case Enum.find(values, fn({_, v}) -> v == wanted end) do 53 | nil -> nil 54 | {name, _} -> to_string(name) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/graphql/type/composite_type.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Type.CompositeType do 3 | @moduledoc ~S""" 4 | Provides *get_field* and *get_fields* accessors for composite types. 5 | This abstacts over the *fields* key being a map or a function that returns a map. 6 | """ 7 | alias GraphQL.Type.{ObjectType, Interface, Input} 8 | 9 | def has_field?(%Interface{fields: fields}, field_name), do: !!do_get_field(fields, field_name) 10 | def has_field?(%ObjectType{fields: fields}, field_name), do: !!do_get_field(fields, field_name) 11 | def has_field?(%Input{fields: fields}, field_name), do: !!do_get_field(fields, field_name) 12 | 13 | def get_fields(%Interface{fields: fields}), do: do_get_fields(fields) 14 | def get_fields(%ObjectType{fields: fields}), do: do_get_fields(fields) 15 | def get_fields(%Input{fields: fields}), do: do_get_fields(fields) 16 | 17 | def get_field(%Interface{fields: fields}, field_name), do: do_get_field(fields, field_name) 18 | def get_field(%ObjectType{fields: fields}, field_name), do: do_get_field(fields, field_name) 19 | def get_field(%Input{fields: fields}, field_name), do: do_get_field(fields, field_name) 20 | 21 | defp do_get_field(fields, field_name) when is_binary(field_name) do 22 | try do 23 | do_get_fields(fields)[String.to_existing_atom(field_name)] 24 | rescue 25 | # Handle the error when String.to_existing_atom/1 fails. 26 | ArgumentError -> nil 27 | end 28 | end 29 | defp do_get_field(fields, field_name) when is_atom(field_name) do 30 | do_get_fields(fields)[field_name] 31 | end 32 | 33 | defp do_get_fields(fields) when is_function(fields), do: fields.() 34 | defp do_get_fields(fields) when is_map(fields), do: fields 35 | end 36 | -------------------------------------------------------------------------------- /lib/graphql/type/union.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.Union do 2 | alias GraphQL.Execution.Completion 3 | 4 | @type t :: %GraphQL.Type.Union{ 5 | name: binary, 6 | description: binary | nil, 7 | resolver: (any -> GraphQL.Type.ObjectType.t), 8 | types: [GraphQL.Type.ObjectType.t] 9 | } 10 | defstruct name: "", description: "", resolver: nil, types: [] 11 | 12 | def new(map) do 13 | struct(GraphQL.Type.Union, map) 14 | end 15 | 16 | defimpl GraphQL.Type.AbstractType do 17 | @doc """ 18 | Returns a boolean indicating if the typedef provided is part of the provided 19 | union type. 20 | """ 21 | def possible_type?(union, object) do 22 | Enum.any?(union.types, fn(t) -> t.name === object.name end) 23 | end 24 | 25 | def possible_types(union, _schema) do 26 | union.types 27 | end 28 | 29 | @doc """ 30 | Returns the typedef for the object that was passed in, which could be a 31 | struct or map. 32 | """ 33 | def get_object_type(%{resolver: nil}=union, _, _) do 34 | throw "Missing 'resolver' field on Union #{union.name}" 35 | end 36 | def get_object_type(%{resolver: resolver}, object, _) do 37 | resolver.(object) 38 | end 39 | end 40 | 41 | defimpl String.Chars do 42 | def to_string(union), do: union.name 43 | end 44 | 45 | defimpl Completion do 46 | alias GraphQL.Execution.Selection 47 | alias GraphQL.Type.AbstractType 48 | 49 | def complete_value(return_type, context, field_asts, info, result) do 50 | runtime_type = AbstractType.get_object_type(return_type, result, info.schema) 51 | Selection.complete_sub_fields(runtime_type, context, field_asts, result) 52 | end 53 | end 54 | end 55 | 56 | -------------------------------------------------------------------------------- /lib/graphql/lang/ast/reducer.ex: -------------------------------------------------------------------------------- 1 | 2 | # Used for computing a result as a function of the an AST traversal. The 3 | # traditional OO Visitor pattern does not really work well in functional 4 | # languages due to its reliance on generating side-effects. 5 | defmodule GraphQL.Lang.AST.Reducer do 6 | 7 | alias GraphQL.Lang.AST.{ 8 | Visitor, 9 | InitialisingVisitor, 10 | PostprocessingVisitor, 11 | Nodes 12 | } 13 | 14 | def reduce(node, visitor, accumulator) do 15 | accumulator = InitialisingVisitor.init(visitor, accumulator) 16 | accumulator = visit(node, visitor, accumulator) 17 | PostprocessingVisitor.finish(visitor, accumulator) 18 | end 19 | 20 | defp visit([child|rest], visitor, accumulator) do 21 | accumulator = visit(child, visitor, accumulator) 22 | visit(rest, visitor, accumulator) 23 | end 24 | 25 | defp visit([], _visitor, accumulator), do: accumulator 26 | 27 | defp visit(node, visitor, accumulator) do 28 | {next_action, accumulator} = Visitor.enter(visitor, node, accumulator) 29 | 30 | accumulator = if next_action != :skip do 31 | visit_children(node, visitor, accumulator) 32 | else 33 | accumulator 34 | end 35 | 36 | Visitor.leave(visitor, node, accumulator) 37 | end 38 | 39 | defp visit_children(node = %{kind: kind}, visitor, accumulator) when is_atom(kind) do 40 | children = for child_key <- Nodes.kinds[node[:kind]], Map.has_key?(node, child_key), do: node[child_key] 41 | visit_each_child(children, visitor, accumulator) 42 | end 43 | 44 | defp visit_each_child([child|rest], visitor, accumulator) do 45 | accumulator = visit(child, visitor, accumulator) 46 | accumulator = visit_each_child(rest, visitor, accumulator) 47 | accumulator 48 | end 49 | 50 | defp visit_each_child([], _visitor, accumulator), do: accumulator 51 | end 52 | -------------------------------------------------------------------------------- /test/graphql/lang/ast/composite_visitor_test.exs: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Lang.AST.CompositeVisitorTest do 3 | use ExUnit.Case, async: true 4 | 5 | alias GraphQL.Lang.Parser 6 | alias GraphQL.Lang.AST.Reducer 7 | alias GraphQL.Lang.AST.CompositeVisitor 8 | 9 | alias GraphQL.TestSupport.VisitorImplementations.{CallReverser, TracingVisitor} 10 | 11 | test "Composed Visitors are called in the correct order" do 12 | v0 = %CallReverser{} 13 | v1 = %TracingVisitor{name: "v1"} 14 | v2 = %TracingVisitor{name: "v2"} 15 | v3 = %TracingVisitor{name: "v3"} 16 | composite_visitor = CompositeVisitor.compose([v0, v1, v2, v3]) 17 | {:ok, ast} = Parser.parse "type Person {name: String}" 18 | calls = Reducer.reduce(ast, composite_visitor, %{calls: []}) 19 | assert calls == [ 20 | "v1 entering Document", "v2 entering Document", "v3 entering Document", 21 | "v1 entering ObjectTypeDefinition", "v2 entering ObjectTypeDefinition", 22 | "v3 entering ObjectTypeDefinition", "v1 entering Name", 23 | "v2 entering Name", "v3 entering Name", "v3 leaving Name", 24 | "v2 leaving Name", "v1 leaving Name", "v1 entering FieldDefinition", 25 | "v2 entering FieldDefinition", "v3 entering FieldDefinition", 26 | "v1 entering Name", "v2 entering Name", "v3 entering Name", 27 | "v3 leaving Name", "v2 leaving Name", "v1 leaving Name", 28 | "v1 entering NamedType", "v2 entering NamedType", "v3 entering NamedType", 29 | "v1 entering Name", "v2 entering Name", "v3 entering Name", 30 | "v3 leaving Name", "v2 leaving Name", "v1 leaving Name", 31 | "v3 leaving NamedType", "v2 leaving NamedType", "v1 leaving NamedType", 32 | "v3 leaving FieldDefinition", "v2 leaving FieldDefinition", 33 | "v1 leaving FieldDefinition", "v3 leaving ObjectTypeDefinition", 34 | "v2 leaving ObjectTypeDefinition", "v1 leaving ObjectTypeDefinition", 35 | "v3 leaving Document", "v2 leaving Document", "v1 leaving Document" 36 | ] 37 | end 38 | end 39 | 40 | 41 | -------------------------------------------------------------------------------- /lib/graphql/lang/ast/nodes.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Lang.AST.Nodes do 3 | @kinds %{ 4 | Name: [], 5 | Document: [:definitions], 6 | OperationDefinition: [:name, :variableDefinitions, :directives, :selectionSet], 7 | VariableDefinition: [:variable, :type, :defaultValue], 8 | Variable: [:name], 9 | SelectionSet: [:selections], 10 | Field: [:alias, :name, :arguments, :directives, :selectionSet], 11 | Argument: [:name, :value], 12 | FragmentSpread: [:name, :directives], 13 | InlineFragment: [:typeCondition, :directives, :selectionSet], 14 | FragmentDefinition: [:name, :typeCondition, :directives, :selectionSet], 15 | IntValue: [], 16 | FloatValue: [], 17 | StringValue: [], 18 | BooleanValue: [], 19 | EnumValue: [], 20 | ListValue: [:values], 21 | ObjectValue: [:fields], 22 | ObjectField: [:name, :value], 23 | Directive: [:name, :arguments], 24 | NamedType: [:name], 25 | ListType: [:type], 26 | NonNullType: [:type], 27 | ObjectTypeDefinition: [:name, :interfaces, :fields], 28 | FieldDefinition: [:name, :arguments, :type], 29 | InputValueDefinition: [:name, :type, :defaultValue], 30 | InterfaceTypeDefinition: [:name, :fields], 31 | UnionTypeDefinition: [:name, :types], 32 | ScalarTypeDefinition: [:name], 33 | EnumTypeDefinition: [:name, :values], 34 | EnumValueDefinition: [:name], 35 | InputObjectTypeDefinition: [:name, :fields], 36 | TypeExtensionDefinition: [:definition] 37 | } 38 | 39 | def kinds, do: @kinds 40 | 41 | @type operation_node :: %{ 42 | kind: :OperationDefinition, 43 | operation: atom 44 | } 45 | end 46 | 47 | -------------------------------------------------------------------------------- /lib/graphql/execution/resolvable.ex: -------------------------------------------------------------------------------- 1 | 2 | defprotocol GraphQL.Execution.Resolvable do 3 | @fallback_to_any true 4 | 5 | def resolve(resolvable, source, args, info) 6 | end 7 | 8 | defmodule GraphQL.Execution.ResolveWrapper do 9 | def wrap(fun) do 10 | try do 11 | case fun.() do 12 | {:ok, result} -> {:ok, result} 13 | {:error, message} -> {:error, message} 14 | result -> {:ok, result} 15 | end 16 | rescue 17 | e in RuntimeError -> {:error, e.message} 18 | _ in FunctionClauseError -> {:error, "Could not find a resolve function for this query."} 19 | end 20 | end 21 | end 22 | 23 | alias GraphQL.Execution.ResolveWrapper 24 | 25 | defimpl GraphQL.Execution.Resolvable, for: Function do 26 | def resolve(fun, source, args, info) do 27 | ResolveWrapper.wrap fn() -> 28 | case arity(fun) do 29 | 0 -> fun.() 30 | 1 -> fun.(source) 31 | 2 -> fun.(source, args) 32 | 3 -> fun.(source, args, info) 33 | end 34 | end 35 | end 36 | 37 | defp arity(fun), do: :erlang.fun_info(fun)[:arity] 38 | end 39 | 40 | defimpl GraphQL.Execution.Resolvable, for: Tuple do 41 | def resolve({mod, fun}, source, args, info), do: do_resolve(mod, fun, source, args, info) 42 | def resolve({mod, fun, _}, source, args, info), do: do_resolve(mod, fun, source, args, info) 43 | 44 | defp do_resolve(mod, fun, source, args, info) do 45 | ResolveWrapper.wrap fn() -> 46 | apply(mod, fun, [source, args, info]) 47 | end 48 | end 49 | end 50 | 51 | defimpl GraphQL.Execution.Resolvable, for: Atom do 52 | def resolve(nil, source, _args, info) do 53 | # NOTE: data keys and field names should be normalized to strings when we load the schema 54 | # and then we wouldn't need this Atom or String logic. 55 | {:ok, Map.get(source, info.field_name, Map.get(source, Atom.to_string(info.field_name)))} 56 | end 57 | end 58 | 59 | defimpl GraphQL.Execution.Resolvable, for: Any do 60 | def resolve(resolution, _source, _args, _info), do: {:ok, resolution} 61 | end 62 | -------------------------------------------------------------------------------- /test/support/star_wars/data.exs: -------------------------------------------------------------------------------- 1 | defmodule StarWars.Data do 2 | def get_character(id) do 3 | get_human(id) || get_droid(id) 4 | end 5 | 6 | def get_human(nil), do: nil 7 | def get_human(id) do 8 | Map.get(human_data, String.to_atom(id), nil) 9 | end 10 | 11 | def get_droid(nil), do: nil 12 | def get_droid(id) do 13 | Map.get(droid_data, String.to_atom(id), nil) 14 | end 15 | 16 | def get_friends(character) do 17 | Map.get(character, :friends) 18 | |> Enum.map(&(get_character(&1))) 19 | end 20 | 21 | def get_hero(5), do: luke 22 | def get_hero(_), do: artoo 23 | def get_hero, do: artoo 24 | 25 | def luke do 26 | %{id: "1000", 27 | name: "Luke Skywalker", 28 | friends: ["1002", "1003", "2000", "2001"], 29 | appears_in: [4,5,6], 30 | home_planet: "Tatooine"} 31 | end 32 | 33 | def vader do 34 | %{id: "1001", 35 | name: "Darth Vader", 36 | friends: ["1004"], 37 | appears_in: [4,5,6], 38 | home_planet: "Tatooine"} 39 | end 40 | 41 | def han do 42 | %{id: "1002", 43 | name: "Han Solo", 44 | friends: ["1000", "1003", "2001"], 45 | appears_in: [4,5,6]} 46 | end 47 | 48 | def leia do 49 | %{id: "1003", 50 | name: "Leia Organa", 51 | friends: ["1000", "1002", "2000", "2001"], 52 | appears_in: [4,5,6], 53 | home_planet: "Alderaan"} 54 | end 55 | 56 | def tarkin do 57 | %{id: "1004", 58 | name: "Wilhuff Tarkin", 59 | friends: ["1001"], 60 | appears_in: [4]} 61 | end 62 | 63 | def human_data do 64 | %{"1000": luke, "1001": vader, "1002": han, 65 | "1003": leia, "1004": tarkin} 66 | end 67 | 68 | def threepio do 69 | %{id: "2000", 70 | name: "C-3PO", 71 | friends: ["1000", "1002", "1003", "2001"], 72 | appears_in: [4,5,6], 73 | primary_function: "Protocol"} 74 | end 75 | 76 | def artoo do 77 | %{id: "2001", 78 | name: "R2-D2", 79 | friends: ["1000", "1002", "1003"], 80 | appears_in: [4,5,6], 81 | primary_function: "Astromech"} 82 | end 83 | 84 | def droid_data do 85 | %{"2000": threepio, "2001": artoo} 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/graphql/lang/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Lang.Parser do 2 | alias GraphQL.Lang.Lexer 3 | 4 | @moduledoc ~S""" 5 | GraphQL parser implemented with yecc. 6 | 7 | Parse a GraphQL query 8 | 9 | iex> GraphQL.parse "{ hello }" 10 | {:ok, %{definitions: [ 11 | %{kind: :OperationDefinition, loc: %{start: 0}, 12 | operation: :query, 13 | selectionSet: %{kind: :SelectionSet, loc: %{start: 0}, 14 | selections: [ 15 | %{kind: :Field, loc: %{start: 0}, name: "hello"} 16 | ] 17 | }} 18 | ], 19 | kind: :Document, loc: %{start: 0} 20 | }} 21 | """ 22 | 23 | @doc """ 24 | Parse the input string into a Document AST. 25 | 26 | iex> GraphQL.parse("{ hello }") 27 | {:ok, 28 | %{definitions: [ 29 | %{kind: :OperationDefinition, loc: %{start: 0}, 30 | operation: :query, 31 | selectionSet: %{kind: :SelectionSet, loc: %{start: 0}, 32 | selections: [ 33 | %{kind: :Field, loc: %{start: 0}, name: "hello"} 34 | ] 35 | }} 36 | ], 37 | kind: :Document, loc: %{start: 0} 38 | } 39 | } 40 | """ 41 | @spec parse(String.t) :: {:ok, GraphQL.Document.t} | {:error, GraphQL.Errors.t} 42 | def parse(input_string) when is_binary(input_string) do 43 | input_string |> to_char_list |> parse 44 | end 45 | 46 | @spec parse(char_list) :: {:ok, GraphQL.Document.t} | {:error, GraphQL.Errors.t} 47 | def parse(input_string) do 48 | case input_string |> Lexer.tokenize |> :graphql_parser.parse do 49 | {:ok, parse_result} -> 50 | {:ok, parse_result} 51 | {:error, {line_number, _, errors}} -> 52 | {:error, %{errors: [ 53 | %{"message" => "GraphQL: #{errors} on line #{line_number}", "line_number" => line_number} 54 | ]}} 55 | end 56 | end 57 | end 58 | 59 | defmodule GraphQL.Document do 60 | @moduledoc """ 61 | Ddefines the structure of a GraphQL document after it has been parsed. 62 | """ 63 | @type t :: %{definitions: list(Map), kind: atom, loc: Map} 64 | # defstruct [definitions: %{}, kind: :Document, loc: %{start: 0}] 65 | end 66 | -------------------------------------------------------------------------------- /lib/graphql/execution/executor.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Execution.Executor do 2 | alias GraphQL.Schema 3 | alias GraphQL.Execution.ExecutionContext 4 | alias GraphQL.Lang.AST.Nodes 5 | alias GraphQL.Util.ArrayMap 6 | 7 | alias GraphQL.Execution.Selection 8 | 9 | @type result_data :: {:ok, Map} 10 | 11 | @doc """ 12 | Execute a query against a schema. 13 | 14 | # iex> Executor.execute(schema, "{ hello }") 15 | # {:ok, %{hello: world}} 16 | """ 17 | @spec execute(GraphQL.Schema.t, GraphQL.Document.t, list) :: result_data | {:error, %{errors: list}} 18 | def execute(schema, document, opts \\ []) do 19 | schema = Schema.with_type_cache(schema) 20 | {root_value, variable_values, operation_name} = expand_options(opts) 21 | context = ExecutionContext.new(schema, document, root_value, variable_values, operation_name) 22 | 23 | case context.errors do 24 | [] -> execute_operation(context, context.operation, root_value) 25 | _ -> {:error, %{errors: Enum.dedup(context.errors)}} 26 | end 27 | end 28 | 29 | defp expand_options(opts) do 30 | {Keyword.get(opts, :root_value, %{}), 31 | Keyword.get(opts, :variable_values, %{}), 32 | Keyword.get(opts, :operation_name, nil)} 33 | end 34 | 35 | @spec execute_operation(ExecutionContext.t, Nodes.operation_node, map) :: result_data | {:error, String.t} 36 | defp execute_operation(context, operation, root_value) do 37 | type = Schema.operation_root_type(context.schema, operation) 38 | {context, %{fields: fields}} = Selection.collect_selections(context, type, operation.selectionSet) 39 | case operation.operation do 40 | :query -> 41 | {context, result} = Selection.execute_fields(context, type, root_value, fields) 42 | {:ok, ArrayMap.expand_result(result), context.errors} 43 | :mutation -> 44 | {context, result} = Selection.execute_fields_serially(context, type, root_value, fields) 45 | {:ok, ArrayMap.expand_result(result), context.errors} 46 | :subscription -> 47 | {:error, "Subscriptions not currently supported"} 48 | _ -> 49 | {:error, "Can only execute queries, mutations and subscriptions"} 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/graphql/execution/field_resolver.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Execution.FieldResolver do 3 | 4 | alias GraphQL.Execution.Arguments 5 | alias GraphQL.Execution.Resolvable 6 | alias GraphQL.Execution.ExecutionContext 7 | alias GraphQL.Execution.Completion 8 | alias GraphQL.Execution.Types 9 | alias GraphQL.Type.CompositeType 10 | 11 | def resolve_field(context, parent_type, source, field_asts) do 12 | field_ast = hd(field_asts) 13 | field_name = String.to_existing_atom(field_ast.name.value) 14 | 15 | if field_def = field_definition(parent_type, field_name) do 16 | return_type = field_def.type 17 | 18 | args = Arguments.argument_values( 19 | Map.get(field_def, :args, %{}), 20 | Map.get(field_ast, :arguments, %{}), 21 | context.variable_values 22 | ) 23 | 24 | info = %{ 25 | field_name: field_name, 26 | field_asts: field_asts, 27 | return_type: return_type, 28 | parent_type: parent_type, 29 | schema: context.schema, 30 | fragments: context.fragments, 31 | root_value: context.root_value, 32 | operation: context.operation, 33 | variable_values: context.variable_values 34 | } 35 | 36 | case resolve(field_def, source, args, info) do 37 | {:ok, nil} -> 38 | {context, nil} 39 | {:ok, result} -> 40 | Completion.complete_value(return_type, context, field_asts, info, result) 41 | {:error, message} -> 42 | {ExecutionContext.report_error(context, message), nil} 43 | end 44 | else 45 | {context, :undefined} 46 | end 47 | end 48 | 49 | defp resolve(field_def, source, args, info) do 50 | Resolvable.resolve( 51 | Map.get(field_def, :resolve), 52 | Types.unwrap_type(source), 53 | args, 54 | info 55 | ) 56 | end 57 | 58 | defp field_definition(parent_type, field_name) do 59 | case field_name do 60 | :__typename -> GraphQL.Type.Introspection.meta(:typename) 61 | :__schema -> GraphQL.Type.Introspection.meta(:schema) 62 | :__type -> GraphQL.Type.Introspection.meta(:type) 63 | _ -> CompositeType.get_field(parent_type, field_name) 64 | end 65 | end 66 | end 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/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|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 | -------------------------------------------------------------------------------- /lib/graphql/execution/ast_value.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Execution.ASTValue do 3 | alias GraphQL.Type.NonNull 4 | alias GraphQL.Type.List 5 | alias GraphQL.Type.Input 6 | alias GraphQL.Type.NonNull 7 | alias GraphQL.Type.CompositeType 8 | 9 | def value_from_ast(value_ast, %NonNull{ofType: inner_type}, variable_values) do 10 | value_from_ast(value_ast, inner_type, variable_values) 11 | end 12 | 13 | def value_from_ast(%{value: obj=%{kind: :ObjectValue}}, type=%Input{}, variable_values) do 14 | input_fields = CompositeType.get_fields(type) 15 | field_asts = Enum.reduce(obj.fields, %{}, fn(ast, result) -> 16 | Map.put(result, ast.name.value, ast) 17 | end) 18 | Enum.reduce(Map.keys(input_fields), %{}, fn(field_name, result) -> 19 | field = Map.get(input_fields, field_name) 20 | field_ast = Map.get(field_asts, to_string(field_name)) # this feels... brittle. 21 | inner_result = value_from_ast(field_ast, field.type, variable_values) 22 | case inner_result do 23 | nil -> result 24 | _ -> Map.put(result, field_name, inner_result) 25 | end 26 | end) 27 | end 28 | 29 | def value_from_ast(%{value: %{kind: :Variable, name: %{value: value}}}, type, variable_values) do 30 | case Map.get(variable_values, value) do 31 | nil -> nil 32 | variable_value -> GraphQL.Types.parse_value(type, variable_value) 33 | end 34 | end 35 | 36 | # if it isn't a variable or object input type, that means it's invalid 37 | # and we shoud return a nil 38 | def value_from_ast(_, %Input{}, _), do: nil 39 | 40 | def value_from_ast(%{value: %{kind: :ListValue, values: values_ast}}, type, _) do 41 | GraphQL.Types.parse_value(type, Enum.map(values_ast, fn(value_ast) -> 42 | value_ast.value 43 | end)) 44 | end 45 | 46 | def value_from_ast(value_ast, %List{ofType: inner_type}, variable_values) do 47 | [value_from_ast(value_ast, inner_type, variable_values)] 48 | end 49 | 50 | def value_from_ast(nil, _, _), do: nil # remove once NonNull is actually done.. 51 | 52 | def value_from_ast(value_ast, type, variable_values) when is_atom(type) do 53 | value_from_ast(value_ast, type.type, variable_values) 54 | end 55 | 56 | def value_from_ast(value_ast, type, _) do 57 | GraphQL.Types.parse_literal(type, value_ast.value) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/graphql/type/directive.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.Directive do 2 | @moduledoc """ 3 | Directives currently supported are @skip and @include 4 | """ 5 | defstruct name: "Directive", 6 | description: nil, 7 | locations: [], 8 | args: %{}, 9 | onOperation: false, 10 | onFragment: true, 11 | onField: true 12 | end 13 | 14 | defmodule GraphQL.Type.Directives do 15 | alias GraphQL.Type.{Directive, Boolean, NonNull, String} 16 | alias GraphQL.Util.Text 17 | 18 | def include do 19 | %Directive{ 20 | name: "include", 21 | description: 22 | """ 23 | Directs the executor to include this field or fragment 24 | only when the `if` argument is true. 25 | """ |> Text.normalize, 26 | locations: [:Field, :FragmentSpread, :InlineFragment], 27 | args: %{ 28 | if: %{ 29 | type: %NonNull{ofType: %Boolean{}}, 30 | name: "if", 31 | description: "Included when true.", 32 | defaultValue: nil 33 | } 34 | } 35 | } 36 | end 37 | 38 | def skip do 39 | %Directive{ 40 | name: "skip", 41 | description: 42 | """ 43 | Directs the executor to skip this field or fragment 44 | when the `if` argument is true. 45 | """ |> Text.normalize, 46 | locations: [:Field, :FragmentSpread, :InlineFragment], 47 | args: %{ 48 | if: %{ 49 | type: %NonNull{ofType: %Boolean{}}, 50 | name: "if", 51 | description: "Skipped when true.", 52 | defaultValue: nil 53 | } 54 | } 55 | } 56 | end 57 | 58 | def deprecated do 59 | %Directive{ 60 | name: "deprecated", 61 | description: 62 | """ 63 | Marks an element of a GraphQL schema as no longer supported. 64 | """, 65 | locations: [:FieldDefinition, :EnumValue], 66 | args: %{ 67 | reason: %{ 68 | type: %String{}, 69 | description: """ 70 | Explains why this element was deprecated, usually also including a 71 | suggestion for how to access supported similar data. Formatted 72 | in [Markdown](https://daringfireball.net/projects/markdown/). 73 | """, 74 | defaultValue: "No longer supported" 75 | } 76 | } 77 | } 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/graphql/validation/validator.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Validation.Validator do 3 | 4 | alias GraphQL.Lang.AST.{ 5 | CompositeVisitor, 6 | ParallelVisitor, 7 | TypeInfoVisitor, 8 | TypeInfo, 9 | DocumentInfo, 10 | Reducer 11 | } 12 | 13 | alias GraphQL.Schema 14 | 15 | alias GraphQL.Validation.Rules 16 | 17 | @doc """ 18 | Runs validations against the document with all known validation rules. 19 | """ 20 | def validate(schema, document) do 21 | validate_with_rules(schema, document, Rules.all) 22 | end 23 | 24 | @doc """ 25 | Short circuits the validations entirely when there are no rules specified. 26 | This is useful for examining the performance impact of validations. 27 | """ 28 | def validate_with_rules(_schema, _document, rules) when length(rules) == 0, do: :ok 29 | 30 | @doc """ 31 | For performance testing with a single rule, the overhead of the ParallelVisitor 32 | can be removed. 33 | """ 34 | def validate_with_rules(schema, document, [rule|[]] = rules) when length(rules) == 1 do 35 | schema = Schema.with_type_cache(schema) 36 | validation_pipeline = CompositeVisitor.compose([ 37 | %TypeInfoVisitor{}, 38 | rule 39 | ]) 40 | result = Reducer.reduce(document, validation_pipeline, %{ 41 | type_info: %TypeInfo{schema: schema}, 42 | document_info: DocumentInfo.new(schema, document), 43 | document: document, 44 | validation_errors: [] 45 | }) 46 | errors = result[:validation_errors] 47 | if length(errors) > 0 do 48 | {:error, Enum.reverse(errors)} 49 | else 50 | :ok 51 | end 52 | end 53 | 54 | @doc """ 55 | Runs validations against the document with only the specified rules. 56 | """ 57 | def validate_with_rules(schema, document, rules) do 58 | schema = Schema.with_type_cache(schema) 59 | validation_pipeline = CompositeVisitor.compose([ 60 | %TypeInfoVisitor{}, 61 | %ParallelVisitor{visitors: rules} 62 | ]) 63 | result = Reducer.reduce(document, validation_pipeline, %{ 64 | type_info: %TypeInfo{schema: schema}, 65 | document_info: DocumentInfo.new(schema, document), 66 | document: document, 67 | validation_errors: [] 68 | }) 69 | errors = result[:validation_errors] 70 | if length(errors) > 0 do 71 | {:error, Enum.reverse(errors)} 72 | else 73 | :ok 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/graphql/util/array_map.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Util.ArrayMap do 2 | @moduledoc """ 3 | ArrayMap is used for representing lists in intermediate results. 4 | This means the entire intermediate Executor result representation can 5 | be manipulated with the Access protocol which will allow for patching 6 | the entire structure in an ad-hoc manner. This is key to implementing 7 | deferred resolvers. 8 | """ 9 | 10 | @behaviour Access 11 | 12 | defstruct map: %{} 13 | 14 | def new(map) do 15 | if !Enum.all?(Map.keys(map), fn(key) -> is_integer(key) end) do 16 | raise "all key must be integers!" 17 | end 18 | %__MODULE__{map: map} 19 | end 20 | 21 | def put(array_map, index, value) when is_integer(index) do 22 | %__MODULE__{ map: Map.put(array_map.map, index, value) } 23 | end 24 | 25 | # Access behaviour 26 | def fetch(array_map, key) do 27 | case Access.fetch(array_map.map, key) do 28 | {:ok, value} -> %__MODULE__{map: value} 29 | :error -> :error 30 | end 31 | end 32 | 33 | def get_and_update(array_map, key, list) do 34 | {value, map} = Access.get_and_update(array_map.map, key, list) 35 | {value, %__MODULE__{map: map}} 36 | end 37 | 38 | def get(array_map, key, value) do 39 | map = Access.get(array_map.map, key, value) 40 | %__MODULE__{map: map} 41 | end 42 | 43 | def pop(array_map, key) do 44 | {value, map} = Access.get(array_map.map, key) 45 | {value, %__MODULE__{map: map}} 46 | end 47 | 48 | @doc """ 49 | Converts an intermediate executor result that contains ArrayMaps into one 50 | where the array maps are converted into lists. 51 | """ 52 | def expand_result(result) when is_list(result) do 53 | Enum.map(result, &expand_result/1) 54 | end 55 | def expand_result(%__MODULE__{} = result) do 56 | Enum.reduce(Enum.sort(Map.keys(result.map)), [], fn(index, acc) -> 57 | [expand_result(Map.get(result.map, index))] ++ acc 58 | end) |> Enum.reverse 59 | end 60 | # Without the following we run into an issue when attempting to process 61 | # structs because they are not enumerable. 62 | # 63 | # FIXME: We need a better way of detecting scalars (Eg: DateTime) 64 | def expand_result(%{__struct__: _} = result), do: result 65 | def expand_result(result) when is_map(result) do 66 | Enum.reduce(result, %{}, fn({k, v}, acc) -> 67 | Map.put(acc, expand_result(k), expand_result(v)) 68 | end) 69 | end 70 | def expand_result(result), do: result 71 | 72 | end 73 | -------------------------------------------------------------------------------- /lib/graphql/validation/rules/provided_non_null_arguments.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Validation.Rules.ProvidedNonNullArguments do 3 | 4 | alias GraphQL.Lang.AST.{Visitor, TypeInfo} 5 | alias GraphQL.Type 6 | import GraphQL.Validation 7 | 8 | defstruct name: "ProvidedNonNullArguments" 9 | 10 | defimpl Visitor do 11 | 12 | def enter(_visitor, _node, accumulator) do 13 | {:continue, accumulator} 14 | end 15 | 16 | def leave(_visitor, %{kind: :Field} = node, accumulator) do 17 | field_arguments = case TypeInfo.field_def(accumulator[:type_info]) do 18 | %{args: arguments} -> Map.to_list(arguments) 19 | _ -> [] 20 | end 21 | report_errors(accumulator, field_arguments, node, make_name_to_arg_map(node)) 22 | end 23 | 24 | # TODO: arg check directives once we have implemented directive support 25 | #def leave(_visitor, %{kind: :Directive} = node, accumulator) do 26 | # directive_arguments = case TypeInfo.directive(accumulator[:type_info]) do 27 | # %{args: arguments} -> Map.to_list(arguments) 28 | # _ -> [] 29 | # end 30 | # {:continue, report_errors(accumulator, directive_arguments, node, make_name_to_arg_map(node))} 31 | #end 32 | 33 | def leave(_visitor, _, accumulator), do: accumulator 34 | 35 | defp arguments_from_ast_node(%{arguments: arguments}), do: arguments 36 | defp arguments_from_ast_node(_), do: [] 37 | 38 | defp report_errors(accumulator, [], _node, _name_to_arg_map), do: accumulator 39 | defp report_errors(accumulator, [name_and_arg|remaining_args], node, name_to_arg_map) do 40 | { arg_name, arg } = name_and_arg 41 | arg_node = name_to_arg_map[Atom.to_string(arg_name)] 42 | is_missing = !arg_node 43 | 44 | case {is_missing, arg.type} do 45 | {true, %Type.NonNull{}} -> 46 | report_error( 47 | accumulator, 48 | missing_field_arg_message(node.name.value, arg_name, arg.type) 49 | ) 50 | _ -> accumulator 51 | end |> report_errors(remaining_args, node, name_to_arg_map) 52 | end 53 | 54 | defp missing_field_arg_message(field_name, arg_name, arg_type) do 55 | "Field \"#{field_name}\" argument \"#{arg_name}\" of type \"#{arg_type}\" is required but not provided." 56 | end 57 | 58 | defp make_name_to_arg_map(node) do 59 | Enum.reduce(arguments_from_ast_node(node), %{}, fn(arg,map) -> 60 | Map.merge(%{ arg.name.value => arg}, map) 61 | end) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/mix/tasks/compile.graphql.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Compile.Graphql do 2 | use Mix.Task 3 | 4 | @recursive true 5 | @manifest ".compile.graphql" 6 | 7 | @moduledoc """ 8 | Compiles `.graphql` files which have changed since the last generation. 9 | 10 | Currently only handles schema files, but will support queries in future. 11 | 12 | To use this you need to add the `:graphql` compiler to the front ofyour compiler chain. 13 | This just needs to be anywhere before the Elixir compiler because we are 14 | generating Elixir code. 15 | 16 | In your `mix.exs` project in a Phoenix project for example: 17 | 18 | compilers: [:phoenix, :graphql] ++ Mix.compilers 19 | 20 | You also need to tell the GraphQL compiler which files to pick up. 21 | 22 | In `config/config.exs` 23 | 24 | config :graphql, source_path: "web/graphql/**/*_schema.graphql" 25 | 26 | 27 | ## Command line options 28 | 29 | * `--force` - forces compilation regardless of modification times 30 | 31 | """ 32 | 33 | @spec run(OptionParser.argv) :: :ok | :noop 34 | def run(args) do 35 | {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean]) 36 | 37 | graphql_schema_glob = Application.get_env(:graphql, :source_path) 38 | 39 | changed = 40 | graphql_schema_glob 41 | |> Path.wildcard 42 | |> compile_all(opts) 43 | 44 | if Enum.any?(changed, &(&1 == :ok)) do 45 | :ok 46 | else 47 | :noop 48 | end 49 | end 50 | 51 | @doc """ 52 | Returns GraphQL manifests. 53 | """ 54 | def manifests, do: [manifest] 55 | defp manifest, do: Path.join(Mix.Project.manifest_path, @manifest) 56 | 57 | def compile_all(schema_paths, opts) do 58 | Enum.map schema_paths, &(compile(&1, opts)) 59 | end 60 | 61 | def compile(schema_path, opts) do 62 | base_filename = extract_file_prefix(schema_path) 63 | target = base_filename <> ".ex" 64 | if opts[:force] || Mix.Utils.stale?([schema_path], [target]) do 65 | Mix.shell.info "Compiling `#{schema_path}` to `#{target}`" 66 | with target, 67 | {:ok, source_schema} <- File.read(schema_path), 68 | {:ok, generated_schema} <- GraphQL.Schema.Generator.generate(base_filename, source_schema), 69 | do: File.write!(target, generated_schema) 70 | else 71 | Mix.shell.info "Skipping `#{schema_path}`" 72 | end 73 | end 74 | 75 | def extract_file_prefix(path) do 76 | Path.join(Path.dirname(path), Path.basename(path, ".graphql")) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/graphql/execution/execution_context.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Execution.ExecutionContext do 3 | 4 | defstruct [:schema, :fragments, :root_value, :operation, :variable_values, :errors] 5 | @type t :: %__MODULE__{ 6 | schema: GraphQL.Schema.t, 7 | fragments: struct, 8 | root_value: Map, 9 | operation: Map, 10 | variable_values: Map, 11 | errors: list(GraphQL.Error.t) 12 | } 13 | 14 | @spec new(GraphQL.Schema.t, GraphQL.Document.t, map, map, String.t) :: __MODULE__.t 15 | def new(schema, document, root_value, variable_values, operation_name) do 16 | 17 | initial_context = %__MODULE__{ 18 | schema: schema, 19 | fragments: %{}, 20 | root_value: root_value, 21 | operation: nil, 22 | variable_values: variable_values || %{}, 23 | errors: [] 24 | } 25 | 26 | document.definitions 27 | |> Enum.reduce(initial_context, build_definition_handler(operation_name)) 28 | |> validate_operation_exists(operation_name) 29 | end 30 | 31 | defp build_definition_handler(operation_name) do 32 | fn(definition, context) -> handle_definition(operation_name, definition, context) end 33 | end 34 | 35 | defp handle_definition(operation_name, definition = %{kind: :OperationDefinition}, context) do 36 | multiple_operations_no_operation_name = !operation_name && context.operation 37 | should_set_operation = !operation_name || definition.name.value === operation_name 38 | cond do 39 | multiple_operations_no_operation_name -> 40 | report_error(context, "Must provide operation name if query contains multiple operations.") 41 | should_set_operation -> 42 | context = %{context | operation: definition} 43 | %{context | variable_values: GraphQL.Execution.Variables.extract(context) } 44 | true -> context 45 | end 46 | end 47 | 48 | defp handle_definition(_, definition = %{kind: :FragmentDefinition}, context) do 49 | put_in(context.fragments[definition.name.value], definition) 50 | end 51 | 52 | defp validate_operation_exists(context, nil), do: context 53 | defp validate_operation_exists(context = %{operation: nil}, operation_name) do 54 | report_error(context, "Operation `#{operation_name}` not found in query.") 55 | end 56 | defp validate_operation_exists(context, _operation_name), do: context 57 | 58 | @spec report_error(__MODULE__.t, String.t) :: __MODULE__.t 59 | def report_error(context, msg) do 60 | put_in(context.errors, [%{"message" => msg} | context.errors]) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/graphql/type/interface.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.Interface do 2 | 3 | alias GraphQL.Type.AbstractType 4 | 5 | @type t :: %GraphQL.Type.Interface{ 6 | name: binary, 7 | description: binary | nil, 8 | fields: Map.t | function, 9 | resolver: (any -> GraphQL.Type.ObjectType.t) | nil 10 | } 11 | defstruct name: "", description: "", fields: %{}, resolver: nil 12 | 13 | def new(map) do 14 | struct(GraphQL.Type.Interface, map) 15 | end 16 | 17 | 18 | defimpl AbstractType do 19 | @doc """ 20 | Returns a boolean indicating if the provided type implements the interface 21 | """ 22 | def possible_type?(interface, object) do 23 | GraphQL.Type.implements?(object, interface) 24 | end 25 | 26 | @doc """ 27 | Unlike Union, Interfaces don't explicitly declare what Types implement them, 28 | so we have to iterate over a full typemap and filter the Types in the Schema 29 | down to just those that implement the provided interface. 30 | """ 31 | def possible_types(interface, schema) do 32 | # get the complete typemap from this schema 33 | schema.type_cache 34 | # filter them down to a list of types that implement this interface 35 | |> Enum.filter(fn {_, typedef} -> GraphQL.Type.implements?(typedef, interface) end) 36 | # then return the type, instead of the {name, type} tuple that comes from 37 | # the type_cache 38 | |> Enum.map(fn({_,v}) -> v end) 39 | end 40 | 41 | @doc """ 42 | Returns the typedef of the provided ObjectType using either the Interface's 43 | resolve function (if it exists), or by iterating over all the typedefs that 44 | implement this Interface and returning the first one that matches against 45 | the ObjectType's isTypeOf function. 46 | """ 47 | def get_object_type(interface, object, schema) do 48 | if interface.resolver do 49 | interface.resolver.(object) 50 | else 51 | AbstractType.possible_types(interface, schema) 52 | |> Enum.find(fn(x) -> x.isTypeOf.(object) end) 53 | end 54 | end 55 | end 56 | 57 | defimpl String.Chars do 58 | def to_string(iface), do: iface.name 59 | end 60 | 61 | defimpl GraphQL.Execution.Completion do 62 | alias GraphQL.Execution.Selection 63 | 64 | def complete_value(return_type, context, field_asts, info, result) do 65 | runtime_type = AbstractType.get_object_type(return_type, result, info.schema) 66 | Selection.complete_sub_fields(runtime_type, context, field_asts, result) 67 | end 68 | end 69 | end 70 | 71 | -------------------------------------------------------------------------------- /test/graphql/type/serialization_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Lang.Type.SerializationTest do 2 | use ExUnit.Case, async: true 3 | import GraphQL.Types 4 | 5 | test "serializes output int" do 6 | assert 1 == serialize(%GraphQL.Type.Int{}, 1) 7 | assert 0 == serialize(%GraphQL.Type.Int{}, 0) 8 | assert -1 == serialize(%GraphQL.Type.Int{}, -1) 9 | assert 0 == serialize(%GraphQL.Type.Int{}, 0.1) 10 | assert 1 == serialize(%GraphQL.Type.Int{}, 1.1) 11 | assert -1 == serialize(%GraphQL.Type.Int{}, -1.1) 12 | assert 100 == serialize(%GraphQL.Type.Int{}, 1.0e2) 13 | assert nil == serialize(%GraphQL.Type.Int{}, 9876504321) 14 | assert nil == serialize(%GraphQL.Type.Int{}, -9876504321) 15 | assert nil == serialize(%GraphQL.Type.Int{}, 1.0e100) 16 | assert nil == serialize(%GraphQL.Type.Int{}, -1.0e100) 17 | assert -1 == serialize(%GraphQL.Type.Int{}, "-1.1") 18 | assert nil == serialize(%GraphQL.Type.Int{}, "one") 19 | assert 0 == serialize(%GraphQL.Type.Int{}, false) 20 | assert 1 == serialize(%GraphQL.Type.Int{}, true) 21 | end 22 | 23 | test "serializes output float" do 24 | assert 1.0 == serialize(%GraphQL.Type.Float{}, 1) 25 | assert 0.0 == serialize(%GraphQL.Type.Float{}, 0) 26 | assert -1.0 == serialize(%GraphQL.Type.Float{}, -1) 27 | assert 0.1 == serialize(%GraphQL.Type.Float{}, 0.1) 28 | assert 1.1 == serialize(%GraphQL.Type.Float{}, 1.1) 29 | assert -1.1 == serialize(%GraphQL.Type.Float{}, -1.1) 30 | assert -1.1 == serialize(%GraphQL.Type.Float{}, "-1.1") 31 | assert nil == serialize(%GraphQL.Type.Float{}, "one") 32 | assert 0.0 == serialize(%GraphQL.Type.Float{}, false) 33 | assert 1.0 == serialize(%GraphQL.Type.Float{}, true) 34 | end 35 | 36 | test "serializes output strings" do 37 | assert "string" == serialize(%GraphQL.Type.String{}, "string") 38 | assert "1" == serialize(%GraphQL.Type.String{}, 1) 39 | assert "-1.1" == serialize(%GraphQL.Type.String{}, -1.1) 40 | assert "true" == serialize(%GraphQL.Type.String{}, true) 41 | assert "false" == serialize(%GraphQL.Type.String{}, false) 42 | end 43 | 44 | test "serializes output boolean" do 45 | assert true == serialize(%GraphQL.Type.Boolean{}, "string") 46 | assert false == serialize(%GraphQL.Type.Boolean{}, "") 47 | assert true == serialize(%GraphQL.Type.Boolean{}, 1) 48 | assert false == serialize(%GraphQL.Type.Boolean{}, 0) 49 | assert true == serialize(%GraphQL.Type.Boolean{}, true) 50 | assert false == serialize(%GraphQL.Type.Boolean{}, false) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/graphql/lang/ast/parallel_visitor.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Lang.AST.ParallelVisitor do 3 | @moduledoc ~S""" 4 | A ParallelVisitor runs all child visitors in parallel instead of serially like the CompositeVisitor. 5 | 6 | In this context, 'in parallel' really means that for each node in the AST, each visitor will be invoked 7 | for each node in the AST, but the :skip/:continue return value of enter and leave is maintained per-visitor. 8 | 9 | This means invividual visitors can bail out of AST processing as soon as possible and not waste cycles. 10 | 11 | This code based on the graphql-js *visitInParallel* function. 12 | """ 13 | 14 | alias GraphQL.Lang.AST.{ 15 | Visitor, 16 | InitialisingVisitor, 17 | PostprocessingVisitor 18 | } 19 | 20 | defstruct visitors: [] 21 | 22 | defimpl Visitor do 23 | def enter(visitor, node, accumulator) do 24 | visitors = Enum.filter(visitor.visitors, fn(child_visitor) -> 25 | !skipping?(accumulator, child_visitor) 26 | end) 27 | accumulator = Enum.reduce(visitors, accumulator, fn(child_visitor, accumulator) -> 28 | case child_visitor |> Visitor.enter(node, accumulator) do 29 | {:continue, next_accumulator} -> next_accumulator 30 | {:skip, next_accumulator} -> 31 | put_in(next_accumulator[:skipping][child_visitor], node) 32 | end 33 | end) 34 | if length(visitors) > 0 do 35 | {:continue, accumulator} 36 | else 37 | {:skip, accumulator} 38 | end 39 | end 40 | 41 | def leave(visitor, node, accumulator) do 42 | Enum.reduce visitor.visitors, accumulator, fn(child_visitor, accumulator) -> 43 | cond do 44 | !skipping?(accumulator, child_visitor) -> 45 | child_visitor |> Visitor.leave(node, accumulator) 46 | accumulator[:skipping][child_visitor] == node -> 47 | Map.delete(accumulator[:skipping], child_visitor) 48 | true -> accumulator 49 | end 50 | end 51 | end 52 | 53 | defp skipping?(accumulator, child_visitor) do 54 | Map.has_key?(accumulator[:skipping], child_visitor) 55 | end 56 | end 57 | 58 | defimpl InitialisingVisitor do 59 | def init(visitor, accumulator) do 60 | accumulator = put_in(accumulator[:skipping], %{}) 61 | Enum.reduce(visitor.visitors, accumulator, &InitialisingVisitor.init/2) 62 | end 63 | end 64 | 65 | defimpl PostprocessingVisitor do 66 | def finish(visitor, accumulator) do 67 | Enum.reduce(visitor.visitors, accumulator, &PostprocessingVisitor.finish/2) 68 | end 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /test/graphql/validation/rules/unique_operation_names_test.exs: -------------------------------------------------------------------------------- 1 | 2 | Code.require_file "../../../support/validations.exs", __DIR__ 3 | 4 | defmodule GraphQL.Validation.Rules.UniqueOperationNamesTest do 5 | use ExUnit.Case, async: true 6 | 7 | import ValidationsSupport 8 | 9 | alias GraphQL.Validation.Rules.UniqueOperationNames 10 | 11 | test "no operations" do 12 | assert_passes_rule(""" 13 | fragment fragA on Type { 14 | field 15 | } 16 | """, %UniqueOperationNames{}) 17 | end 18 | 19 | test "one anon operation" do 20 | assert_passes_rule(""" 21 | { 22 | field 23 | } 24 | """, %UniqueOperationNames{}) 25 | end 26 | 27 | test "one named operation" do 28 | assert_passes_rule(""" 29 | query Foo { 30 | field 31 | } 32 | """, %UniqueOperationNames{}) 33 | end 34 | 35 | test "multiple operations" do 36 | assert_passes_rule(""" 37 | query Foo { 38 | field 39 | } 40 | 41 | query Bar { 42 | field 43 | } 44 | """, %UniqueOperationNames{}) 45 | end 46 | 47 | test "multiple operations of different types" do 48 | assert_passes_rule(""" 49 | query Foo { 50 | field 51 | } 52 | 53 | mutation Bar { 54 | field 55 | } 56 | 57 | # TODO: add this when subscription support is added 58 | #subscription Baz { 59 | # field 60 | #} 61 | """, %UniqueOperationNames{}) 62 | end 63 | 64 | test "fragment and operation named the same" do 65 | assert_passes_rule(""" 66 | query Foo { 67 | ...Foo 68 | } 69 | fragment Foo on Type { 70 | field 71 | } 72 | """, %UniqueOperationNames{}) 73 | end 74 | 75 | test "multiple operations of same name" do 76 | assert_fails_rule(""" 77 | query Foo { 78 | fieldA 79 | } 80 | query Foo { 81 | fieldB 82 | } 83 | """, %UniqueOperationNames{}) 84 | end 85 | 86 | test "multiple ops of same name of different types (mutation)" do 87 | assert_fails_rule(""" 88 | query Foo { 89 | fieldA 90 | } 91 | mutation Foo { 92 | fieldB 93 | } 94 | """, %UniqueOperationNames{}) 95 | end 96 | 97 | @tag :skip 98 | test "multiple ops of same name of different types (subscription)" do 99 | assert_fails_rule(""" 100 | query Foo { 101 | fieldA 102 | } 103 | subscription Foo { 104 | fieldB 105 | } 106 | """, %UniqueOperationNames{}) 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/graphql/lang/ast/type_info.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Lang.AST.TypeInfo do 3 | @moduledoc ~S""" 4 | TypeInfo maintains type metadata pertaining to the current node of a query AST, 5 | and is generated by the TypeInfoVistor. 6 | 7 | The type information is made available to validation rules. 8 | """ 9 | 10 | alias GraphQL.Util.Stack 11 | alias GraphQL.Type.{ 12 | CompositeType, 13 | Introspection, 14 | List, 15 | NonNull, 16 | Interface, 17 | ObjectType 18 | } 19 | 20 | defstruct schema: nil, 21 | type_stack: %Stack{}, 22 | parent_type_stack: %Stack{}, 23 | input_type_stack: %Stack{}, 24 | field_def_stack: %Stack{}, 25 | directive: nil, 26 | argument: nil 27 | 28 | @doc """ 29 | Return the top of the type stack, or nil if empty. 30 | """ 31 | def type(type_info), do: Stack.peek(type_info.type_stack) 32 | 33 | @doc """ 34 | Dereferences a type to a proper type. If the type is a List or NonNull it is dereferenced, 35 | otherwise it just returns its type argument. 36 | """ 37 | def named_type(%List{} = type), do: named_type(type.ofType) 38 | def named_type(%NonNull{} = type), do: named_type(type.ofType) 39 | def named_type(type), do: type 40 | 41 | @doc """ 42 | Return the top of the parent type stack, or nil if empty. 43 | """ 44 | def parent_type(type_info) do 45 | Stack.peek(type_info.parent_type_stack) 46 | end 47 | 48 | @doc """ 49 | Return the top of the input type stack, or nil if empty. 50 | """ 51 | def input_type(type_info) do 52 | Stack.peek(type_info.input_type_stack) 53 | end 54 | 55 | @doc """ 56 | Return the top of the field def stack, or nil if empty. 57 | """ 58 | def field_def(type_info) do 59 | Stack.peek(type_info.field_def_stack) 60 | end 61 | 62 | @doc """ 63 | Return the current directive 64 | """ 65 | def directive(type_info) do 66 | type_info.directive 67 | end 68 | 69 | def find_field_def(schema, parent_type, field_node) do 70 | cond do 71 | field_node.name.value == Introspection.meta(:schema)[:name] && schema.query == parent_type -> 72 | Introspection.meta(:schema) 73 | field_node.name.value == Introspection.meta(:type)[:name] && schema.query == parent_type -> 74 | Introspection.meta(:type) 75 | field_node.name.value == Introspection.meta(:typename)[:name] -> 76 | Introspection.meta(:typename) 77 | true -> 78 | find_field_def(parent_type, field_node) 79 | end 80 | end 81 | 82 | defp find_field_def(%Interface{} = parent_type, field_node) do 83 | CompositeType.get_field(parent_type, field_node.name.value) 84 | end 85 | defp find_field_def(%ObjectType{} = parent_type, field_node) do 86 | CompositeType.get_field(parent_type, field_node.name.value) 87 | end 88 | defp find_field_def(_, _), do: nil 89 | end 90 | -------------------------------------------------------------------------------- /lib/graphql/lang/ast/visitor.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Lang.AST do 3 | defprotocol Visitor do 4 | @moduledoc """ 5 | Implementations of Visitor are used by the ASTReducer to transform a GraphQL AST 6 | into an arbitrary value. 7 | 8 | The value can be the result of validations, or a transformation of the AST into 9 | a new AST, for example. 10 | 11 | The fallback implementations of 'enter' and 'leave' return the accumulator untouched. 12 | """ 13 | 14 | @fallback_to_any true 15 | 16 | @doc """ 17 | Called when entering a node of the AST. 18 | 19 | The return value should be: 20 | 21 | {next_action, acc} 22 | 23 | where next_action is either :break or :continue and acc is the new value of the accumulator. 24 | 25 | :break will abort the visitor and AST traversal will cease returning the current value of the accumulator. 26 | """ 27 | def enter(visitor, node, accumulator) 28 | 29 | @doc """ 30 | Called when leaving a node of the AST. 31 | 32 | The return value should be: 33 | 34 | acc 35 | """ 36 | def leave(visitor, node, accumulator) 37 | end 38 | 39 | defimpl Visitor, for: Any do 40 | def enter(_visitor, _node, accumulator), do: {:continue, accumulator} 41 | def leave(_visitor, _node, accumulator), do: accumulator 42 | end 43 | 44 | defprotocol InitialisingVisitor do 45 | @moduledoc """ 46 | A Visitor that implements this protocol will have the opportunity to perform some 47 | initialisation and set up the accumulator before AST traversal is started. 48 | 49 | The fallback implementation returns the accumulator untouched. 50 | """ 51 | 52 | @fallback_to_any true 53 | 54 | @doc """ 55 | Invoked before traversal begins. The Visitor can do any once-off accumulator initialisation here. 56 | """ 57 | def init(visitor, accumulator) 58 | end 59 | 60 | defprotocol PostprocessingVisitor do 61 | @moduledoc """ 62 | A Visitor that implements this protocol will have the opportunity to transform 63 | the accumulator into something more consumer friendly. It can often be the case that 64 | the working form of the accumulator is not what should be returned from ASTReducer.reduce/3. 65 | 66 | This also means that the accumulator (always a Map) can be transformed into an 67 | arbitrary struct or other Erlang/Elixir type. 68 | 69 | The fallback implementation returns the accumulator untouched. 70 | """ 71 | 72 | @fallback_to_any true 73 | 74 | @doc """ 75 | Invoked once after traversal ends. This can be used to transform the accumulator 76 | into an arbitrary value. 77 | """ 78 | def finish(visitor, accumulator) 79 | end 80 | 81 | defimpl InitialisingVisitor, for: Any do 82 | def init(_visitor, accumulator), do: accumulator 83 | end 84 | 85 | defimpl PostprocessingVisitor, for: Any do 86 | def finish(_visitor, accumulator), do: accumulator 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/graphql/execution/mutations_test.exs: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Execution.Executor.MutationsTest do 3 | use ExUnit.Case, async: true 4 | 5 | import ExUnit.TestHelpers 6 | 7 | alias GraphQL 8 | alias GraphQL.Schema 9 | alias GraphQL.Type.ObjectType 10 | alias GraphQL.Type.Int 11 | 12 | defmodule NumberHolder do 13 | def type do 14 | %ObjectType{ 15 | name: "NumberHolder", 16 | fields: %{ 17 | theNumber: %{type: %Int{}} 18 | } 19 | } 20 | end 21 | end 22 | 23 | defmodule TestSchema do 24 | def schema do 25 | Schema.new(%{ 26 | query: %ObjectType{ 27 | name: "Query", 28 | fields: %{ 29 | theNumber: %{type: NumberHolder.type} 30 | } 31 | }, 32 | mutation: %ObjectType{ 33 | name: "Mutation", 34 | fields: %{ 35 | changeTheNumber: %{ 36 | type: NumberHolder.type, 37 | args: %{ newNumber: %{ type: %Int{} }}, 38 | resolve: fn(source, %{ newNumber: newNumber }) -> 39 | Map.put(source, :theNumber, newNumber) 40 | end 41 | }, 42 | failToChangeTheNumber: %{ 43 | type: NumberHolder.type, 44 | args: %{ newNumber: %{ type: %Int{} }}, 45 | resolve: fn(_, %{ newNumber: _ }) -> 46 | raise "Cannot change the number" 47 | end 48 | } 49 | } 50 | } 51 | }) 52 | end 53 | end 54 | 55 | test "evaluates mutations serially" do 56 | doc = """ 57 | mutation M { 58 | first: changeTheNumber(newNumber: 1) { 59 | theNumber 60 | }, 61 | second: changeTheNumber(newNumber: 2) { 62 | theNumber 63 | }, 64 | third: changeTheNumber(newNumber: 3) { 65 | theNumber 66 | } 67 | } 68 | """ 69 | 70 | {:ok, result} = execute(TestSchema.schema, doc) 71 | 72 | assert_data(result, %{ 73 | first: %{theNumber: 1}, 74 | second: %{theNumber: 2}, 75 | third: %{theNumber: 3}, 76 | }) 77 | end 78 | 79 | test "evaluates mutations correctly in the presense of a failed mutation" do 80 | doc = """ 81 | mutation M { 82 | first: changeTheNumber(newNumber: 1) { 83 | theNumber 84 | }, 85 | second: failToChangeTheNumber(newNumber: 2) { 86 | theNumber 87 | } 88 | third: changeTheNumber(newNumber: 3) { 89 | theNumber 90 | } 91 | } 92 | """ 93 | 94 | {:ok, result} = execute(TestSchema.schema, doc) 95 | 96 | assert_data(result, %{ 97 | first: %{ 98 | theNumber: 1 99 | }, 100 | second: nil, 101 | third: %{ 102 | theNumber: 3 103 | } 104 | }) 105 | 106 | assert_has_error(result, %{"message" => "Cannot change the number"}) 107 | end 108 | end 109 | 110 | -------------------------------------------------------------------------------- /lib/graphql/validation/rules/fields_on_correct_type.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Validation.Rules.FieldsOnCorrectType do 3 | 4 | alias GraphQL.Lang.AST.{Visitor, TypeInfo} 5 | alias GraphQL.Type.{CompositeType, ObjectType, AbstractType} 6 | alias GraphQL.Type 7 | import GraphQL.Validation 8 | 9 | defstruct name: "FieldsOnCorrectType" 10 | 11 | defimpl Visitor do 12 | 13 | @max_type_suggestions 5 14 | 15 | def enter(_visitor, %{kind: :Field} = node, accumulator) do 16 | schema = accumulator[:type_info].schema 17 | parent_type = TypeInfo.parent_type(accumulator[:type_info]) 18 | field_def = TypeInfo.find_field_def(schema, parent_type, node) 19 | if parent_type && !field_def do 20 | {:continue, report_error( 21 | accumulator, 22 | undefined_field_message(schema, node.name.value, parent_type) 23 | )} 24 | else 25 | {:continue, accumulator} 26 | end 27 | end 28 | 29 | def enter(_visitor, _node, accumulator), do: {:continue, accumulator} 30 | 31 | def leave(_visitor, _node, accumulator), do: accumulator 32 | 33 | defp sibling_interfaces_including_field(schema, type, field_name) do 34 | AbstractType.possible_types(type, schema) 35 | |> Enum.filter(&is_a_graphql_object_type/1) 36 | |> Enum.flat_map(&to_self_and_interfaces/1) 37 | |> Enum.reduce(%{}, to_field_usage_counts(field_name)) 38 | |> Enum.filter(&by_at_least_one_usage/1) 39 | |> Enum.uniq() 40 | |> Enum.sort_by(&field_usage_count/1) 41 | |> Enum.map(fn({iface,_}) -> iface end) 42 | end 43 | 44 | defp field_usage_count({_, count}), do: count 45 | 46 | # TODO is this meant to be a ==? 47 | defp is_a_graphql_object_type(type), do: %ObjectType{} = type 48 | 49 | defp to_self_and_interfaces(type), do: [type] ++ type.interfaces 50 | 51 | defp to_field_usage_counts(field_name) do 52 | fn(iface, counts) -> 53 | incr = if CompositeType.has_field?(iface, field_name), do: 1, else: 0 54 | Map.merge(counts, %{ 55 | iface.name => Map.get(counts, iface.name, 0) + incr 56 | }) 57 | end 58 | end 59 | 60 | defp by_at_least_one_usage({_iface, count}), do: count > 0 61 | 62 | defp implementations_including_field(schema, type, field_name) do 63 | AbstractType.possible_types(type, schema) 64 | |> Enum.filter(&CompositeType.get_field(&1, field_name)) 65 | |> Enum.map(&(&1.name)) 66 | |> Enum.sort() 67 | end 68 | 69 | defp suggest_types(schema, field_name, type) do 70 | if Type.is_abstract?(type) do 71 | (sibling_interfaces_including_field(schema, type, field_name) 72 | ++ implementations_including_field(schema, type, field_name)) 73 | |> Enum.uniq() 74 | else 75 | [] 76 | end 77 | end 78 | 79 | defp undefined_field_message(schema, field_name, type) do 80 | suggested_types = suggest_types(schema, field_name, type) 81 | message = "Cannot query field \"#{field_name}\" on type \"#{type.name}\"." 82 | if length(suggested_types) > 0 do 83 | suggestions = 84 | suggested_types 85 | |> Enum.slice(0, @max_type_suggestions) 86 | |> Enum.map(&"\"#{&1}\"") 87 | |> Enum.join(", ") 88 | "#{message} However, this field exists on #{suggestions}. " <> 89 | "Perhaps you meant to use an inline fragment?" 90 | else 91 | message 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at josh@canoniq.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /docs/graphql_grammar_summary.md: -------------------------------------------------------------------------------- 1 | # B. Appendix: Grammar Summary 2 | 3 | SourceCharacter :: "Any Unicode code point" 4 | 5 | 6 | ## Ignored Tokens 7 | 8 | Ignored :: 9 | - WhiteSpace 10 | - LineTerminator 11 | - Comment 12 | - Comma 13 | 14 | WhiteSpace :: 15 | - "Horizontal Tab (U+0009)" 16 | - "Vertical Tab (U+000B)" 17 | - "Form Feed (U+000C)" 18 | - "Space (U+0020)" 19 | - "No-break Space (U+00A0)" 20 | 21 | LineTerminator :: 22 | - "New Line (U+000A)" 23 | - "Carriage Return (U+000D)" 24 | - "Line Separator (U+2028)" 25 | - "Paragraph Separator (U+2029)" 26 | 27 | Comment :: 28 | - `#` CommentChar* 29 | 30 | CommentChar :: SourceCharacter but not LineTerminator 31 | 32 | Comma :: , 33 | 34 | 35 | ## Lexical Tokens 36 | 37 | Token :: 38 | - Punctuator 39 | - Name 40 | - IntValue 41 | - FloatValue 42 | - StringValue 43 | 44 | Punctuator :: one of ! $ ( ) ... : = @ [ ] { | } 45 | 46 | Name :: /[_A-Za-z][_0-9A-Za-z]*/ 47 | 48 | IntValue :: IntegerPart 49 | 50 | IntegerPart :: 51 | - NegativeSign? 0 52 | - NegativeSign? NonZeroDigit Digit* 53 | 54 | NegativeSign :: - 55 | 56 | Digit :: one of 0 1 2 3 4 5 6 7 8 9 57 | 58 | NonZeroDigit :: Digit but not `0` 59 | 60 | FloatValue :: 61 | - IntegerPart FractionalPart 62 | - IntegerPart ExponentPart 63 | - IntegerPart FractionalPart ExponentPart 64 | 65 | FractionalPart :: . Digit+ 66 | 67 | ExponentPart :: ExponentIndicator Sign? Digit+ 68 | 69 | ExponentIndicator :: one of `e` `E` 70 | 71 | Sign :: one of + - 72 | 73 | StringValue :: 74 | - `""` 75 | - `"` StringCharacter+ `"` 76 | 77 | StringCharacter :: 78 | - SourceCharacter but not `"` or \ or LineTerminator 79 | - \ EscapedUnicode 80 | - \ EscapedCharacter 81 | 82 | EscapedUnicode :: u /[0-9A-Fa-f]{4}/ 83 | 84 | EscapedCharacter :: one of `"` \ `/` b f n r t 85 | 86 | 87 | ## Query Document 88 | 89 | Document : Definition+ 90 | 91 | Definition : 92 | - OperationDefinition 93 | - FragmentDefinition 94 | 95 | OperationDefinition : 96 | - SelectionSet 97 | - OperationType Name VariableDefinitions? Directives? SelectionSet 98 | 99 | OperationType : one of query mutation 100 | 101 | SelectionSet : { Selection+ } 102 | 103 | Selection : 104 | - Field 105 | - FragmentSpread 106 | - InlineFragment 107 | 108 | Field : Alias? Name Arguments? Directives? SelectionSet? 109 | 110 | Alias : Name : 111 | 112 | Arguments : ( Argument+ ) 113 | 114 | Argument : Name : Value 115 | 116 | FragmentSpread : ... FragmentName Directives? 117 | 118 | InlineFragment : ... on TypeCondition Directives? SelectionSet 119 | 120 | FragmentDefinition : fragment FragmentName on TypeCondition Directives? SelectionSet 121 | 122 | FragmentName : Name but not `on` 123 | 124 | TypeCondition : NamedType 125 | 126 | Value[Const] : 127 | - [~Const] Variable 128 | - IntValue 129 | - FloatValue 130 | - StringValue 131 | - BooleanValue 132 | - EnumValue 133 | - ListValue[?Const] 134 | - ObjectValue[?Const] 135 | 136 | BooleanValue : one of `true` `false` 137 | 138 | EnumValue : Name but not `true`, `false` or `null` 139 | 140 | ListValue[Const] : 141 | - [ ] 142 | - [ Value[?Const]+ ] 143 | 144 | ObjectValue[Const] : 145 | - { } 146 | - { ObjectField[?Const]+ } 147 | 148 | ObjectField[Const] : Name : Value[?Const] 149 | 150 | VariableDefinitions : ( VariableDefinition+ ) 151 | 152 | VariableDefinition : Variable : Type DefaultValue? 153 | 154 | Variable : $ Name 155 | 156 | DefaultValue : = Value[Const] 157 | 158 | Type : 159 | - NamedType 160 | - ListType 161 | - NonNullType 162 | 163 | NamedType : Name 164 | 165 | ListType : [ Type ] 166 | 167 | NonNullType : 168 | - NamedType ! 169 | - ListType ! 170 | 171 | Directives : Directive+ 172 | 173 | Directive : @ Name Arguments? -------------------------------------------------------------------------------- /lib/graphql/lang/ast/composite_visitor.ex: -------------------------------------------------------------------------------- 1 | 2 | alias GraphQL.Lang.AST.Visitor 3 | alias GraphQL.Lang.AST.InitialisingVisitor 4 | alias GraphQL.Lang.AST.PostprocessingVisitor 5 | 6 | defmodule GraphQL.Lang.AST.CompositeVisitor do 7 | @moduledoc """ 8 | A CompositeVisitor composes two Visitor implementations into a single Visitor. 9 | 10 | This provides the ability to chain an arbitrary number of visitors together. 11 | 12 | The *outer_visitor* notionally wraps the *inner_visitor*. The order of operations is thus: 13 | 14 | 1. outer_visitor.enter 15 | 2. inner_visitor.enter 16 | 3. inner_visitor.leave 17 | 4. outer_visitor.leave 18 | """ 19 | 20 | defstruct outer_visitor: nil, inner_visitor: nil 21 | 22 | @doc """ 23 | Composes two Visitors, returning a new one. 24 | """ 25 | def compose(outer_visitor, inner_visitor) do 26 | %GraphQL.Lang.AST.CompositeVisitor{outer_visitor: outer_visitor, inner_visitor: inner_visitor} 27 | end 28 | 29 | @doc """ 30 | Composes an arbitrarily long list of Visitors into a single Visitor. 31 | 32 | The order of the list is outer-to-inner. The leftmost visitor will be invoked first 33 | upon 'enter' and last upon 'leave'. 34 | """ 35 | def compose([visitor]), do: visitor 36 | def compose([outer_visitor|rest]), do: compose(outer_visitor, compose(rest)) 37 | end 38 | 39 | defimpl Visitor, for: GraphQL.Lang.AST.CompositeVisitor do 40 | 41 | @doc """ 42 | Invoke *enter* on the outer visitor first, passing the resulting accumulator to the *enter* 43 | call on the *inner* visitor. 44 | 45 | If either visitor's enter method returns :skip, both visitors will still be executed, but 46 | then execution will cease. 47 | """ 48 | def enter(composite_visitor, node, accumulator) do 49 | {v1_next_action, v1_accumulator} 50 | = Visitor.enter(composite_visitor.outer_visitor, node, accumulator) 51 | accumulator = Map.merge(accumulator, v1_accumulator) 52 | 53 | if v1_next_action == :skip do 54 | {:skip, accumulator} 55 | else 56 | Visitor.enter(composite_visitor.inner_visitor, node, accumulator) 57 | end 58 | end 59 | 60 | @doc """ 61 | Invoke *leave* on the inner visitor first, passing the resulting accumulator to the *leave* 62 | call on the *outer* visitor. 63 | 64 | If either visitor's enter method returns :skip, both visitors will still be executed, but 65 | then execution will cease. 66 | """ 67 | def leave(composite_visitor, node, accumulator) do 68 | v1_accumulator = Visitor.leave(composite_visitor.inner_visitor, node, accumulator) 69 | v2_accumulator = Visitor.leave(composite_visitor.outer_visitor, node, Map.merge(accumulator, v1_accumulator)) 70 | v2_accumulator 71 | end 72 | end 73 | 74 | defimpl InitialisingVisitor, for: GraphQL.Lang.AST.CompositeVisitor do 75 | @doc """ 76 | Invokes *start* on the outer visitor first, then calls *start* on the inner visitor 77 | passing the accumulator from the *outer*. 78 | 79 | Returns the accumulator of the *inner* visitor. 80 | """ 81 | def init(composite_visitor, accumulator) do 82 | accumulator = InitialisingVisitor.init(composite_visitor.outer_visitor, accumulator) 83 | InitialisingVisitor.init(composite_visitor.inner_visitor, accumulator) 84 | end 85 | end 86 | 87 | defimpl PostprocessingVisitor, for: GraphQL.Lang.AST.CompositeVisitor do 88 | @doc """ 89 | Invokes *finish* on the inner visitor first, then calls *finish* on the outer visitor 90 | passing the accumulator from the *inner*. 91 | 92 | Returns the accumulator of the *outer* visitor. 93 | """ 94 | def finish(composite_visitor, accumulator) do 95 | accumulator = PostprocessingVisitor.finish(composite_visitor.inner_visitor, accumulator) 96 | PostprocessingVisitor.finish(composite_visitor.outer_visitor, accumulator) 97 | end 98 | end 99 | 100 | -------------------------------------------------------------------------------- /test/support/star_wars/schema.exs: -------------------------------------------------------------------------------- 1 | defmodule StarWars.Schema do 2 | 3 | alias GraphQL.Type.ObjectType 4 | alias GraphQL.Type.List 5 | alias GraphQL.Type.Interface 6 | alias GraphQL.Type.String 7 | alias GraphQL.Type.NonNull 8 | 9 | alias StarWars.Schema.Episode 10 | alias StarWars.Schema.Character 11 | alias StarWars.Schema.Droid 12 | alias StarWars.Schema.Human 13 | 14 | # GraphQL: 15 | # 16 | # enum Episode { NEWHOPE, EMPIRE, JEDI } 17 | # 18 | defmodule Episode do 19 | def type do 20 | GraphQL.Type.Enum.new %{ 21 | name: "Episode", 22 | description: "One of the films in the Star Wars Trilogy", 23 | values: %{ 24 | NEWHOPE: %{value: 4, description: "Released in 1977"}, 25 | EMPIRE: %{value: 5, description: "Released in 1980"}, 26 | JEDI: %{value: 6, description: "Released in 1983"} 27 | } 28 | } 29 | end 30 | end 31 | 32 | defmodule Character do 33 | def type do 34 | Interface.new %{ 35 | name: "Character", 36 | description: "A character in the Star Wars Trilogy", 37 | fields: %{ 38 | id: %{type: %NonNull{ofType: %String{}}}, 39 | name: %{type: %String{}}, 40 | friends: %{type: %List{ofType: Character}}, 41 | appears_in: %{type: %List{ofType: Episode}} 42 | }, 43 | resolver: fn(x) -> 44 | if StarWars.Data.get_human(x.id), do: Human, else: Droid 45 | end 46 | } 47 | end 48 | end 49 | 50 | defmodule Human do 51 | def type do 52 | %ObjectType{ 53 | name: "Human", 54 | description: "A humanoid creature in the Star Wars universe", 55 | fields: %{ 56 | id: %{type: %NonNull{ofType: %String{}}}, 57 | name: %{type: %String{}}, 58 | friends: %{ 59 | type: %List{ofType: Character}, 60 | resolve: fn(item) -> StarWars.Data.get_friends(item) end 61 | }, 62 | appears_in: %{type: %List{ofType: Episode}}, 63 | home_planet: %{type: %String{}} 64 | }, 65 | interfaces: [Character] 66 | } 67 | end 68 | end 69 | 70 | defmodule Droid do 71 | def type do 72 | %ObjectType{ 73 | name: "Droid", 74 | description: "A mechanical creature in the Star Wars universe", 75 | fields: %{ 76 | id: %{type: %NonNull{ofType: %String{}}}, 77 | name: %{type: %String{}}, 78 | friends: %{ 79 | type: %List{ofType: Character}, 80 | resolve: fn(item) -> StarWars.Data.get_friends(item) end 81 | }, 82 | appears_in: %{type: %List{ofType: Episode}}, 83 | primary_function: %{type: %String{}} 84 | }, 85 | interfaces: [Character] 86 | } 87 | end 88 | end 89 | 90 | def query do 91 | %ObjectType{ 92 | name: "Query", 93 | fields: %{ 94 | hero: %{ 95 | type: Character, 96 | args: %{ 97 | # TODO this should be a type InputObject 98 | episode: %{ 99 | type: Episode, 100 | description: "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode" 101 | } 102 | }, 103 | resolve: fn(_, args) -> 104 | StarWars.Data.get_hero(Map.get(args, :episode)) 105 | end 106 | }, 107 | human: %{ 108 | type: Human, 109 | args: %{ 110 | id: %{type: %NonNull{ofType: %String{}}, description: "id of the human"} 111 | }, 112 | resolve: fn(_, args) -> StarWars.Data.get_human(args.id) end 113 | }, 114 | droid: %{ 115 | type: Droid, 116 | args: %{ 117 | id: %{type: %NonNull{ofType: %String{}}, description: "id of the droid"} 118 | }, 119 | resolve: fn(_, args) -> StarWars.Data.get_droid(args.id) end 120 | } 121 | } 122 | } 123 | end 124 | 125 | def schema do 126 | GraphQL.Schema.new(%{query: query}) 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/graphql/type/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Schema do 2 | 3 | @type t :: %GraphQL.Schema{ 4 | query: Map, 5 | mutation: Map, 6 | type_cache: Map, 7 | directives: [GraphQL.Type.Directive.t] 8 | } 9 | 10 | alias GraphQL.Type.Input 11 | alias GraphQL.Type.Interface 12 | alias GraphQL.Type.Union 13 | alias GraphQL.Type.ObjectType 14 | alias GraphQL.Type.Introspection 15 | alias GraphQL.Type.CompositeType 16 | alias GraphQL.Lang.AST.Nodes 17 | 18 | defstruct query: nil, 19 | mutation: nil, 20 | type_cache: nil, 21 | directives: [ 22 | GraphQL.Type.Directives.include, 23 | GraphQL.Type.Directives.skip 24 | ] 25 | 26 | def with_type_cache(schema = %{type_cache: nil}), do: new(schema) 27 | def with_type_cache(schema), do: schema 28 | 29 | def new(%{query: query, mutation: mutation}) do 30 | %GraphQL.Schema{query: query, mutation: mutation, type_cache: do_reduce_types(query, mutation)} 31 | end 32 | def new(%{mutation: mutation}), do: new(%{query: nil, mutation: mutation}) 33 | def new(%{query: query}), do: new(%{query: query, mutation: nil}) 34 | 35 | # FIXME: I think *schema* should be the first argument in this module. 36 | def type_from_ast(nil, _), do: nil 37 | def type_from_ast(%{kind: :NonNullType,} = input_type_ast, schema) do 38 | %GraphQL.Type.NonNull{ofType: type_from_ast(input_type_ast.type, schema)} 39 | end 40 | def type_from_ast(%{kind: :ListType,} = input_type_ast, schema) do 41 | %GraphQL.Type.List{ofType: type_from_ast(input_type_ast.type, schema)} 42 | end 43 | def type_from_ast(%{kind: :NamedType} = input_type_ast, schema) do 44 | schema.type_cache |> Map.get(input_type_ast.name.value, :not_found) 45 | end 46 | 47 | defp do_reduce_types(query, mutation) do 48 | %{} 49 | |> reduce_types(query) 50 | |> reduce_types(mutation) 51 | |> reduce_types(Introspection.Schema.type) 52 | end 53 | 54 | defp reduce_types(typemap, %{ofType: list_type}) do 55 | reduce_types(typemap, list_type) 56 | end 57 | 58 | defp reduce_types(typemap, %Interface{} = type) do 59 | Map.put(typemap, type.name, type) 60 | end 61 | 62 | defp reduce_types(typemap, %Union{} = type) do 63 | typemap = Map.put(typemap, type.name, type) 64 | Enum.reduce(type.types, typemap, fn(fieldtype,map) -> 65 | reduce_types(map, fieldtype) 66 | end) 67 | end 68 | 69 | defp reduce_types(typemap, %ObjectType{} = type) do 70 | if Map.has_key?(typemap, type.name) do 71 | typemap 72 | else 73 | typemap = Map.put(typemap, type.name, type) 74 | thunk_fields = CompositeType.get_fields(type) 75 | typemap = Enum.reduce(thunk_fields, typemap, fn({_,fieldtype},typemap) -> 76 | _reduce_arguments(typemap, fieldtype) 77 | |> reduce_types(fieldtype.type) 78 | end) 79 | typemap = Enum.reduce(type.interfaces, typemap, fn(fieldtype,map) -> 80 | reduce_types(map, fieldtype) 81 | end) 82 | end 83 | end 84 | 85 | defp reduce_types(typemap, %Input{} = type) do 86 | if Map.has_key?(typemap, type.name) do 87 | typemap 88 | else 89 | typemap = Map.put(typemap, type.name, type) 90 | thunk_fields = CompositeType.get_fields(type) 91 | typemap = Enum.reduce(thunk_fields, typemap, fn({_,fieldtype},typemap) -> 92 | _reduce_arguments(typemap, fieldtype) 93 | |> reduce_types(fieldtype.type) 94 | end) 95 | end 96 | end 97 | 98 | defp reduce_types(typemap, %{name: name} = type), do: Map.put(typemap, name, type) 99 | defp reduce_types(typemap, nil), do: typemap 100 | 101 | defp reduce_types(typemap, type_module) when is_atom(type_module) do 102 | reduce_types(typemap, apply(type_module, :type, [])) 103 | end 104 | 105 | @spec operation_root_type(GraphQL.Schema.t, Nodes.operation_node) :: atom 106 | def operation_root_type(schema, operation) do 107 | Map.get(schema, operation.operation) 108 | end 109 | 110 | defp _reduce_arguments(typemap, %{args: args}) do 111 | field_arg_types = Enum.map(args, fn{_,v} -> v.type end) 112 | Enum.reduce(field_arg_types, typemap, fn(fieldtype,typemap) -> 113 | reduce_types(typemap, fieldtype) 114 | end) 115 | end 116 | defp _reduce_arguments(typemap, _), do: typemap 117 | end 118 | -------------------------------------------------------------------------------- /test/graphql/type/introspection_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Type.IntrospectionTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExUnit.TestHelpers 5 | 6 | alias GraphQL.Schema 7 | alias GraphQL.Type.Input 8 | alias GraphQL.Type.Int 9 | alias GraphQL.Type.NonNull 10 | alias GraphQL.Type.ObjectType 11 | alias GraphQL.Type.String 12 | 13 | defmodule EmptySchema do 14 | def schema do 15 | Schema.new(%{ 16 | query: %ObjectType{ 17 | name: "QueryRoot", 18 | fields: %{ 19 | onlyField: %{type: %String{}} 20 | } 21 | } 22 | }) 23 | end 24 | end 25 | 26 | test "include input types in response to introspection query" do 27 | type = %ObjectType{ 28 | name: "Thing", 29 | description: "Things", 30 | fields: %{ 31 | id: %{type: %Int{}}, 32 | name: %{type: %String{}}, 33 | }, 34 | } 35 | 36 | thing_input_type = %Input{ 37 | name: "ThingInput", 38 | fields: %{ 39 | name: %{type: %String{}}, 40 | } 41 | } 42 | 43 | output_type = %ObjectType{ 44 | name: "SaveThingPayload", 45 | fields: %{ 46 | thing: %{ 47 | type: type, 48 | resolve: fn(payload, _, _) -> 49 | payload 50 | end 51 | }, 52 | }, 53 | } 54 | 55 | input_type = %Input{ 56 | name: "SaveThingInput", 57 | fields: %{ 58 | id: %{type: %NonNull{ofType: %Int{}}}, 59 | params: %{type: %NonNull{ofType: thing_input_type}}, 60 | } 61 | } 62 | 63 | schema = %Schema{ 64 | query: %ObjectType{ 65 | name: "QueryRoot", 66 | fields: %{onlyField: %{type: %String{}}} 67 | }, 68 | mutation: %ObjectType{ 69 | name: "Mutation", 70 | description: "Root object for performing data mutations", 71 | fields: %{ 72 | save_thing: %{ 73 | type: output_type, 74 | args: %{ 75 | input: %{ 76 | type: %NonNull{ofType: input_type} 77 | } 78 | }, 79 | resolve: fn(data, _, _) -> 80 | data 81 | end 82 | } 83 | } 84 | } 85 | } 86 | 87 | {:ok, result} = execute(schema, GraphQL.Type.Introspection.query) 88 | assert Enum.find(result.data["__schema"]["types"], fn(type) -> type["name"] == "ThingInput" end) 89 | end 90 | 91 | test "exposes descriptions on types and fields" do 92 | schema = Schema.new(%{ 93 | query: %ObjectType{ 94 | name: "QueryRoot", 95 | fields: %{onlyField: %{type: %String{}}} 96 | } 97 | }) 98 | 99 | query = """ 100 | { 101 | schemaType: __type(name: "__Schema") { 102 | name 103 | description 104 | fields { 105 | name, 106 | description 107 | } 108 | } 109 | } 110 | """ 111 | 112 | {:ok, result} = execute(schema, query) 113 | assert_data(result, %{ 114 | schemaType: %{ 115 | name: "__Schema", 116 | description: 117 | """ 118 | A GraphQL Schema defines the capabilities of a 119 | GraphQL server. It exposes all available types and 120 | directives on the server, as well as the entry 121 | points for query, mutation, 122 | and subscription operations. 123 | """ |> GraphQL.Util.Text.normalize, 124 | fields: [ 125 | %{ 126 | name: "directives", 127 | description: "A list of all directives supported by this server." 128 | }, 129 | %{ 130 | name: "mutationType", 131 | description: "If this server supports mutation, the type that mutation operations will be rooted at." 132 | }, 133 | %{ 134 | name: "queryType", 135 | description: "The type that query operations will be rooted at." 136 | }, 137 | %{ 138 | name: "subscriptionType", 139 | description: "If this server support subscription, the type that subscription operations will be rooted at.", 140 | }, 141 | %{ 142 | name: "types", 143 | description: "A list of all types supported by this server." 144 | } 145 | ] 146 | } 147 | }) 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/graphql/validation/rules/no_fragment_cycles.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Validation.Rules.NoFragmentCycles do 2 | 3 | alias GraphQL.Lang.AST.{Visitor, InitialisingVisitor, DocumentInfo} 4 | alias GraphQL.Util.Stack 5 | import GraphQL.Validation 6 | 7 | defstruct name: "NoFragmentCycles" 8 | 9 | defimpl InitialisingVisitor do 10 | def init(_visitor, acc) do 11 | Map.merge(acc, %{ 12 | visited_fragments: %{}, 13 | spread_path: %Stack{}, 14 | spread_path_indices: %{} 15 | }) 16 | end 17 | end 18 | 19 | defimpl Visitor do 20 | def enter(_visitor, %{kind: :FragmentDefinition} = node, acc) do 21 | if !visited?(acc, node) do 22 | {:continue, detect_cycles(acc, node)} 23 | else 24 | {:continue, acc} 25 | end 26 | end 27 | 28 | def enter(_visitor, _node, acc) do 29 | {:continue, acc} 30 | end 31 | 32 | def leave(_visitor, _node, acc), do: acc 33 | 34 | defp detect_cycles(acc, nil), do: acc 35 | defp detect_cycles(acc, fragment_def) do 36 | acc 37 | |> mark_visited(fragment_def) 38 | |> detect_cycles_via_spread_nodes(fragment_def, spread_nodes_of_fragment(fragment_def)) 39 | end 40 | 41 | defp detect_cycles_via_spread_nodes(acc, _, []), do: acc 42 | defp detect_cycles_via_spread_nodes(acc, fragment_def, spread_nodes) do 43 | frag_name = fragment_def.name.value 44 | acc = %{acc | spread_path_indices: 45 | Map.merge(acc[:spread_path_indices], %{frag_name => Stack.length(acc[:spread_path])})} 46 | 47 | acc = process_spread_nodes(acc, spread_nodes) 48 | 49 | %{acc | spread_path_indices: Map.delete(acc[:spread_path_indices], frag_name)} 50 | end 51 | 52 | defp process_spread_nodes(acc, []), do: acc 53 | defp process_spread_nodes(acc, [spread_node|rest]) do 54 | spread_name = spread_node.name.value 55 | cycle_index = Map.get(acc[:spread_path_indices], spread_name) 56 | process_spread_nodes(process_one_node(acc, spread_node, cycle_index), rest) 57 | end 58 | 59 | defp process_one_node(acc, spread_node, cycle_index) when is_integer(cycle_index) do 60 | cycle_path = Enum.slice( 61 | Enum.reverse(acc[:spread_path].elements), 62 | cycle_index, 63 | Stack.length(acc[:spread_path]) 64 | ) 65 | report_error(acc, cycle_error_message(spread_node.name.value, Enum.map(cycle_path, fn(s) -> s.name.value end))) 66 | end 67 | 68 | defp process_one_node(acc, spread_node, _) do 69 | acc = %{acc | spread_path: Stack.push(acc[:spread_path], spread_node)} 70 | acc = 71 | if !visited?(acc, spread_node) do 72 | spread_fragment = DocumentInfo.get_fragment_definition(acc[:document_info], spread_node.name.value) 73 | detect_cycles(acc, spread_fragment) 74 | else 75 | acc 76 | end 77 | %{acc | spread_path: Stack.pop(acc[:spread_path])} 78 | end 79 | 80 | defp visited?(acc, node) do 81 | Map.has_key?(acc[:visited_fragments], node.name.value) 82 | end 83 | 84 | defp mark_visited(acc, node) do 85 | %{acc | visited_fragments: 86 | Map.merge(acc[:visited_fragments], %{node.name.value => true})} 87 | end 88 | 89 | defp cycle_error_message(frag_name, spread_names) do 90 | via = case length(spread_names) do 91 | 0 -> "" 92 | _ -> " via #{Enum.join(spread_names, ", ")}" 93 | end 94 | "Cannot spread fragment #{frag_name} within itself#{via}." 95 | end 96 | 97 | defp spread_nodes_of_fragment(fragment_def) do 98 | spread_nodes_of_selection_sets([fragment_def.selectionSet]) 99 | end 100 | 101 | defp spread_nodes_of_selection_sets([]), do: [] 102 | defp spread_nodes_of_selection_sets([selection_set|rest]) do 103 | spread_nodes_of_selections(selection_set.selections) ++ spread_nodes_of_selection_sets(rest) 104 | end 105 | 106 | defp spread_nodes_of_selections([]), do: [] 107 | defp spread_nodes_of_selections([%{kind: :FragmentSpread} = selection|rest]) do 108 | [selection] ++ spread_nodes_of_selections(rest) 109 | end 110 | defp spread_nodes_of_selections([%{selectionSet: selection_set}|rest]) do 111 | spread_nodes_of_selection_sets([selection_set]) ++ spread_nodes_of_selections(rest) 112 | end 113 | defp spread_nodes_of_selections([_|rest]) do 114 | spread_nodes_of_selections(rest) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/graphql.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL do 2 | @moduledoc ~S""" 3 | An Elixir implementation of Facebook's GraphQL. 4 | 5 | This is the core GraphQL query parsing and execution engine whose goal is to be 6 | transport, server and datastore agnostic. 7 | 8 | In order to setup an HTTP server (ie Phoenix) to handle GraphQL queries you will 9 | need: 10 | 11 | * [GraphQL Plug](https://github.com/graphql-elixir/plug_graphql) 12 | 13 | Examples for Phoenix can be found: 14 | 15 | * [Phoenix Examples](https://github.com/graphql-elixir/hello_graphql_phoenix) 16 | 17 | Here you'll find some examples which can be used as a starting point for writing your own schemas. 18 | 19 | Other ways of handling queries will be added in due course. 20 | 21 | ## Execute a Query on the Schema 22 | 23 | First setup your schema 24 | 25 | iex> defmodule TestSchema do 26 | ...> def schema do 27 | ...> %GraphQL.Schema{ 28 | ...> query: %GraphQL.Type.ObjectType{ 29 | ...> name: "RootQueryType", 30 | ...> fields: %{ 31 | ...> greeting: %{ 32 | ...> type: %GraphQL.Type.String{}, 33 | ...> resolve: &TestSchema.greeting/3, 34 | ...> description: "Greeting", 35 | ...> args: %{ 36 | ...> name: %{type: %GraphQL.Type.String{}, description: "The name of who you'd like to greet."}, 37 | ...> } 38 | ...> } 39 | ...> } 40 | ...> } 41 | ...> } 42 | ...> end 43 | ...> def greeting(_, %{name: name}, _), do: "Hello, #{name}!" 44 | ...> def greeting(_, _, _), do: "Hello, world!" 45 | ...> end 46 | ...> 47 | ...> GraphQL.execute(TestSchema.schema, "{ greeting }") 48 | {:ok, %{data: %{"greeting" => "Hello, world!"}}} 49 | ...> 50 | ...> GraphQL.execute(TestSchema.schema, ~S[{ greeting(name: "Josh") }]) 51 | {:ok, %{data: %{"greeting" => "Hello, Josh!"}}} 52 | """ 53 | 54 | alias GraphQL.Validation.Validator 55 | alias GraphQL.Execution.Executor 56 | 57 | @doc """ 58 | Execute a query against a schema (with validation) 59 | 60 | # iex> GraphQL.execute_with_opts(schema, "{ hello }") 61 | # {:ok, %{hello: world}} 62 | 63 | This is the preferred function signature for `execute` and 64 | will replace `execute/5`. 65 | """ 66 | def execute_with_opts(schema, query, opts \\ []) do 67 | execute_with_optional_validation(schema, query, opts) 68 | end 69 | 70 | @doc """ 71 | Execute a query against a schema (with validation) 72 | 73 | # iex> GraphQL.execute(schema, "{ hello }") 74 | # {:ok, %{hello: world}} 75 | 76 | *Deprecation warning*: This will be replaced in a future version with the 77 | function signature for `execute_with_opts/3`. 78 | """ 79 | def execute(schema, query, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do 80 | execute_with_optional_validation( 81 | schema, 82 | query, 83 | root_value: root_value, 84 | variable_values: variable_values, 85 | operation_name: operation_name, 86 | validate: true 87 | ) 88 | end 89 | 90 | @doc """ 91 | Execute a query against a schema (without validation) 92 | 93 | # iex> GraphQL.execute(schema, "{ hello }") 94 | # {:ok, %{hello: world}} 95 | """ 96 | def execute_without_validation(schema, query, opts) do 97 | execute_with_optional_validation(schema, query, Keyword.put(opts, :validate, false)) 98 | end 99 | 100 | defp execute_with_optional_validation(schema, query, opts) do 101 | case GraphQL.Lang.Parser.parse(query) do 102 | {:ok, document} -> 103 | case optionally_validate(Keyword.get(opts, :validate, true), schema, document) do 104 | :ok -> 105 | case Executor.execute(schema, document, opts) do 106 | {:ok, data, []} -> {:ok, %{data: data}} 107 | {:ok, data, errors} -> {:ok, %{data: data, errors: errors}} 108 | {:error, errors} -> {:error, errors} 109 | end 110 | {:error, errors} -> 111 | {:error, errors} 112 | end 113 | {:error, errors} -> 114 | {:error, errors} 115 | end 116 | end 117 | 118 | defp optionally_validate(false, _schema, _document), do: :ok 119 | defp optionally_validate(true, schema, document), do: Validator.validate(schema, document) 120 | end 121 | -------------------------------------------------------------------------------- /test/graphql/execution/executor_blog_schema_test.exs: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Execution.Executor.ExecutorBlogSchemaTest do 3 | use ExUnit.Case, async: true 4 | 5 | import ExUnit.TestHelpers 6 | 7 | alias GraphQL.Schema 8 | alias GraphQL.Type.ObjectType 9 | alias GraphQL.Type.List 10 | alias GraphQL.Type.ID 11 | alias GraphQL.Type.String 12 | alias GraphQL.Type.Int 13 | alias GraphQL.Type.Boolean 14 | 15 | def make_article(id) do 16 | %{ 17 | id: "#{id}", 18 | isPublished: true, 19 | author: %{ 20 | id: "123", 21 | name: "John Smith", 22 | pic: fn(w, h) -> %{url: "cdn://123", width: w, height: h} end, 23 | recentArticle: %{ 24 | id: "1000", 25 | isPublished: true, 26 | title: "GraphQL and Elixir: A powerful pair", 27 | body: "Elixir is fast, GraphQL is awesome!", 28 | keywords: ["elixir", "graphql"] 29 | } 30 | }, 31 | title: "My Article #{id}", 32 | body: "This is a post", 33 | hidden: "This data is not exposed in the schema", 34 | keywords: ["tech", "elixir", "graphql", 1, true, nil] 35 | } 36 | end 37 | 38 | test "Handle execution with a complex schema" do 39 | image = %ObjectType{ 40 | name: "Image", 41 | description: "Images for an article or a profile picture", 42 | fields: %{ 43 | url: %{type: %String{}}, 44 | width: %{type: %Int{}}, 45 | height: %{type: %Int{}} 46 | } 47 | } 48 | 49 | author = %ObjectType{ 50 | name: "Author", 51 | description: "Author of the blog, with their profile picture and latest article", 52 | fields: %{ 53 | id: %{type: %ID{}}, 54 | name: %{type: %String{}}, 55 | pic: %{ 56 | args: %{ 57 | width: %{type: %Int{}}, 58 | height: %{type: %Int{}} 59 | }, 60 | type: image, 61 | resolve: fn(o, %{width: w, height: h}) -> o.pic.(w, h) end 62 | }, 63 | recentArticle: nil 64 | } 65 | } 66 | 67 | article = %ObjectType{ 68 | name: "Article", 69 | fields: %{ 70 | id: %{type: %ID{}}, 71 | isPublished: %{type: %Boolean{}}, 72 | author: %{type: author}, 73 | title: %{type: %String{}}, 74 | body: %{type: %String{}}, 75 | keywords: %{type: %List{ofType: %String{}}} 76 | } 77 | } 78 | 79 | # resolve circular dependency 80 | author = put_in author.fields.recentArticle, %{type: article} 81 | article = put_in article.fields.author, %{type: author} 82 | 83 | blog_query = %ObjectType{ 84 | name: "Query", 85 | fields: %{ 86 | article: %{ 87 | type: article, 88 | args: %{id: %{type: %ID{}}}, 89 | resolve: fn(_, %{id: id}) -> make_article(id) end 90 | }, 91 | feed: %{ 92 | type: %List{ofType: article}, 93 | resolve: fn() -> for id <- 1..2, do: make_article(id) end 94 | } 95 | } 96 | } 97 | 98 | blog_schema = Schema.new(%{query: blog_query}) 99 | 100 | query = """ 101 | { 102 | feed { 103 | id, 104 | title 105 | }, 106 | article(id: "1") { 107 | ...articleFields, 108 | author { 109 | id, 110 | name, 111 | pic(width: 640, height: 480) { 112 | url, 113 | width, 114 | height 115 | }, 116 | recentArticle { 117 | ...articleFields, 118 | keywords 119 | } 120 | } 121 | } 122 | } 123 | 124 | fragment articleFields on Article { 125 | id, 126 | isPublished, 127 | title, 128 | body 129 | } 130 | """ 131 | 132 | {:ok, result} = execute(blog_schema, query) 133 | 134 | assert_data(result, %{ 135 | feed: [ 136 | %{id: "1", title: "My Article 1"}, 137 | %{id: "2", title: "My Article 2"} 138 | ], 139 | article: %{ 140 | id: "1", 141 | isPublished: true, 142 | title: "My Article 1", 143 | body: "This is a post", 144 | author: %{ 145 | id: "123", 146 | name: "John Smith", 147 | pic: %{ 148 | url: "cdn://123", 149 | width: 640, 150 | height: 480 151 | }, 152 | recentArticle: %{ 153 | id: "1000", 154 | isPublished: true, 155 | title: "GraphQL and Elixir: A powerful pair", 156 | body: "Elixir is fast, GraphQL is awesome!", 157 | keywords: ["elixir", "graphql"] 158 | } 159 | } 160 | } 161 | }) 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /test/graphql/type/enum_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Lang.Type.EnumTest do 2 | use ExUnit.Case, async: true 3 | import ExUnit.TestHelpers 4 | 5 | alias GraphQL.Type.ObjectType 6 | alias GraphQL.Type.Int 7 | alias GraphQL.Type.String 8 | 9 | defmodule TestSchema do 10 | def color_type do 11 | %{ 12 | name: "Color", 13 | values: %{ 14 | "RED": %{value: 0}, 15 | "GREEN": %{value: 1}, 16 | "BLUE": %{value: 2} 17 | } 18 | } |> GraphQL.Type.Enum.new 19 | end 20 | 21 | def query do 22 | %ObjectType{ 23 | name: "Query", 24 | fields: %{ 25 | color_enum: %{ 26 | type: color_type, 27 | args: %{ 28 | from_enum: %{type: color_type}, 29 | from_int: %{type: %Int{}}, 30 | from_string: %{type: %String{}}, 31 | }, 32 | resolve: fn(_, args) -> 33 | Map.get(args, :from_enum) || 34 | Map.get(args, :from_int) || 35 | Map.get(args, :from_string) 36 | end 37 | }, 38 | color_int: %{ 39 | type: %Int{}, 40 | args: %{ 41 | from_enum: %{type: color_type}, 42 | from_int: %{type: %Int{}} 43 | }, 44 | resolve: fn(_, args) -> 45 | Map.get(args, :from_enum) || 46 | Map.get(args, :from_int) 47 | end 48 | } 49 | } 50 | } 51 | end 52 | 53 | def schema, do: GraphQL.Schema.new(%{query: query}) 54 | end 55 | 56 | test "enum values are able to be parsed" do 57 | assert 1 == GraphQL.Types.parse_value(TestSchema.color_type, "GREEN") 58 | end 59 | 60 | test "enum values are able to be serialized" do 61 | assert "GREEN" == GraphQL.Types.serialize(TestSchema.color_type, 1) 62 | end 63 | 64 | test "accepts enum literals as input" do 65 | {:ok, result} = execute(TestSchema.schema, "{ color_int(from_enum: GREEN) }") 66 | assert_data(result, %{color_int: 1}) 67 | end 68 | 69 | test "enum may be output type" do 70 | {:ok, result} = execute(TestSchema.schema, "{ color_enum(from_int: 1) }") 71 | assert_data(result, %{color_enum: "GREEN"}) 72 | end 73 | 74 | test "enum may be both input and output type" do 75 | {:ok, result} = execute(TestSchema.schema, "{ color_enum(from_enum: GREEN) }") 76 | assert_data(result, %{color_enum: "GREEN"}) 77 | end 78 | 79 | @tag :skip # needs type validation 80 | test "does not accept string literals" do 81 | {:ok, result} = execute(TestSchema.schema, ~S[{ color_enum(from_enum: "GREEN") }]) 82 | assert_has_error(result, %{message: "replace with actual message"}) 83 | end 84 | 85 | test "does not accept incorrect internal value" do 86 | {:ok, result} = execute(TestSchema.schema, ~S[{ color_enum(from_string: "GREEN") }]) 87 | assert_data(result, %{color_enum: nil}) 88 | end 89 | 90 | @tag :skip # needs type validation 91 | test "does not accept internal value in place of enum literal" do 92 | {:ok, result} = execute(TestSchema.schema, ~S[{ color_enum(from_enum: 1) }]) 93 | assert_has_error(result, %{message: "replace with actual message"}) 94 | end 95 | 96 | @tag :skip # needs type validation 97 | test "does not accept enum literal in place of int" do 98 | {:ok, result} = execute(TestSchema.schema, ~S[{ color_enum(from_int: GREEN) }]) 99 | assert_has_error(result, %{message: "replace with actual message"}) 100 | end 101 | 102 | test "accepts JSON string as enum variable" do 103 | query = "query test($color: Color!) { color_enum(from_enum: $color) }" 104 | {:ok, result} = execute(TestSchema.schema, query, variable_values: %{"color" => "BLUE"}) 105 | assert_data(result, %{"color_enum" => "BLUE"}) 106 | end 107 | 108 | @tag :skip 109 | test "accepts enum literals as input arguments to mutations", do: :skipped 110 | @tag :skip 111 | test "accepts enum literals as input arguments to subscriptions", do: :skipped 112 | @tag :skip 113 | test "does not accept internal value as enum variable", do: :skipped 114 | @tag :skip 115 | test "does not accept string variables as enum input", do: :skipped 116 | @tag :skip 117 | test "does not accept internal value variable as enum input", do: :skipped 118 | 119 | test "enum value may have an internal value of 0" do 120 | {:ok, result} = execute(TestSchema.schema, "{ color_enum(from_enum: RED), color_int(from_enum: RED) }") 121 | assert_data(result, %{color_enum: "RED", color_int: 0}) 122 | end 123 | 124 | test "enum inputs may be nullable" do 125 | {:ok, result} = execute(TestSchema.schema, "{color_enum, color_int}") 126 | assert_data(result, %{color_enum: nil, color_int: nil}) 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/graphql/schema/generator.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Schema.Generator do 2 | 3 | # TODO generate comments with the source types 4 | 5 | def generate(base_filename, source_schema) do 6 | {:ok, ast} = GraphQL.Lang.Parser.parse(source_schema) 7 | module_name = base_filename |> Path.basename("_schema") |> Macro.camelize 8 | {:ok, generate_module(module_name, ast)} 9 | end 10 | 11 | def generate_module(name, ast) do 12 | """ 13 | defmodule #{name}.Schema do 14 | #{walk_ast(ast)} 15 | end 16 | """ 17 | end 18 | 19 | def walk_ast(doc = %{kind: :Document}) do 20 | """ 21 | alias GraphQL.Type 22 | 23 | #{doc.definitions |> Enum.map(&walk_ast/1) |> Enum.join("\n")} 24 | 25 | def schema do 26 | %GraphQL.Schema{ 27 | query: Query.type, 28 | # mutation: Mutation.type 29 | } 30 | end 31 | """ 32 | end 33 | 34 | def walk_ast(type_def = %{kind: :ObjectTypeDefinition}) do 35 | """ 36 | defmodule #{type_def.name.value} do 37 | def type do 38 | %Type.ObjectType{ 39 | name: "#{type_def.name.value}", 40 | description: "#{type_def.name.value} description", 41 | fields: %{ 42 | #{type_def.fields |> Enum.map(&walk_ast/1) |> Enum.join(",\n ")} 43 | }#{interfaces(type_def)} 44 | } 45 | end 46 | end 47 | """ 48 | end 49 | 50 | def walk_ast(field = %{kind: :FieldDefinition, arguments: args}) when is_list(args) and length(args) > 0 do 51 | """ 52 | #{field.name.value}: %{ 53 | type: #{walk_ast(field.type)}, 54 | args: %{ 55 | #{args |> Enum.map(&walk_ast/1) |> Enum.join(",\n")} 56 | } 57 | } 58 | """ |> String.strip 59 | end 60 | 61 | def walk_ast(input = %{kind: :InputValueDefinition}) do 62 | "#{input.name.value}: %{type: #{walk_ast(input.type)}}" 63 | end 64 | 65 | def walk_ast(field = %{kind: :FieldDefinition}) do 66 | "#{field.name.value}: %{type: #{walk_ast(field.type)}}" 67 | end 68 | 69 | def walk_ast(type = %{kind: :NonNullType}) do 70 | "%Type.NonNull{ofType: #{walk_ast(type.type)}}" 71 | end 72 | 73 | def walk_ast(type = %{kind: :ListType}) do 74 | "%Type.List{ofType: #{walk_ast(type.type)}}" 75 | end 76 | 77 | def walk_ast(type = %{kind: :NamedType}) do 78 | if type.name.value in ~w(String Int ID) do 79 | "%Type.#{type.name.value}{}" 80 | else 81 | type.name.value 82 | end 83 | end 84 | 85 | def walk_ast(type_def = %{kind: :EnumTypeDefinition}) do 86 | """ 87 | defmodule #{type_def.name.value} do 88 | def type do 89 | Type.Enum.new %{ 90 | name: "#{type_def.name.value}", 91 | description: "#{type_def.name.value} description", 92 | values: %{ 93 | #{type_def.values |> Enum.map(fn (v) -> "#{v}: %{value: 0}" end) |> Enum.join(",\n ")} 94 | } 95 | } 96 | end 97 | end 98 | """ 99 | end 100 | 101 | def walk_ast(type_def = %{kind: :InterfaceTypeDefinition}) do 102 | """ 103 | defmodule #{type_def.name.value} do 104 | def type do 105 | Type.Interface.new %{ 106 | name: "#{type_def.name.value}", 107 | description: "#{type_def.name.value} description", 108 | fields: %{ 109 | #{type_def.fields |> Enum.map(&walk_ast/1) |> Enum.join(",\n ")} 110 | } 111 | } 112 | end 113 | end 114 | """ 115 | end 116 | 117 | def walk_ast(type_def = %{kind: :UnionTypeDefinition}) do 118 | """ 119 | defmodule #{type_def.name.value} do 120 | def type do 121 | GraphQL.Type.Union.new %{ 122 | name: "#{type_def.name.value}", 123 | description: "#{type_def.name.value} description", 124 | types: [#{type_def.types |> Enum.map(&walk_ast/1) |> Enum.join(", ")}] 125 | } 126 | end 127 | end 128 | """ 129 | end 130 | 131 | def walk_ast(type_def = %{kind: :ScalarTypeDefinition}) do 132 | """ 133 | defmodule #{type_def.name.value} do 134 | def type do 135 | GraphQL.Type.Union.new %{ 136 | name: "#{type_def.name.value}", 137 | description: "#{type_def.name.value} description", 138 | types: [#{type_def.types |> Enum.map(&walk_ast/1) |> Enum.join(", ")}] 139 | } 140 | end 141 | end 142 | """ 143 | end 144 | 145 | def walk_ast(n) do 146 | "#{n.kind} not handled!!!" 147 | end 148 | 149 | def interfaces(type_def) do 150 | if i = Map.get(type_def, :interfaces) do 151 | ", 152 | interfaces: [#{Enum.map_join(i, ", ", &(&1.name.value))}]" 153 | else 154 | "" 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /test/graphql/lang/lexer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Lang.Lexer.LexerTest do 2 | use ExUnit.Case, async: true 3 | 4 | def assert_tokens(input, tokens) do 5 | case :graphql_lexer.string(input) do 6 | {:ok, output, _} -> 7 | assert output == tokens 8 | {:error, {_, :graphql_lexer, output}, _} -> 9 | assert output == tokens 10 | end 11 | end 12 | 13 | # Ignored tokens 14 | test "WhiteSpace is ignored" do 15 | assert_tokens '\u0009', [] # horizontal tab 16 | assert_tokens '\u000B', [] # vertical tab 17 | assert_tokens '\u000C', [] # form feed 18 | assert_tokens '\u0020', [] # space 19 | assert_tokens '\u00A0', [] # non-breaking space 20 | end 21 | 22 | test "LineTerminator is ignored" do 23 | assert_tokens '\u000A', [] # new line 24 | assert_tokens '\u000D', [] # carriage return 25 | assert_tokens '\u2028', [] # line separator 26 | assert_tokens '\u2029', [] # paragraph separator 27 | end 28 | 29 | test "Comment is ignored" do 30 | assert_tokens '# some comment', [] 31 | end 32 | 33 | test "Comma is ignored" do 34 | assert_tokens ',', [] 35 | end 36 | 37 | # Lexical tokens 38 | test "Punctuator" do 39 | assert_tokens '!', [{ :"!", 1 }] 40 | assert_tokens '$', [{ :"$", 1 }] 41 | assert_tokens '(', [{ :"(", 1 }] 42 | assert_tokens ')', [{ :")", 1 }] 43 | assert_tokens ':', [{ :":", 1 }] 44 | assert_tokens '=', [{ :"=", 1 }] 45 | assert_tokens ':', [{ :":", 1 }] 46 | assert_tokens '@', [{ :"@", 1 }] 47 | assert_tokens '[', [{ :"[", 1 }] 48 | assert_tokens ']', [{ :"]", 1 }] 49 | assert_tokens '{', [{ :"{", 1 }] 50 | assert_tokens '}', [{ :"}", 1 }] 51 | assert_tokens '|', [{ :"|", 1 }] 52 | assert_tokens '...', [{ :"...", 1 }] 53 | end 54 | 55 | test "Name" do 56 | assert_tokens '_', [{ :name, 1, '_' }] 57 | assert_tokens 'a', [{ :name, 1, 'a' }] 58 | assert_tokens 'Z', [{ :name, 1, 'Z' }] 59 | assert_tokens 'foo', [{ :name, 1, 'foo' }] 60 | assert_tokens 'Foo', [{ :name, 1, 'Foo' }] 61 | assert_tokens '_foo', [{ :name, 1, '_foo' }] 62 | assert_tokens 'foo0', [{ :name, 1, 'foo0' }] 63 | assert_tokens '_fu_Ba_QX_2', [{ :name, 1, '_fu_Ba_QX_2' }] 64 | end 65 | 66 | test "Literals" do 67 | assert_tokens 'query', [{ :"query", 1 }] 68 | assert_tokens 'mutation', [{ :"mutation", 1 }] 69 | assert_tokens 'fragment', [{ :"fragment", 1 }] 70 | assert_tokens 'on', [{ :"on", 1 }] 71 | assert_tokens 'type', [{ :"type", 1 }] 72 | end 73 | 74 | test "IntValue" do 75 | assert_tokens '0', [{ :int_value, 1, '0' }] 76 | assert_tokens '-0', [{ :int_value, 1, '-0' }] 77 | assert_tokens '-1', [{ :int_value, 1, '-1' }] 78 | assert_tokens '2340', [{ :int_value, 1, '2340' }] 79 | assert_tokens '56789', [{ :int_value, 1, '56789' }] 80 | end 81 | 82 | test "FloatValue" do 83 | assert_tokens '0.0', [{ :float_value, 1, '0.0' }] 84 | assert_tokens '-0.1', [{ :float_value, 1, '-0.1' }] 85 | assert_tokens '0.1', [{ :float_value, 1, '0.1' }] 86 | assert_tokens '2.340', [{ :float_value, 1, '2.340' }] 87 | assert_tokens '5678.9', [{ :float_value, 1, '5678.9' }] 88 | assert_tokens '1.23e+45', [{ :float_value, 1, '1.23e+45' }] 89 | assert_tokens '1.23E-45', [{ :float_value, 1, '1.23E-45' }] 90 | assert_tokens '0.23E-45', [{ :float_value, 1, '0.23E-45' }] 91 | end 92 | 93 | test "StringValue" do 94 | assert_tokens '""', [{ :string_value, 1, '""' }] 95 | assert_tokens '"a"', [{ :string_value, 1, '"a"' }] 96 | assert_tokens '"\u000f"', [{ :string_value, 1, '"\u000f"' }] 97 | assert_tokens '"\t"', [{ :string_value, 1, '"\t"' }] 98 | assert_tokens '"\\""', [{ :string_value, 1, '"\\""' }] 99 | assert_tokens '"a\\n"', [{ :string_value, 1, '"a\\n"' }] 100 | end 101 | 102 | test "BooleanValue" do 103 | assert_tokens 'true', [{ :boolean_value, 1, 'true' }] 104 | assert_tokens 'false', [{ :boolean_value, 1, 'false' }] 105 | end 106 | 107 | test "EnumValue" do 108 | assert_tokens 'null', [{ :null, 1 }] 109 | assert_tokens 'ENUM_VALUE', [{ :name, 1, 'ENUM_VALUE' }] 110 | assert_tokens 'enum_value', [{ :name, 1, 'enum_value' }] 111 | end 112 | 113 | # Sample GraphQL 114 | test "Simple statement" do 115 | assert_tokens '{ hero }', [ 116 | { :"{", 1 }, 117 | { :name, 1, 'hero' }, 118 | { :"}", 1 } 119 | ] 120 | end 121 | 122 | test "Named query with nested selection set" do 123 | assert_tokens 'query myName { me { name } }', [ 124 | {:'query', 1}, 125 | {:name, 1, 'myName'}, 126 | {:'{', 1}, 127 | {:name, 1, 'me'}, 128 | {:'{', 1}, 129 | {:name, 1, 'name'}, 130 | {:'}', 1}, 131 | {:'}', 1} 132 | ] 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Elixir 2 | 3 | [![Build Status](https://travis-ci.org/graphql-elixir/graphql.svg)](https://travis-ci.org/graphql-elixir/graphql) 4 | [![Public Slack Discussion](https://graphql-slack.herokuapp.com/badge.svg)](https://graphql-slack.herokuapp.com/) 5 | 6 | An Elixir implementation of Facebook's GraphQL. 7 | 8 | This is the core GraphQL query parsing and execution engine whose goal is to be 9 | transport, server and datastore agnostic. 10 | 11 | In order to setup an HTTP server (ie Phoenix) to handle GraphQL queries you will 12 | need [plug_graphql](https://github.com/graphql-elixir/plug_graphql). 13 | Examples for Phoenix can be found at [hello_graphql_phoenix](https://github.com/graphql-elixir/hello_graphql_phoenix), so look here for a starting point for writing your own schemas. 14 | 15 | Other ways of handling queries will be added in due course. 16 | 17 | ## Installation 18 | 19 | First, add GraphQL to your `mix.exs` dependencies: 20 | 21 | ```elixir 22 | defp deps do 23 | [{:graphql, "~> 0.3"}] 24 | end 25 | ``` 26 | 27 | Add GraphQL to your `mix.exs` applications: 28 | 29 | ```elixir 30 | def application do 31 | # Add the application to your list of applications. 32 | # This will ensure that it will be included in a release. 33 | [applications: [:logger, :graphql]] 34 | end 35 | ``` 36 | 37 | Then, update your dependencies: 38 | 39 | ```sh-session 40 | $ mix deps.get 41 | ``` 42 | 43 | ## Usage 44 | 45 | First setup your schema 46 | 47 | ```elixir 48 | defmodule TestSchema do 49 | def schema do 50 | %GraphQL.Schema{ 51 | query: %GraphQL.Type.ObjectType{ 52 | name: "RootQueryType", 53 | fields: %{ 54 | greeting: %{ 55 | type: %GraphQL.Type.String{}, 56 | resolve: &TestSchema.greeting/3, 57 | description: "Greeting", 58 | args: %{ 59 | name: %{type: %GraphQL.Type.String{}, description: "The name of who you'd like to greet."}, 60 | } 61 | } 62 | } 63 | } 64 | } 65 | end 66 | 67 | def greeting(_, %{name: name}, _), do: "Hello, #{name}!" 68 | def greeting(_, _, _), do: "Hello, world!" 69 | end 70 | ``` 71 | 72 | Execute a simple GraphQL query 73 | 74 | ```elixir 75 | iex> GraphQL.execute(TestSchema.schema, "{greeting}") 76 | {:ok, %{data: %{"greeting" => "Hello, world!"}}} 77 | ``` 78 | 79 | ## Status 80 | 81 | This is a work in progress, right now here's what is done: 82 | 83 | - [x] Parser for GraphQL (including Type definitions) 84 | - [x] AST matching the `graphql-js` types as closely as possible 85 | - [x] Schema definition 86 | - [x] Query execution 87 | - [x] Scalar types 88 | - [x] Arguments 89 | - [x] Multiple forms of resolution 90 | - [x] Complex types (List, Object, etc) 91 | - [x] Fragments in queries 92 | - [x] Extract variable values 93 | - [x] Introspection 94 | - [WIP] Query validation 95 | - [ ] Directives 96 | 97 | ## Resources 98 | 99 | - [GraphQL Spec](http://facebook.github.io/graphql/) This incredibly well written spec made writing the GraphQL parser pretty straightforward. 100 | - [GraphQL JS Reference Implementation](https://github.com/graphql/graphql-js) 101 | 102 | ## Implementation 103 | 104 | Tokenisation is done with [leex](http://erlang.org/doc/man/leex.html) and parsing with [yecc](http://erlang.org/doc/man/yecc.html). Both very useful Erlang tools for parsing. Yecc in particular is used by Elixir itself. 105 | 106 | Some resources on using leex and yecc: 107 | 108 | * http://relops.com/blog/2014/01/13/leex_and_yecc/ 109 | * http://andrealeopardi.com/posts/tokenizing-and-parsing-in-elixir-using-leex-and-yecc/ 110 | 111 | The Execution logic follows the [GraphQL JS Reference Implementation](https://github.com/graphql/graphql-js) pretty closely, as does the module structure of the project. Not to mention the naming of files and concepts. 112 | 113 | If you spot anything that isn't following Elixir conventions though, that's a mistake. Please let us know by opening an issue or a PR and we'll fix it. 114 | 115 | ## Developers 116 | 117 | ### Getting Started 118 | 119 | Clone the repo and fetch its dependencies: 120 | 121 | ``` 122 | $ git clone https://github.com/graphql-elixir/graphql.git 123 | $ cd graphql 124 | $ mix deps.get 125 | $ mix test 126 | ``` 127 | 128 | ### Atom Editor Support 129 | 130 | > Using the `language-erlang` package? `.xrl` and `.yrl` files not syntax highlighting? 131 | 132 | Syntax highlighting in Atom for `leex` (`.xrl`) and `yecc` (`yrl`) can be added by modifying `grammars/erlang.cson`. 133 | 134 | Just open the `atom-language-erlang` package code in Atom and make the change described here: 135 | 136 | https://github.com/jonathanmarvens/atom-language-erlang/pull/11 137 | 138 | however if that PR has been merged then just grab the latest version of the plugin! 139 | 140 | ## Contributing 141 | 142 | We actively welcome pull requests, bug reports, feedback, issues, questions. Come and chat in the [#erlang channel on Slack](https://graphql-slack.herokuapp.com/) 143 | 144 | If you're planning to implement anything major, please let us know before you get too far so we can make sure your PR will be as mergable as possible. Oh, and don't forget to write tests. 145 | 146 | ## License 147 | 148 | [BSD](https://github.com/graphql-elixir/graphql/blob/master/LICENSE). 149 | -------------------------------------------------------------------------------- /test/graphql/validation/rules/provided_non_null_arguments_test.exs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Code.require_file "../../../support/validations.exs", __DIR__ 4 | 5 | defmodule GraphQL.Validation.Rules.ProvidedNonNullArgumentsTest do 6 | use ExUnit.Case, async: true 7 | 8 | import ValidationsSupport 9 | 10 | alias GraphQL.Validation.Rules.ProvidedNonNullArguments, as: Rule 11 | 12 | test "ignores unknown arguments" do 13 | assert_passes_rule( 14 | """ 15 | { 16 | dog { 17 | isHousetrained(unknownArgument: true) 18 | } 19 | } 20 | """, 21 | %Rule{} 22 | ) 23 | end 24 | 25 | test "Arg an optional arg" do 26 | assert_passes_rule( 27 | """ 28 | { 29 | dog { 30 | isHousetrained(atOtherHomes: true) 31 | } 32 | } 33 | """, 34 | %Rule{} 35 | ) 36 | end 37 | 38 | test "No Arg an optional arg" do 39 | assert_passes_rule( 40 | """ 41 | { 42 | dog { 43 | isHousetrained 44 | } 45 | } 46 | """, 47 | %Rule{} 48 | ) 49 | end 50 | 51 | test "Multiple args" do 52 | assert_passes_rule( 53 | """ 54 | { 55 | complicatedArgs { 56 | multipleReqs(req1: 1, req2: 2) 57 | } 58 | } 59 | """, 60 | %Rule{} 61 | ) 62 | end 63 | 64 | test "Multiple args in reverse order" do 65 | assert_passes_rule( 66 | """ 67 | { 68 | complicatedArgs { 69 | multipleReqs(req2: 2, req1: 1) 70 | } 71 | } 72 | """, 73 | %Rule{} 74 | ) 75 | end 76 | 77 | test "No args on multiple optional" do 78 | assert_passes_rule( 79 | """ 80 | { 81 | complicatedArgs { 82 | multipleOpts 83 | } 84 | } 85 | """, 86 | %Rule{} 87 | ) 88 | end 89 | 90 | test "One arg on multiple optional" do 91 | assert_passes_rule( 92 | """ 93 | { 94 | complicatedArgs { 95 | multipleOpts(opt1: 1) 96 | } 97 | } 98 | """, 99 | %Rule{} 100 | ) 101 | end 102 | 103 | test "Second arg on multiple optional" do 104 | assert_passes_rule( 105 | """ 106 | { 107 | complicatedArgs { 108 | multipleOpts(opt2: 1) 109 | } 110 | } 111 | """, 112 | %Rule{} 113 | ) 114 | end 115 | 116 | test "Multiple reqs on mixedList" do 117 | assert_passes_rule( 118 | """ 119 | { 120 | complicatedArgs { 121 | multipleOptAndReq(req1: 3, req2: 4) 122 | } 123 | } 124 | """, 125 | %Rule{} 126 | ) 127 | end 128 | 129 | test "Multiple reqs and one opt on mixedList" do 130 | assert_passes_rule( 131 | """ 132 | { 133 | complicatedArgs { 134 | multipleOptAndReq(req1: 3, req2: 4, opt1: 5) 135 | } 136 | } 137 | """, 138 | %Rule{} 139 | ) 140 | end 141 | 142 | test "All reqs and opts on mixedList" do 143 | assert_passes_rule( 144 | """ 145 | { 146 | complicatedArgs { 147 | multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6) 148 | } 149 | } 150 | """, 151 | %Rule{} 152 | ) 153 | end 154 | 155 | test "Missing one non-nullable argument" do 156 | assert_fails_rule( 157 | """ 158 | { 159 | complicatedArgs { 160 | multipleReqs(req2: 2) 161 | } 162 | } 163 | """, 164 | %Rule{}, 165 | ["Field \"multipleReqs\" argument \"req1\" of type \"Int!\" is required but not provided."] 166 | ) 167 | end 168 | 169 | test "Missing multiple non-nullable arguments" do 170 | assert_fails_rule( 171 | """ 172 | { 173 | complicatedArgs { 174 | multipleReqs 175 | } 176 | } 177 | """, 178 | %Rule{}, 179 | ["Field \"multipleReqs\" argument \"req1\" of type \"Int!\" is required but not provided.", 180 | "Field \"multipleReqs\" argument \"req2\" of type \"Int!\" is required but not provided."] 181 | ) 182 | end 183 | 184 | test "Incorrect value and missing argument" do 185 | assert_fails_rule( 186 | """ 187 | { 188 | complicatedArgs { 189 | multipleReqs(req1: "one") 190 | } 191 | } 192 | """, 193 | %Rule{}, 194 | ["Field \"multipleReqs\" argument \"req2\" of type \"Int!\" is required but not provided."] 195 | ) 196 | end 197 | 198 | @tag :skip 199 | test "ignores unknown directives" do 200 | assert_passes_rule( 201 | """ 202 | { 203 | dog @unknown 204 | } 205 | """, 206 | %Rule{} 207 | ) 208 | end 209 | 210 | @tag :skip 211 | test "with directives of valid types" do 212 | assert_passes_rule( 213 | """ 214 | { 215 | dog @include(if: true) { 216 | name 217 | } 218 | human @skip(if: false) { 219 | name 220 | } 221 | } 222 | """, 223 | %Rule{} 224 | ) 225 | end 226 | 227 | @tag :skip 228 | test "with directive with missing types" do 229 | assert_fails_rule( 230 | """ 231 | { 232 | dog @include { 233 | name @skip 234 | } 235 | } 236 | """, 237 | %Rule{}, 238 | ["Directive \"include\" argument \"if\" of type \"Boolean!\" is required but not provided.", 239 | "Directive \"skip\" argument \"if\" of type \"Boolean!\" is required but not provided." 240 | ] 241 | ) 242 | end 243 | end 244 | -------------------------------------------------------------------------------- /lib/graphql/execution/selection.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Execution.Selection do 3 | 4 | alias GraphQL.Execution.FieldResolver 5 | alias GraphQL.Execution.Types 6 | alias GraphQL.Execution.Directives 7 | alias GraphQL.Type.AbstractType 8 | 9 | @spec execute_fields_serially(ExecutionContext.t, atom, map, any) :: {ExecutionContext.t, map} 10 | def execute_fields_serially(context, parent_type, source_value, fields) do 11 | # call execute_fields because no async operations yet 12 | execute_fields(context, parent_type, source_value, fields) 13 | end 14 | 15 | @spec execute_fields(ExecutionContext.t, atom | Map, any, any) :: {ExecutionContext.t, map} 16 | def execute_fields(context, parent_type, source_value, fields) do 17 | Enum.reduce fields, {context, %{}}, fn({field_name_ast, field_asts}, {context, results}) -> 18 | FieldResolver.resolve_field(context, Types.unwrap_type(parent_type), source_value, field_asts) 19 | |> unwrap_result(results, field_name_ast) 20 | end 21 | end 22 | 23 | def complete_sub_fields(return_type, context, field_asts, result) do 24 | {context, sub_field_asts} = collect_sub_fields(context, return_type, field_asts) 25 | execute_fields(context, return_type, result, sub_field_asts.fields) 26 | end 27 | 28 | def collect_sub_fields(context, return_type, field_asts) do 29 | Enum.reduce field_asts, {context, %{fields: %{}, fragments: %{}}}, fn(field_ast, {context, field_fragment_map}) -> 30 | if selection_set = Map.get(field_ast, :selectionSet) do 31 | collect_selections(context, return_type, selection_set, field_fragment_map) 32 | else 33 | {context, field_fragment_map} 34 | end 35 | end 36 | end 37 | 38 | def collect_selections(context, runtime_type, selection_set, field_fragment_map \\ %{fields: %{}, fragments: %{}}) do 39 | Enum.reduce selection_set[:selections], {context, field_fragment_map}, fn(selection, {context, field_fragment_map}) -> 40 | collect_selection(context, runtime_type, selection, field_fragment_map) 41 | end 42 | end 43 | 44 | def collect_selection(context, _, %{kind: :Field} = selection, field_fragment_map) do 45 | if include_node?(context, selection[:directives]) do 46 | field_name = field_entry_key(selection) 47 | fields = field_fragment_map.fields[field_name] || [] 48 | {context, put_in(field_fragment_map.fields[field_name], [selection | fields])} 49 | else 50 | {context, field_fragment_map} 51 | end 52 | end 53 | 54 | def collect_selection(context, runtime_type, %{kind: :InlineFragment} = selection, field_fragment_map) do 55 | if include_node?(context, selection[:directives]) do 56 | collect_fragment(context, runtime_type, selection, field_fragment_map) 57 | else 58 | {context, field_fragment_map} 59 | end 60 | end 61 | 62 | def collect_selection(context, runtime_type, %{kind: :FragmentSpread} = selection, field_fragment_map) do 63 | fragment_name = selection.name.value 64 | if include_node?(context, selection[:directives]) do 65 | if !field_fragment_map.fragments[fragment_name] do 66 | field_fragment_map = put_in(field_fragment_map.fragments[fragment_name], true) 67 | collect_fragment(context, runtime_type, context.fragments[fragment_name], field_fragment_map) 68 | else 69 | {context, field_fragment_map} 70 | end 71 | else 72 | {context, field_fragment_map} 73 | end 74 | end 75 | 76 | def collect_selection(context, _, _, field_fragment_map), do: {context, field_fragment_map} 77 | 78 | def collect_fragment(context, runtime_type, selection, field_fragment_map) do 79 | condition_matches = typecondition_matches?(context, selection, runtime_type) 80 | if condition_matches do 81 | collect_selections(context, runtime_type, selection.selectionSet, field_fragment_map) 82 | else 83 | {context, field_fragment_map} 84 | end 85 | end 86 | 87 | defp unwrap_result({context, :undefined}, results, _), do: {context, results} 88 | defp unwrap_result({context, value}, results, field_name_ast) do 89 | {context, Map.put(results, field_name_ast.value, value)} 90 | end 91 | 92 | defp include_node?(_context, nil), do: true 93 | defp include_node?(context, directives) do 94 | Directives.resolve_directive(context, directives, :include) && 95 | !Directives.resolve_directive(context, directives, :skip) 96 | end 97 | 98 | defp field_entry_key(field) do 99 | Map.get(field, :alias, field.name) 100 | end 101 | 102 | defp typecondition_matches?(context, selection, runtime_type) do 103 | condition_ast = Map.get(selection, :typeCondition) 104 | typed_condition = GraphQL.Schema.type_from_ast(condition_ast, context.schema) 105 | 106 | cond do 107 | # no type condition was defined on this selectionset, so it's ok to run 108 | typed_condition == nil -> true 109 | # if the type condition is an interface or union, check to see if the 110 | # type implements the interface or belongs to the union. 111 | GraphQL.Type.is_abstract?(typed_condition) -> 112 | AbstractType.possible_type?(typed_condition, runtime_type) 113 | # in some cases with interfaces, the type won't be associated anywhere 114 | # else in the schema besides in the resolve function, which we can't 115 | # peek into when the typemap is generated. Because of this, the type 116 | # won't be found (:not_found). Before we return `false` because of that, 117 | # make a last check to see if the type exists after the interface's 118 | # resolve function has run. 119 | condition_ast.name.value == runtime_type.name -> true 120 | # the type doesn't exist, so, it can't match 121 | typed_condition == :not_found -> false 122 | # last chance to check if the type names (which are unique) match 123 | true -> runtime_type.name == typed_condition.name 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/graphql/validation/rules/no_fragment_cycles_test.exs: -------------------------------------------------------------------------------- 1 | 2 | Code.require_file "../../../support/validations.exs", __DIR__ 3 | 4 | defmodule GraphQL.Validation.Rules.NoFragmentCyclesTest do 5 | use ExUnit.Case, async: true 6 | 7 | import ValidationsSupport 8 | 9 | alias GraphQL.Validation.Rules.NoFragmentCycles, as: Rule 10 | 11 | test "single reference is valid" do 12 | assert_passes_rule( 13 | """ 14 | fragment fragA on Dog { ...fragB } 15 | fragment fragB on Dog { name } 16 | """, 17 | %Rule{} 18 | ) 19 | end 20 | 21 | test "spreading twice is not circular" do 22 | assert_passes_rule( 23 | """ 24 | fragment fragA on Dog { ...fragB, ...fragB } 25 | fragment fragB on Dog { name } 26 | """, 27 | %Rule{} 28 | ) 29 | end 30 | 31 | test "spreading twice indirectly is not circular" do 32 | assert_passes_rule( 33 | """ 34 | fragment fragA on Dog { ...fragB, ...fragC } 35 | fragment fragB on Dog { ...fragC } 36 | fragment fragC on Dog { name } 37 | """, 38 | %Rule{} 39 | ) 40 | end 41 | 42 | test "double spread within abstract types" do 43 | assert_passes_rule( 44 | """ 45 | fragment nameFragment on Pet { 46 | ... on Dog { name } 47 | ... on Cat { name } 48 | } 49 | 50 | fragment spreadsInAnon on Pet { 51 | ... on Dog { ...nameFragment } 52 | ... on Cat { ...nameFragment } 53 | } 54 | """, 55 | %Rule{} 56 | ) 57 | end 58 | 59 | test "does not false positive on unknown fragment" do 60 | assert_passes_rule( 61 | """ 62 | fragment nameFragment on Pet { 63 | ...UnknownFragment 64 | } 65 | """, 66 | %Rule{} 67 | ) 68 | end 69 | 70 | test "spreading recursively within field fails" do 71 | assert_fails_rule( 72 | """ 73 | fragment fragA on Human { relatives { ...fragA } } 74 | """, 75 | %Rule{}, 76 | ["Cannot spread fragment fragA within itself."] 77 | ) 78 | end 79 | 80 | test "no spreading itself directly" do 81 | assert_fails_rule( 82 | """ 83 | fragment fragA on Dog { ...fragA } 84 | """, 85 | %Rule{}, 86 | ["Cannot spread fragment fragA within itself."] 87 | ) 88 | end 89 | 90 | test "no spreading itself directly within inline fragment" do 91 | assert_fails_rule( 92 | """ 93 | fragment fragA on Pet { 94 | ... on Dog { 95 | ...fragA 96 | } 97 | } 98 | """, 99 | %Rule{}, 100 | ["Cannot spread fragment fragA within itself."] 101 | ) 102 | end 103 | 104 | test "no spreading itself indirectly" do 105 | assert_fails_rule( 106 | """ 107 | fragment fragA on Dog { ...fragB } 108 | fragment fragB on Dog { ...fragA } 109 | """, 110 | %Rule{}, 111 | ["Cannot spread fragment fragA within itself via fragB."] 112 | ) 113 | end 114 | 115 | test "no spreading itself indirectly reports opposite order" do 116 | assert_fails_rule( 117 | """ 118 | fragment fragB on Dog { ...fragA } 119 | fragment fragA on Dog { ...fragB } 120 | """, 121 | %Rule{}, 122 | ["Cannot spread fragment fragB within itself via fragA."] 123 | ) 124 | end 125 | 126 | test "no spreading itself indirectly within inline fragment" do 127 | assert_fails_rule( 128 | """ 129 | fragment fragA on Pet { 130 | ... on Dog { 131 | ...fragB 132 | } 133 | } 134 | fragment fragB on Pet { 135 | ... on Dog { 136 | ...fragA 137 | } 138 | } 139 | """, 140 | %Rule{}, 141 | ["Cannot spread fragment fragA within itself via fragB."] 142 | ) 143 | end 144 | 145 | test "no spreading itself deeply" do 146 | assert_fails_rule( 147 | """ 148 | fragment fragA on Dog { ...fragB } 149 | fragment fragB on Dog { ...fragC } 150 | fragment fragC on Dog { ...fragO } 151 | fragment fragX on Dog { ...fragY } 152 | fragment fragY on Dog { ...fragZ } 153 | fragment fragZ on Dog { ...fragO } 154 | fragment fragO on Dog { ...fragP } 155 | fragment fragP on Dog { ...fragA, ...fragX } 156 | """, 157 | %Rule{}, 158 | ["Cannot spread fragment fragA within itself via fragB, fragC, fragO, fragP.", 159 | "Cannot spread fragment fragO within itself via fragP, fragX, fragY, fragZ."] 160 | ) 161 | end 162 | 163 | test "no spreading itself deeply two paths" do 164 | assert_fails_rule( 165 | """ 166 | fragment fragA on Dog { ...fragB, ...fragC } 167 | fragment fragB on Dog { ...fragA } 168 | fragment fragC on Dog { ...fragA } 169 | """, 170 | %Rule{}, 171 | ["Cannot spread fragment fragA within itself via fragB.", 172 | "Cannot spread fragment fragA within itself via fragC."] 173 | ) 174 | end 175 | 176 | test "no spreading itself deeply two paths -- alt traverse order" do 177 | assert_fails_rule( 178 | """ 179 | fragment fragA on Dog { ...fragC } 180 | fragment fragB on Dog { ...fragC } 181 | fragment fragC on Dog { ...fragA, ...fragB } 182 | """, 183 | %Rule{}, 184 | ["Cannot spread fragment fragA within itself via fragC.", 185 | "Cannot spread fragment fragC within itself via fragB."] 186 | ) 187 | end 188 | 189 | test "no spreading itself deeply and immediately" do 190 | assert_fails_rule( 191 | """ 192 | fragment fragA on Dog { ...fragB } 193 | fragment fragB on Dog { ...fragB, ...fragC } 194 | fragment fragC on Dog { ...fragA, ...fragB } 195 | """, 196 | %Rule{}, 197 | ["Cannot spread fragment fragB within itself.", 198 | "Cannot spread fragment fragA within itself via fragB, fragC.", 199 | "Cannot spread fragment fragB within itself via fragC."] 200 | ) 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /test/graphql/execution/directive_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Execution.Executor.DirectiveTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExUnit.TestHelpers 5 | 6 | alias GraphQL.Schema 7 | alias GraphQL.Type.ObjectType 8 | alias GraphQL.Type.String 9 | 10 | defmodule TestSchema do 11 | def schema do 12 | %Schema{ 13 | query: %ObjectType{ 14 | name: "TestType", 15 | fields: %{ 16 | a: %{type: %String{}, resolve: "a"}, 17 | b: %{type: %String{}, resolve: "b"} 18 | } 19 | } 20 | } 21 | end 22 | end 23 | 24 | test "works without directives" do 25 | {:ok, result} = execute(TestSchema.schema, "{ a, b }") 26 | assert_data(result, %{a: "a", b: "b"}) 27 | end 28 | 29 | test "if true includes the scalar" do 30 | {:ok, result} = execute(TestSchema.schema, "{ a, b @include(if: true) }") 31 | assert_data(result, %{a: "a", b: "b"}) 32 | end 33 | 34 | test "if false omits the scalar" do 35 | {:ok, result} = execute(TestSchema.schema, "{ a, b @include(if: false) }") 36 | assert_data(result, %{a: "a"}) 37 | end 38 | 39 | test "unless false includes scalar" do 40 | {:ok, result} = execute(TestSchema.schema, "{ a, b @skip(if: false) }") 41 | assert_data(result, %{a: "a", b: "b"}) 42 | end 43 | 44 | test "unless true omits scalar" do 45 | {:ok, result} = execute(TestSchema.schema, "{ a, b @skip(if: true) }") 46 | assert_data(result, %{a: "a"}) 47 | end 48 | 49 | test "if false omits fragment spread" do 50 | {:ok, result} = execute(TestSchema.schema, """ 51 | query Q { 52 | a 53 | ...Frag @include(if: false) 54 | } 55 | fragment Frag on TestType { 56 | b 57 | } 58 | """) 59 | assert_data(result, %{a: "a"}) 60 | end 61 | 62 | test "if true includes fragment spread" do 63 | {:ok, result} = execute(TestSchema.schema, """ 64 | query Q { 65 | a 66 | ...Frag @include(if: true) 67 | } 68 | fragment Frag on TestType { 69 | b 70 | } 71 | """) 72 | assert_data(result, %{a: "a", b: "b"}) 73 | end 74 | 75 | test "unless false includes fragment spread" do 76 | {:ok, result} = execute(TestSchema.schema, """ 77 | query Q { 78 | a 79 | ...Frag @skip(if: false) 80 | } 81 | fragment Frag on TestType { 82 | b 83 | } 84 | """) 85 | assert_data(result, %{a: "a", b: "b"}) 86 | end 87 | 88 | test "unless true omits fragment spread" do 89 | {:ok, result} = execute(TestSchema.schema, """ 90 | query Q { 91 | a 92 | ...Frag @skip(if: true) 93 | } 94 | fragment Frag on TestType { 95 | b 96 | } 97 | """) 98 | assert_data(result, %{a: "a"}) 99 | end 100 | 101 | test "if false omits inline fragment" do 102 | {:ok, result} = execute(TestSchema.schema, """ 103 | query Q { 104 | a 105 | ... on TestType @include(if: false) { 106 | b 107 | } 108 | } 109 | """) 110 | assert_data(result, %{a: "a"}) 111 | end 112 | 113 | test "if true includes inline fragment" do 114 | {:ok, result} = execute(TestSchema.schema, """ 115 | query Q { 116 | a 117 | ... on TestType @include(if: true) { 118 | b 119 | } 120 | } 121 | """) 122 | assert_data(result, %{a: "a", b: "b"}) 123 | end 124 | 125 | test "unless false includes inline fragment" do 126 | {:ok, result} = execute(TestSchema.schema, """ 127 | query Q { 128 | a 129 | ... on TestType @skip(if: false) { 130 | b 131 | } 132 | } 133 | """) 134 | assert_data(result, %{a: "a", b: "b"}) 135 | end 136 | 137 | test "unless true includes inline fragment" do 138 | {:ok, result} = execute(TestSchema.schema, """ 139 | query Q { 140 | a 141 | ... on TestType @skip(if: true) { 142 | b 143 | } 144 | } 145 | """) 146 | assert_data(result, %{a: "a"}) 147 | end 148 | 149 | test "if false omits anonymous inline fragment" do 150 | {:ok, result} = execute(TestSchema.schema, """ 151 | query Q { 152 | a 153 | ... @include(if: false) { 154 | b 155 | } 156 | } 157 | """) 158 | assert_data(result, %{a: "a"}) 159 | end 160 | 161 | test "if true includes anonymous inline fragment" do 162 | {:ok, result} = execute(TestSchema.schema, """ 163 | query Q { 164 | a 165 | ... @include(if: true) { 166 | b 167 | } 168 | } 169 | """) 170 | assert_data(result, %{a: "a", b: "b"}) 171 | end 172 | 173 | test "unless false includes anonymous inline fragment" do 174 | {:ok, result} = execute(TestSchema.schema, """ 175 | query Q { 176 | a 177 | ... @skip(if: false) { 178 | b 179 | } 180 | } 181 | """) 182 | assert_data(result, %{a: "a", b: "b"}) 183 | end 184 | 185 | test "unless true includes anonymous inline fragment" do 186 | {:ok, result} = execute(TestSchema.schema, """ 187 | query Q { 188 | a 189 | ... @skip(if: true) { 190 | b 191 | } 192 | } 193 | """) 194 | assert_data(result, %{a: "a"}) 195 | end 196 | 197 | test "include and no skip" do 198 | {:ok, result} = execute(TestSchema.schema, "{ a, b @include(if: true) @skip(if: false)}") 199 | assert_data(result, %{a: "a", b: "b"}) 200 | end 201 | 202 | test "include and skip" do 203 | {:ok, result} = execute(TestSchema.schema, "{ a, b @include(if: true) @skip(if: true)}") 204 | assert_data(result, %{a: "a"}) 205 | end 206 | 207 | test "no include or skip" do 208 | {:ok, result} = execute(TestSchema.schema, "{ a, b @include(if: false) @skip(if: false)}") 209 | assert_data(result, %{a: "a"}) 210 | end 211 | 212 | test "include with variable" do 213 | query = "query q($test: Boolean) { a, b @include(if: $test) }" 214 | {:ok, result} = execute(TestSchema.schema, query, variable_values: %{"test" => false}) 215 | assert_data(result, %{a: "a"}) 216 | end 217 | 218 | test "skip with variable" do 219 | query = "query q($test: Boolean) { a, b @skip(if: $test) }" 220 | {:ok, result} = execute(TestSchema.schema, query, variable_values: %{"test" => true}) 221 | assert_data(result, %{a: "a"}) 222 | end 223 | end 224 | -------------------------------------------------------------------------------- /test/graphql/validation/rules/fields_on_correct_type_test.exs: -------------------------------------------------------------------------------- 1 | 2 | Code.require_file "../../../support/validations.exs", __DIR__ 3 | 4 | defmodule GraphQL.Validation.Rules.FieldOnCorrectTypeTest do 5 | use ExUnit.Case, async: true 6 | 7 | import ValidationsSupport 8 | 9 | alias GraphQL.Validation.Rules.FieldsOnCorrectType, as: Rule 10 | 11 | test "Object field selection" do 12 | assert_passes_rule( 13 | """ 14 | fragment objectFieldSelection on Dog { 15 | __typename 16 | name 17 | } 18 | """, 19 | %Rule{} 20 | ) 21 | end 22 | 23 | test "Aliased object field selection" do 24 | assert_passes_rule( 25 | """ 26 | fragment aliasedObjectFieldSelection on Dog { 27 | tn: __typename 28 | otherName: name 29 | } 30 | """, 31 | %Rule{} 32 | ) 33 | end 34 | 35 | test "Interface field selection" do 36 | assert_passes_rule( 37 | """ 38 | fragment interfaceFieldSelection on Pet { 39 | __typename 40 | name 41 | } 42 | """, 43 | %Rule{} 44 | ) 45 | end 46 | 47 | test "Aliased interface field selection" do 48 | assert_passes_rule( 49 | """ 50 | fragment interfaceFieldSelection on Pet { 51 | otherName : name 52 | } 53 | """, 54 | %Rule{} 55 | ) 56 | end 57 | 58 | test "Lying alias selection" do 59 | assert_passes_rule( 60 | """ 61 | fragment lyingAliasSelection on Dog { 62 | name : nickname 63 | } 64 | """, 65 | %Rule{} 66 | ) 67 | end 68 | 69 | test "Ignores fields on unknown type" do 70 | assert_passes_rule( 71 | """ 72 | fragment unknownSelection on UnknownType { 73 | unknownField 74 | } 75 | """, 76 | %Rule{} 77 | ) 78 | end 79 | 80 | test "reports errors when type is known again" do 81 | assert_fails_rule( 82 | """ 83 | fragment typeKnownAgain on Pet { 84 | unknown_pet_field { 85 | ... on Cat { 86 | unknown_cat_field 87 | } 88 | } 89 | } 90 | """, 91 | %Rule{} 92 | ) 93 | end 94 | 95 | test "Field not defined on fragment" do 96 | assert_fails_rule( 97 | """ 98 | fragment fieldNotDefined on Dog { 99 | meowVolume 100 | } 101 | """, 102 | %Rule{}, 103 | [~S(Cannot query field "meowVolume" on type "Dog".)] 104 | ) 105 | end 106 | 107 | test "Ignores deeply unknown field" do 108 | assert_fails_rule( 109 | """ 110 | fragment deepFieldNotDefined on Dog { 111 | unknown_field { 112 | deeper_unknown_field 113 | } 114 | } 115 | """, 116 | %Rule{}, 117 | [~S(Cannot query field "unknown_field" on type "Dog".)] 118 | ) 119 | end 120 | 121 | test "Sub-field not defined" do 122 | assert_fails_rule( 123 | """ 124 | fragment subFieldNotDefined on Human { 125 | pets { 126 | unknown_field 127 | } 128 | } 129 | """, 130 | %Rule{}, 131 | [~S(Cannot query field "unknown_field" on type "Pet".)] 132 | ) 133 | end 134 | 135 | test "Field not defined on inline fragment" do 136 | assert_fails_rule( 137 | """ 138 | fragment fieldNotDefined on Pet { 139 | ... on Dog { 140 | meowVolume 141 | } 142 | } 143 | """, 144 | %Rule{}, 145 | [~S(Cannot query field "meowVolume" on type "Dog".)] 146 | ) 147 | end 148 | 149 | test "Aliased field target not defined" do 150 | assert_fails_rule( 151 | """ 152 | fragment aliasedFieldTargetNotDefined on Dog { 153 | volume : mooVolume 154 | } 155 | """, 156 | %Rule{}, 157 | [~S(Cannot query field "mooVolume" on type "Dog".)] 158 | ) 159 | end 160 | 161 | test "Aliased lying field target not defined" do 162 | assert_fails_rule( 163 | """ 164 | fragment aliasedLyingFieldTargetNotDefined on Dog { 165 | barkVolume : kawVolume 166 | } 167 | """, 168 | %Rule{}, 169 | [~S(Cannot query field "kawVolume" on type "Dog".)] 170 | ) 171 | end 172 | 173 | test "Not defined on interface" do 174 | assert_fails_rule( 175 | """ 176 | fragment notDefinedOnInterface on Pet { 177 | tailLength 178 | } 179 | """, 180 | %Rule{}, 181 | [~S(Cannot query field "tailLength" on type "Pet".)] 182 | ) 183 | end 184 | 185 | test "Defined on implementors but not on interface" do 186 | assert_fails_rule( 187 | """ 188 | fragment definedOnImplementorsButNotInterface on Pet { 189 | nickname 190 | } 191 | """, 192 | %Rule{}, 193 | [ 194 | ~S(Cannot query field "nickname" on type "Pet". ) <> 195 | ~S(However, this field exists on "Cat", "Dog". ) <> 196 | ~S(Perhaps you meant to use an inline fragment?) 197 | ] 198 | ) 199 | end 200 | 201 | test "Meta field selection on union" do 202 | assert_passes_rule( 203 | """ 204 | fragment directFieldSelectionOnUnion on CatOrDog { 205 | __typename 206 | } 207 | """, 208 | %Rule{} 209 | ) 210 | end 211 | 212 | test "Direct field selection on union" do 213 | assert_fails_rule( 214 | """ 215 | fragment directFieldSelectionOnUnion on CatOrDog { 216 | directField 217 | } 218 | """, 219 | %Rule{}, 220 | [~S(Cannot query field "directField" on type "CatOrDog".)] 221 | ) 222 | end 223 | 224 | test "Defined on implementors queried on union" do 225 | assert_fails_rule( 226 | """ 227 | fragment definedOnImplementorsQueriedOnUnion on CatOrDog { 228 | name 229 | } 230 | """, 231 | %Rule{}, 232 | [ 233 | ~S(Cannot query field "name" on type "CatOrDog". ) <> 234 | ~S(However, this field exists on "Canine", "Cat", "Dog", "Being", "Pet". ) <> 235 | ~S(Perhaps you meant to use an inline fragment?) 236 | ] 237 | ) 238 | end 239 | 240 | test "valid field in inline fragment" do 241 | assert_passes_rule( 242 | """ 243 | fragment objectFieldSelection on Pet { 244 | ... on Dog { 245 | name 246 | } 247 | ... { 248 | name 249 | } 250 | } 251 | """, 252 | %Rule{} 253 | ) 254 | end 255 | 256 | end 257 | -------------------------------------------------------------------------------- /test/star_wars/query_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../support/star_wars/data.exs", __DIR__ 2 | Code.require_file "../support/star_wars/schema.exs", __DIR__ 3 | 4 | defmodule GraphQL.StarWars.QueryTest do 5 | use ExUnit.Case, async: true 6 | import ExUnit.TestHelpers 7 | 8 | test "correctly identifies R2-D2 as the hero of the Star Wars Saga" do 9 | query = ~S[ query hero_name_query { hero { name } }] 10 | 11 | {:ok, result} = execute(StarWars.Schema.schema, query) 12 | assert_data(result, %{ 13 | hero: %{name: "R2-D2"} 14 | }) 15 | end 16 | 17 | test "Allows us to query for the ID and friends of R2-D2" do 18 | query = ~S[ 19 | query hero_and_friends_query { 20 | hero { id, name, friends { name }} 21 | } 22 | ] 23 | 24 | {:ok, result} = execute(StarWars.Schema.schema, query) 25 | assert_data(result, %{ 26 | hero: %{ 27 | friends: [ 28 | %{name: "Luke Skywalker"}, 29 | %{name: "Han Solo"}, 30 | %{name: "Leia Organa"} 31 | ], 32 | id: "2001", 33 | name: "R2-D2" 34 | } 35 | }) 36 | end 37 | 38 | test "Allows us to query for the friends of friends of R2-D2" do 39 | query = ~S[ 40 | query nested_query { 41 | hero { 42 | name 43 | friends { 44 | name 45 | appears_in 46 | friends { 47 | name 48 | } 49 | } 50 | } 51 | } 52 | ] 53 | 54 | {:ok, result} = execute(StarWars.Schema.schema, query) 55 | assert_data(result, %{ 56 | hero: %{ 57 | name: "R2-D2", 58 | friends: [ 59 | %{appears_in: ["NEWHOPE", "EMPIRE", "JEDI"], 60 | friends: [%{name: "Han Solo"}, %{name: "Leia Organa"}, %{name: "C-3PO"}, %{name: "R2-D2"}], 61 | name: "Luke Skywalker"}, 62 | %{appears_in: ["NEWHOPE", "EMPIRE", "JEDI"], 63 | friends: [%{name: "Luke Skywalker"}, %{name: "Leia Organa"}, %{name: "R2-D2"}], 64 | name: "Han Solo"}, 65 | %{appears_in: ["NEWHOPE", "EMPIRE", "JEDI"], 66 | friends: [%{name: "Luke Skywalker"}, %{name: "Han Solo"}, %{name: "C-3PO"}, %{name: "R2-D2"}], 67 | name: "Leia Organa"}] 68 | } 69 | }) 70 | end 71 | 72 | test "Allows us to query for Luke Skywalker directly, using his ID" do 73 | query = ~S[query find_luke { human(id: "1000") { name } } ] # would have been useful for Episode VII 74 | 75 | {:ok, result} = execute(StarWars.Schema.schema, query) 76 | assert_data(result, %{ 77 | human: %{name: "Luke Skywalker"} 78 | }) 79 | end 80 | 81 | test "Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID" do 82 | query = ~S[query fetch_id($some_id: String!) { human(id: $some_id) { name }}] 83 | 84 | {:ok, result} = execute(StarWars.Schema.schema, query, variable_values: %{"some_id" => "1000"}) 85 | assert_data(result, %{ 86 | human: %{name: "Luke Skywalker"} 87 | }) 88 | end 89 | 90 | test "Allows us to create a generic query, then use it to fetch Han Solo using his ID" do 91 | query = ~S[query fetch_some_id($some_id: String!) { human(id: $some_id) { name }}] 92 | 93 | {:ok, result} = execute(StarWars.Schema.schema, query, variable_values: %{"some_id" => "1002"}) 94 | assert_data(result, %{ 95 | human: %{name: "Han Solo"} 96 | }) 97 | end 98 | 99 | @tag :skip # returns %{} instead of nil. Which is right? 100 | test "Allows us to create a generic query, then pass an invalid ID to get null back" do 101 | query = ~S[query human_query($id: String!) { human(id: $id) { name }}] 102 | 103 | {:ok, result} = execute(StarWars.Schema.schema, query, variable_values: %{id: "invalid id"}) 104 | assert_data(result, %{ 105 | human: nil 106 | }) 107 | end 108 | 109 | test "Allows us to query for Luke, changing his key with an alias" do 110 | query = ~S[query fetch_luke_aliased { luke: human(id: "1000") { name }}] 111 | 112 | {:ok, result} = execute(StarWars.Schema.schema, query, variable_values: %{id: "invalid id"}) 113 | assert_data(result, %{ 114 | luke: %{name: "Luke Skywalker"} 115 | }) 116 | end 117 | 118 | test "Allows us to query for both Luke and Leia, using two root fields and an alias" do 119 | query = ~S[query fetch_luke_and_leia_aliased { 120 | luke: human(id: "1000") { name } 121 | leia: human(id: "1003") { name } 122 | }] 123 | 124 | {:ok, result} = execute(StarWars.Schema.schema, query) 125 | assert_data(result, %{ 126 | leia: %{name: "Leia Organa"}, 127 | luke: %{name: "Luke Skywalker"} 128 | }) 129 | end 130 | 131 | test "Allows us to query using duplicated content" do 132 | query = ~S[ 133 | query duplicate_fields { 134 | luke: human(id: "1000") { name, home_planet } 135 | leia: human(id: "1003") { name, home_planet } 136 | } 137 | ] 138 | 139 | {:ok, result} = execute(StarWars.Schema.schema, query) 140 | assert_data(result, %{ 141 | leia: %{home_planet: "Alderaan", name: "Leia Organa"}, 142 | luke: %{home_planet: "Tatooine", name: "Luke Skywalker"} 143 | }) 144 | end 145 | 146 | test "Allows us to use a fragment to avoid duplicating content" do 147 | query = ~S[ 148 | query duplicate_fields { 149 | luke: human(id: "1000") { ...human_fragment } 150 | leia: human(id: "1003") { ...human_fragment } 151 | } 152 | fragment human_fragment on Human { 153 | name, home_planet 154 | } 155 | ] 156 | 157 | {:ok, result} = execute(StarWars.Schema.schema, query) 158 | assert_data(result, %{ 159 | leia: %{home_planet: "Alderaan", name: "Leia Organa"}, 160 | luke: %{home_planet: "Tatooine", name: "Luke Skywalker"} 161 | }) 162 | end 163 | 164 | test "Allows us to verify that R2-D2 is a droid" do 165 | query = ~S[ 166 | query check_type_of_r2d2 { 167 | hero { 168 | __typename 169 | name 170 | } 171 | } 172 | ] 173 | 174 | {:ok, result} = execute(StarWars.Schema.schema, query) 175 | assert_data(result, %{ 176 | hero: %{name: "R2-D2", "__typename": "Droid"} 177 | }) 178 | end 179 | 180 | test "Allows us to verify that Luke is a human" do 181 | query = ~S[ 182 | query check_type_of_luke { 183 | hero(episode: EMPIRE) { 184 | __typename 185 | name 186 | } 187 | } 188 | ] 189 | 190 | {:ok, result} = execute(StarWars.Schema.schema, query) 191 | assert_data(result, %{ 192 | hero: %{name: "Luke Skywalker", "__typename": "Human"} 193 | }) 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /lib/graphql/lang/ast/type_info_visitor.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule GraphQL.Lang.AST.TypeInfoVisitor do 3 | @moduledoc ~S""" 4 | A Visitor implementation that adds type information to the accumulator, so that subsequent 5 | visitors can use the information to perform validations. 6 | 7 | NOTE: this file is mostly a straight clone from the graphql-js implementation. 8 | There were no tests for the graphql-js implementation and there are no tests for this one. 9 | Like the JS version, this implementation will be tested indirectly by the validator tests. 10 | """ 11 | 12 | alias GraphQL.Type 13 | alias GraphQL.Util.Stack 14 | alias GraphQL.Lang.AST.TypeInfo 15 | alias GraphQL.Lang.AST.Visitor 16 | alias GraphQL.Schema 17 | 18 | defstruct name: "TypeInfoVisitor" 19 | 20 | defimpl Visitor do 21 | 22 | def stack_push(accumulator, stack_name, value) do 23 | old_type_info = accumulator[:type_info] 24 | stack = Stack.push(Map.get(old_type_info, stack_name), value) 25 | new_type_info = Map.merge(old_type_info, %{stack_name => stack}) 26 | put_in(accumulator[:type_info], new_type_info) 27 | end 28 | 29 | def stack_pop(accumulator, stack_name) do 30 | old_type_info = accumulator[:type_info] 31 | stack = Stack.pop(Map.get(old_type_info, stack_name)) 32 | new_type_info = Map.merge(old_type_info, %{stack_name => stack}) 33 | put_in(accumulator[:type_info], new_type_info) 34 | end 35 | 36 | def set_directive(accumulator, directive) do 37 | put_in( 38 | accumulator[:type_info], 39 | %TypeInfo{accumulator[:type_info] | directive: directive} 40 | ) 41 | end 42 | 43 | def set_argument(accumulator, argument) do 44 | put_in( 45 | accumulator[:type_info], 46 | %TypeInfo{accumulator[:type_info] | argument: argument} 47 | ) 48 | end 49 | 50 | def enter(_visitor, node, accumulator) do 51 | accumulator = case node.kind do 52 | :SelectionSet -> 53 | type = TypeInfo.type(accumulator[:type_info]) 54 | named_type = TypeInfo.named_type(type) 55 | if Type.is_composite_type?(named_type) do 56 | stack_push(accumulator, :parent_type_stack, named_type) 57 | else 58 | stack_push(accumulator, :parent_type_stack, nil) 59 | end 60 | :Field -> 61 | parent_type = TypeInfo.parent_type(accumulator[:type_info]) 62 | if parent_type do 63 | field_def = TypeInfo.find_field_def( 64 | accumulator[:type_info].schema, 65 | parent_type, 66 | node 67 | ) 68 | field_def_type = if field_def, do: field_def.type, else: nil 69 | accumulator = stack_push(accumulator, :field_def_stack, field_def) 70 | stack_push(accumulator, :type_stack, field_def_type) 71 | else 72 | accumulator = stack_push(accumulator, :field_def_stack, nil) 73 | stack_push(accumulator, :type_stack, nil) 74 | end 75 | :Directive -> 76 | # add this once we add directive validations 77 | # see ref impl: src/validation/rules/KnownDirectives.js 78 | # 79 | # TODO: once we implement directive support in the schema, 80 | # get the directive definition from the schema by name and 81 | #this._directive = schema.getDirective(node.name.value); // JS example 82 | # and set it like this 83 | #set_directive(directive_def) 84 | set_directive(accumulator, nil) 85 | :OperationDefinition -> 86 | type = case node.operation do 87 | :query -> accumulator[:type_info].schema.query 88 | :mutation -> accumulator[:type_info].schema.mutation 89 | _ -> raise "node operation #{node.operation} not handled" 90 | end 91 | stack_push(accumulator, :type_stack, type) 92 | kind when kind in [:InlineFragment, :FragmentDefinition] -> 93 | output_type = if Map.has_key?(node, :typeCondition) do 94 | Schema.type_from_ast(node.typeCondition, accumulator[:type_info].schema) 95 | else 96 | TypeInfo.type(accumulator[:type_info]) 97 | end 98 | stack_push(accumulator, :type_stack, output_type) 99 | :VariableDefinition -> 100 | input_type = Schema.type_from_ast(node.type, accumulator[:type_info].schema) 101 | stack_push(accumulator, :input_type_stack, input_type) 102 | :Argument -> 103 | field_or_directive = TypeInfo.directive(accumulator[:type_info]) || 104 | TypeInfo.field_def(accumulator[:type_info]) 105 | if field_or_directive do 106 | arg_def = Enum.find( 107 | Map.get(field_or_directive, :arguments, %{}), 108 | fn(arg) -> arg == node.name.value end 109 | ) 110 | accumulator = set_argument(accumulator, arg_def) 111 | stack_push(accumulator, :input_type_stack, (if arg_def && Map.has_key?(arg_def, :type), do: arg_def.type, else: nil)) 112 | else 113 | accumulator = set_argument(accumulator, nil) 114 | stack_push(accumulator, :input_type_stack, nil) 115 | end 116 | :List -> 117 | input_type = TypeInfo.input_type(accumulator[:type_info]) 118 | list_type = TypeInfo.named_type(input_type) 119 | if %Type.List{} === list_type do 120 | stack_push(accumulator, :input_type_stack, list_type.ofType) 121 | else 122 | stack_push(accumulator, :input_type_stack, nil) 123 | end 124 | :ObjectField -> 125 | input_type = TypeInfo.input_type(accumulator[:type_info]) 126 | object_type = TypeInfo.named_type(input_type) 127 | if %Type.Input{} === object_type do 128 | input_field = TypeInfo.find_field_def( 129 | accumulator[:type_info].schema, 130 | object_type, 131 | node 132 | ) 133 | field_type = if input_field, do: input_field.type, else: nil 134 | stack_push(accumulator, :input_type_stack, field_type) 135 | else 136 | stack_push(accumulator, :input_type_stack, nil) 137 | end 138 | _ -> 139 | accumulator 140 | end 141 | {:continue, accumulator} 142 | end 143 | 144 | def leave(_visitor, node, accumulator) do 145 | case node.kind do 146 | :SelectionSet -> 147 | stack_pop(accumulator, :parent_type_stack) 148 | :Field -> 149 | accumulator = stack_pop(accumulator, :field_def_stack) 150 | stack_pop(accumulator, :type_stack) 151 | :Directive -> 152 | set_directive(accumulator, nil) 153 | kind when kind in [:OperationDefinition, :InlineFragment, :FragmentDefinition] -> 154 | stack_pop(accumulator, :type_stack) 155 | :Argument -> 156 | set_argument(accumulator, nil) 157 | kind when kind in [:List, :ObjectField, :VariableDefinition] -> 158 | stack_pop(accumulator, :input_type_stack) 159 | _ -> 160 | accumulator 161 | end 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/graphql/type/union_interface_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Lang.Type.UnionInterfaceTest do 2 | use ExUnit.Case, async: true 3 | import ExUnit.TestHelpers 4 | 5 | alias GraphQL.Type.String 6 | alias GraphQL.Type.ObjectType 7 | alias GraphQL.Type.Boolean 8 | alias GraphQL.Type.List 9 | 10 | defmodule Dog do 11 | defstruct name: nil, barks: nil 12 | end 13 | 14 | defmodule Cat do 15 | defstruct name: nil, meows: nil 16 | end 17 | 18 | defmodule Person do 19 | defstruct name: nil, pets: [], friends: [] 20 | end 21 | 22 | def named_type do 23 | GraphQL.Type.Interface.new %{ 24 | name: "Named", 25 | fields: %{ 26 | name: %{type: %String{}} 27 | } 28 | } 29 | end 30 | 31 | def dog_type do 32 | %ObjectType{ 33 | name: "Dog", 34 | interfaces: [named_type], 35 | fields: %{ 36 | name: %{type: %String{}}, 37 | barks: %{type: %Boolean{}} 38 | }, 39 | isTypeOf: fn (%Dog{}) -> true; (_) -> false end 40 | } 41 | end 42 | 43 | def cat_type do 44 | %ObjectType{ 45 | name: "Cat", 46 | interfaces: [named_type], 47 | fields: %{ 48 | name: %{type: %String{}}, 49 | meows: %{type: %Boolean{}} 50 | }, 51 | isTypeOf: fn (%Cat{}) -> true; (_) -> false end 52 | } 53 | end 54 | 55 | def pet_type do 56 | GraphQL.Type.Union.new %{ 57 | name: "Pet", 58 | types: [dog_type, cat_type], 59 | resolver: fn 60 | (%Dog{}) -> dog_type 61 | (%Cat{}) -> cat_type 62 | end 63 | } 64 | end 65 | 66 | def person_type do 67 | %ObjectType{ 68 | name: "Person", 69 | interfaces: [named_type], 70 | fields: %{ 71 | name: %{type: %String{}}, 72 | pets: %{type: %List{ofType: pet_type}}, 73 | friends: %{type: %List{ofType: named_type}}, 74 | }, 75 | isTypeOf: fn (%Person{}) -> true; (_) -> false end 76 | } 77 | end 78 | 79 | def schema do 80 | GraphQL.Schema.new(%{query: person_type}) 81 | end 82 | 83 | def garfield, do: %Cat{name: "Garfield", meows: false} 84 | def odie, do: %Dog{name: "Odie", barks: true} 85 | def liz, do: %Person{name: "Liz"} 86 | def john, do: %Person{name: "John", pets: [ odie, garfield ], friends: [liz, odie]} 87 | 88 | test "can introspect on union and intersection types" do 89 | query = """ 90 | { 91 | Named: __type(name: "Named") { 92 | kind 93 | name 94 | fields { name } 95 | interfaces { name } 96 | possibleTypes { name } 97 | enumValues { name } 98 | inputFields { name } 99 | } 100 | Pet: __type(name: "Pet") { 101 | kind 102 | name 103 | fields { name } 104 | interfaces { name } 105 | possibleTypes { name } 106 | enumValues { name } 107 | inputFields { name } 108 | } 109 | } 110 | """ 111 | {:ok, result} = execute(schema, query) 112 | assert_data(result, 113 | %{"Named" => %{"enumValues" => nil, 114 | "fields" => [%{"name" => "name"}], 115 | "inputFields" => nil, "interfaces" => nil, 116 | "kind" => "INTERFACE", "name" => "Named", 117 | "possibleTypes" => [%{"name" => "Cat"}, 118 | %{"name" => "Dog"}, %{"name" => "Person"}]}, 119 | "Pet" => %{"enumValues" => nil, "fields" => nil, 120 | "inputFields" => nil, "interfaces" => nil, 121 | "kind" => "UNION", "name" => "Pet", 122 | "possibleTypes" => [%{"name" => "Dog"}, 123 | %{"name" => "Cat"}]}} 124 | ) 125 | end 126 | 127 | test "executes using union types" do 128 | # NOTE: This is an *invalid* query, but it should be an *executable* query. 129 | query = """ 130 | { 131 | __typename 132 | name 133 | pets { 134 | __typename 135 | name 136 | barks 137 | meows 138 | } 139 | } 140 | """ 141 | 142 | {:ok, result} = execute(schema, query, root_value: john, validate: false) 143 | assert_data(result, 144 | %{"__typename" => "Person", 145 | "name" => "John", 146 | "pets" => [ 147 | %{"__typename" => "Dog", "barks" => true, "name" => "Odie"}, 148 | %{"__typename" => "Cat", "meows" => false, "name" => "Garfield"} 149 | ] 150 | } 151 | ) 152 | end 153 | 154 | test "executes union types with inline fragments" do 155 | query = """ 156 | { 157 | __typename 158 | name 159 | pets { 160 | __typename 161 | ... on Dog { 162 | name 163 | barks 164 | } 165 | ... on Cat { 166 | name 167 | meows 168 | } 169 | } 170 | } 171 | """ 172 | 173 | {:ok, result} = execute(schema, query, root_value: john, validate: false) 174 | assert_data(result, 175 | %{"__typename" => "Person", 176 | "name" => "John", 177 | "pets" => [ 178 | %{"__typename" => "Dog", "barks" => true, "name" => "Odie"}, 179 | %{"__typename" => "Cat", "meows" => false, "name" => "Garfield"} 180 | ] 181 | } 182 | ) 183 | end 184 | 185 | test "executes using interface types" do 186 | # NOTE: This is an *invalid* query, but it should be an *executable* query. 187 | query = """ 188 | { 189 | __typename 190 | name 191 | friends { 192 | __typename 193 | name 194 | barks 195 | meows 196 | } 197 | } 198 | """ 199 | 200 | {:ok, result} = execute(schema, query, root_value: john, validate: false) 201 | assert_data(result, 202 | %{"__typename" => "Person", 203 | "friends" => [ 204 | %{"__typename" => "Person", "name" => "Liz"}, 205 | %{"__typename" => "Dog", "barks" => true, "name" => "Odie"} 206 | ], 207 | "name" => "John" 208 | } 209 | ) 210 | end 211 | 212 | test "executes types with inline fragments" do 213 | # This is the valid version of the query in the above test. 214 | query = """ 215 | { 216 | __typename 217 | name 218 | friends { 219 | __typename 220 | name 221 | ... on Dog { 222 | barks 223 | } 224 | ... on Cat { 225 | meows 226 | } 227 | } 228 | } 229 | """ 230 | 231 | {:ok, result} = execute(schema, query, root_value: john, validate: false) 232 | assert_data(result, 233 | %{"__typename" => "Person", 234 | "friends" => [ 235 | %{"__typename" => "Person", "name" => "Liz"}, 236 | %{"__typename" => "Dog", "barks" => true, "name" => "Odie"} 237 | ], 238 | "name" => "John" 239 | } 240 | ) 241 | end 242 | 243 | test "allows fragment conditions to be abstract types" do 244 | query = """ 245 | { 246 | __typename 247 | name 248 | pets { ...PetFields } 249 | friends { ...FriendFields } 250 | } 251 | 252 | fragment PetFields on Pet { 253 | __typename 254 | ... on Dog { 255 | name 256 | barks 257 | } 258 | ... on Cat { 259 | name 260 | meows 261 | } 262 | } 263 | 264 | fragment FriendFields on Named { 265 | __typename 266 | name 267 | ... on Dog { 268 | barks 269 | } 270 | ... on Cat { 271 | meows 272 | } 273 | } 274 | """ 275 | 276 | {:ok, result} = execute(schema, query, root_value: john, validate: false) 277 | assert_data(result, 278 | %{"__typename" => "Person", 279 | "name" => "John", 280 | "friends" => [ 281 | %{"__typename" => "Person", "name" => "Liz"}, 282 | %{"__typename" => "Dog", "barks" => true, "name" => "Odie"} 283 | ], 284 | "pets" => [ 285 | %{"__typename" => "Dog", "barks" => true, "name" => "Odie"}, 286 | %{"__typename" => "Cat", "meows" => false, "name" => "Garfield"} 287 | ] 288 | } 289 | ) 290 | end 291 | 292 | test "gets execution info in resolver" do 293 | 294 | end 295 | 296 | end 297 | -------------------------------------------------------------------------------- /docs/graphql-js_ast.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /** 3 | * Copyright (c) 2015, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. An additional grant 8 | * of patent rights can be found in the PATENTS file in the same directory. 9 | */ 10 | 11 | import type { Source } from './source'; 12 | 13 | 14 | /** 15 | * Contains a range of UTF-8 character offsets that identify 16 | * the region of the source from which the AST derived. 17 | */ 18 | export type Location = { 19 | start: number; 20 | end: number; 21 | source?: ?Source 22 | } 23 | 24 | /** 25 | * The list of all possible AST node types. 26 | */ 27 | export type Node = Name 28 | | Document 29 | | OperationDefinition 30 | | VariableDefinition 31 | | Variable 32 | | SelectionSet 33 | | Field 34 | | Argument 35 | | FragmentSpread 36 | | InlineFragment 37 | | FragmentDefinition 38 | | IntValue 39 | | FloatValue 40 | | StringValue 41 | | BooleanValue 42 | | EnumValue 43 | | ListValue 44 | | ObjectValue 45 | | ObjectField 46 | | Directive 47 | | ListType 48 | | NonNullType 49 | | ObjectTypeDefinition 50 | | FieldDefinition 51 | | InputValueDefinition 52 | | InterfaceTypeDefinition 53 | | UnionTypeDefinition 54 | | ScalarTypeDefinition 55 | | EnumTypeDefinition 56 | | EnumValueDefinition 57 | | InputObjectTypeDefinition 58 | | TypeExtensionDefinition 59 | 60 | // Name 61 | 62 | export type Name = { 63 | kind: 'Name'; 64 | loc?: ?Location; 65 | value: string; 66 | } 67 | 68 | // Document 69 | 70 | export type Document = { 71 | kind: 'Document'; 72 | loc?: ?Location; 73 | definitions: Array; 74 | } 75 | 76 | export type Definition = OperationDefinition 77 | | FragmentDefinition 78 | | TypeDefinition 79 | 80 | export type OperationDefinition = { 81 | kind: 'OperationDefinition'; 82 | loc?: ?Location; 83 | // Note: subscription is an experimental non-spec addition. 84 | operation: 'query' | 'mutation' | 'subscription'; 85 | name?: ?Name; 86 | variableDefinitions?: ?Array; 87 | directives?: ?Array; 88 | selectionSet: SelectionSet; 89 | } 90 | 91 | export type VariableDefinition = { 92 | kind: 'VariableDefinition'; 93 | loc?: ?Location; 94 | variable: Variable; 95 | type: Type; 96 | defaultValue?: ?Value; 97 | } 98 | 99 | export type Variable = { 100 | kind: 'Variable'; 101 | loc?: ?Location; 102 | name: Name; 103 | } 104 | 105 | export type SelectionSet = { 106 | kind: 'SelectionSet'; 107 | loc?: ?Location; 108 | selections: Array; 109 | } 110 | 111 | export type Selection = Field 112 | | FragmentSpread 113 | | InlineFragment 114 | 115 | export type Field = { 116 | kind: 'Field'; 117 | loc?: ?Location; 118 | alias?: ?Name; 119 | name: Name; 120 | arguments?: ?Array; 121 | directives?: ?Array; 122 | selectionSet?: ?SelectionSet; 123 | } 124 | 125 | export type Argument = { 126 | kind: 'Argument'; 127 | loc?: ?Location; 128 | name: Name; 129 | value: Value; 130 | } 131 | 132 | 133 | // Fragments 134 | 135 | export type FragmentSpread = { 136 | kind: 'FragmentSpread'; 137 | loc?: ?Location; 138 | name: Name; 139 | directives?: ?Array; 140 | } 141 | 142 | export type InlineFragment = { 143 | kind: 'InlineFragment'; 144 | loc?: ?Location; 145 | typeCondition: NamedType; 146 | directives?: ?Array; 147 | selectionSet: SelectionSet; 148 | } 149 | 150 | export type FragmentDefinition = { 151 | kind: 'FragmentDefinition'; 152 | loc?: ?Location; 153 | name: Name; 154 | typeCondition: NamedType; 155 | directives?: ?Array; 156 | selectionSet: SelectionSet; 157 | } 158 | 159 | 160 | // Values 161 | 162 | export type Value = Variable 163 | | IntValue 164 | | FloatValue 165 | | StringValue 166 | | BooleanValue 167 | | EnumValue 168 | | ListValue 169 | | ObjectValue 170 | 171 | export type IntValue = { 172 | kind: 'IntValue'; 173 | loc?: ?Location; 174 | value: string; 175 | } 176 | 177 | export type FloatValue = { 178 | kind: 'FloatValue'; 179 | loc?: ?Location; 180 | value: string; 181 | } 182 | 183 | export type StringValue = { 184 | kind: 'StringValue'; 185 | loc?: ?Location; 186 | value: string; 187 | } 188 | 189 | export type BooleanValue = { 190 | kind: 'BooleanValue'; 191 | loc?: ?Location; 192 | value: boolean; 193 | } 194 | 195 | export type EnumValue = { 196 | kind: 'EnumValue'; 197 | loc?: ?Location; 198 | value: string; 199 | } 200 | 201 | export type ListValue = { 202 | kind: 'ListValue'; 203 | loc?: ?Location; 204 | values: Array; 205 | } 206 | 207 | export type ObjectValue = { 208 | kind: 'ObjectValue'; 209 | loc?: ?Location; 210 | fields: Array; 211 | } 212 | 213 | export type ObjectField = { 214 | kind: 'ObjectField'; 215 | loc?: ?Location; 216 | name: Name; 217 | value: Value; 218 | } 219 | 220 | 221 | // Directives 222 | 223 | export type Directive = { 224 | kind: 'Directive'; 225 | loc?: ?Location; 226 | name: Name; 227 | arguments?: ?Array; 228 | } 229 | 230 | 231 | // Type Reference 232 | 233 | export type Type = NamedType 234 | | ListType 235 | | NonNullType 236 | 237 | export type NamedType = { 238 | kind: 'NamedType'; 239 | loc?: ?Location; 240 | name: Name; 241 | }; 242 | 243 | export type ListType = { 244 | kind: 'ListType'; 245 | loc?: ?Location; 246 | type: Type; 247 | } 248 | 249 | export type NonNullType = { 250 | kind: 'NonNullType'; 251 | loc?: ?Location; 252 | type: NamedType | ListType; 253 | } 254 | 255 | // Type Definition 256 | 257 | export type TypeDefinition = ObjectTypeDefinition 258 | | InterfaceTypeDefinition 259 | | UnionTypeDefinition 260 | | ScalarTypeDefinition 261 | | EnumTypeDefinition 262 | | InputObjectTypeDefinition 263 | | TypeExtensionDefinition 264 | 265 | export type ObjectTypeDefinition = { 266 | kind: 'ObjectTypeDefinition'; 267 | loc?: ?Location; 268 | name: Name; 269 | interfaces?: ?Array; 270 | fields: Array; 271 | } 272 | 273 | export type FieldDefinition = { 274 | kind: 'FieldDefinition'; 275 | loc?: ?Location; 276 | name: Name; 277 | arguments: Array; 278 | type: Type; 279 | } 280 | 281 | export type InputValueDefinition = { 282 | kind: 'InputValueDefinition'; 283 | loc?: ?Location; 284 | name: Name; 285 | type: Type; 286 | defaultValue?: ?Value; 287 | } 288 | 289 | export type InterfaceTypeDefinition = { 290 | kind: 'InterfaceTypeDefinition'; 291 | loc?: ?Location; 292 | name: Name; 293 | fields: Array; 294 | } 295 | 296 | export type UnionTypeDefinition = { 297 | kind: 'UnionTypeDefinition'; 298 | loc?: ?Location; 299 | name: Name; 300 | types: Array; 301 | } 302 | 303 | export type ScalarTypeDefinition = { 304 | kind: 'ScalarTypeDefinition'; 305 | loc?: ?Location; 306 | name: Name; 307 | } 308 | 309 | export type EnumTypeDefinition = { 310 | kind: 'EnumTypeDefinition'; 311 | loc?: ?Location; 312 | name: Name; 313 | values: Array; 314 | } 315 | 316 | export type EnumValueDefinition = { 317 | kind: 'EnumValueDefinition'; 318 | loc?: ?Location; 319 | name: Name; 320 | } 321 | 322 | export type InputObjectTypeDefinition = { 323 | kind: 'InputObjectTypeDefinition'; 324 | loc?: ?Location; 325 | name: Name; 326 | fields: Array; 327 | } 328 | 329 | export type TypeExtensionDefinition = { 330 | kind: 'TypeExtensionDefinition'; 331 | loc?: ?Location; 332 | definition: ObjectTypeDefinition; 333 | } 334 | --------------------------------------------------------------------------------