graphql 0.9.5 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/base_type.rb +24 -0
  3. data/lib/graphql/definition_helpers/defined_by_config.rb +5 -4
  4. data/lib/graphql/definition_helpers/non_null_with_bang.rb +1 -1
  5. data/lib/graphql/definition_helpers/type_definer.rb +1 -1
  6. data/lib/graphql/enum_type.rb +16 -3
  7. data/lib/graphql/input_object_type.rb +10 -0
  8. data/lib/graphql/language.rb +0 -5
  9. data/lib/graphql/language/nodes.rb +79 -75
  10. data/lib/graphql/language/parser.rb +109 -106
  11. data/lib/graphql/language/transform.rb +100 -91
  12. data/lib/graphql/language/visitor.rb +78 -74
  13. data/lib/graphql/list_type.rb +5 -0
  14. data/lib/graphql/non_null_type.rb +6 -2
  15. data/lib/graphql/query.rb +58 -9
  16. data/lib/graphql/query/arguments.rb +29 -26
  17. data/lib/graphql/query/base_execution/value_resolution.rb +3 -3
  18. data/lib/graphql/query/directive_chain.rb +1 -1
  19. data/lib/graphql/query/executor.rb +6 -27
  20. data/lib/graphql/query/literal_input.rb +89 -0
  21. data/lib/graphql/query/ruby_input.rb +20 -0
  22. data/lib/graphql/query/serial_execution/field_resolution.rb +1 -1
  23. data/lib/graphql/query/variables.rb +39 -0
  24. data/lib/graphql/scalar_type.rb +27 -5
  25. data/lib/graphql/schema.rb +5 -0
  26. data/lib/graphql/schema/type_expression.rb +28 -0
  27. data/lib/graphql/static_validation/literal_validator.rb +5 -2
  28. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +0 -2
  29. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +1 -1
  30. data/lib/graphql/version.rb +1 -1
  31. data/readme.md +3 -2
  32. data/spec/graphql/enum_type_spec.rb +7 -2
  33. data/spec/graphql/input_object_type_spec.rb +45 -0
  34. data/spec/graphql/language/parser_spec.rb +2 -1
  35. data/spec/graphql/language/transform_spec.rb +5 -0
  36. data/spec/graphql/query/base_execution/value_resolution_spec.rb +46 -0
  37. data/spec/graphql/query/context_spec.rb +37 -0
  38. data/spec/graphql/query/executor_spec.rb +33 -0
  39. data/spec/graphql/query_spec.rb +110 -26
  40. data/spec/graphql/schema/type_expression_spec.rb +38 -0
  41. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +2 -2
  42. data/spec/support/dairy_app.rb +17 -17
  43. data/spec/support/dairy_data.rb +2 -2
  44. metadata +12 -2
@@ -1,34 +1,37 @@
1
- # Provide read-only access to arguments by string or symbol names.
2
- class GraphQL::Query::Arguments
3
- extend Forwardable
1
+ module GraphQL
2
+ class Query
3
+ # Read-only access to values, normalizing all keys to strings
4
+ #
5
+ # {Arguments} recursively wraps the input in {Arguments} instances.
6
+ class Arguments
7
+ extend Forwardable
4
8
 
5
- def initialize(ast_arguments, argument_hash, variables)
6
- @hash = ast_arguments.reduce({}) do |memo, arg|
7
- arg_defn = argument_hash[arg.name]
8
- value = reduce_value(arg.value, arg_defn, variables)
9
- memo[arg.name] = value
10
- memo
11
- end
12
- end
9
+ def initialize(values)
10
+ @values = values.inject({}) do |memo, (inner_key, inner_value)|
11
+ memo[inner_key] = wrap_value(inner_value)
12
+ memo
13
+ end
14
+ end
13
15
 
14
- def_delegators :@hash, :keys, :values, :inspect, :to_h
16
+ # @param [String, Symbol] name or index of value to access
17
+ # @return [Object] the argument at that key
18
+ def [](key)
19
+ @values[key.to_s]
20
+ end
15
21
 
16
- def [](key)
17
- @hash[key.to_s]
18
- end
22
+ def_delegators :@values_hash, :keys, :values, :each
19
23
 
20
- private
24
+ private
21
25
 
22
- def reduce_value(value, arg_defn, variables)
23
- if value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
24
- value = variables[value.name]
25
- elsif value.is_a?(GraphQL::Language::Nodes::Enum)
26
- value = arg_defn.type.coerce(value.name)
27
- elsif value.is_a?(GraphQL::Language::Nodes::InputObject)
28
- wrapped_type = arg_defn.type.unwrap
29
- value = self.class.new(value.pairs, wrapped_type.input_fields, variables)
30
- else
31
- value
26
+ def wrap_value(value)
27
+ if value.is_a?(Array)
28
+ value.map { |item| wrap_value(item) }
29
+ elsif value.is_a?(Hash)
30
+ self.class.new(value)
31
+ else
32
+ value
33
+ end
34
+ end
32
35
  end
33
36
  end
34
37
  end
@@ -29,9 +29,9 @@ module GraphQL
29
29
  end
30
30
 
31
31
  class ScalarResolution < BaseResolution
32
- # Apply the scalar's defined `coerce` method to the value
32
+ # Apply the scalar's defined `coerce_result` method to the value
33
33
  def result
34
- field_type.coerce(value)
34
+ field_type.coerce_result(value)
35
35
  end
36
36
  end
37
37
 
@@ -60,7 +60,7 @@ module GraphQL
60
60
  class EnumResolution < BaseResolution
61
61
  # Get the string name for this enum value
62
62
  def result
63
- value.to_s
63
+ field_type.coerce_result(value)
64
64
  end
65
65
  end
66
66
 
@@ -30,7 +30,7 @@ class GraphQL::Query::DirectiveChain
30
30
  @result = block.call
31
31
  else
32
32
  applicable_directives.map do |(ast_directive, directive)|
33
- args = GraphQL::Query::Arguments.new(ast_directive.arguments, directive.arguments, query.variables)
33
+ args = GraphQL::Query::LiteralInput.from_arguments(ast_directive.arguments, directive.arguments, query.variables)
34
34
  @result = directive.resolve(args, block)
35
35
  end
36
36
  @result ||= {}
@@ -1,23 +1,11 @@
1
1
  module GraphQL
2
2
  class Query
3
3
  class Executor
4
- class OperationNameMissingError < StandardError
5
- def initialize(names)
6
- msg = "You must provide an operation name from: #{names.join(", ")}"
7
- super(msg)
8
- end
9
- end
10
-
11
4
  # @return [GraphQL::Query] the query being executed
12
5
  attr_reader :query
13
6
 
14
- # @return [String] the operation to run in {query}
15
- attr_reader :operation_name
16
-
17
-
18
- def initialize(query, operation_name)
7
+ def initialize(query)
19
8
  @query = query
20
- @operation_name = operation_name
21
9
  end
22
10
 
23
11
  # Evalute {operation_name} on {query}. Handle errors by putting them in the "errors" key.
@@ -25,19 +13,20 @@ module GraphQL
25
13
  # @return [Hash] A GraphQL response, with either a "data" key or an "errors" key
26
14
  def result
27
15
  execute
28
- rescue OperationNameMissingError => err
16
+ rescue GraphQL::Query::OperationNameMissingError, GraphQL::Query::VariableMissingError => err
29
17
  {"errors" => [{"message" => err.message}]}
30
18
  rescue StandardError => err
31
19
  query.debug && raise(err)
32
- message = "Something went wrong during query execution: #{err}" # \n #{err.backtrace.join("\n ")}"
20
+ message = "Something went wrong during query execution: #{err}" #\n#{err.backtrace.join("\n ")}"
33
21
  {"errors" => [{"message" => message}]}
34
22
  end
35
23
 
36
24
  private
37
25
 
38
26
  def execute
39
- return {} if query.operations.none?
40
- operation = find_operation(operation_name, query.operations)
27
+ operation = query.selected_operation
28
+ return {} if operation.nil?
29
+
41
30
  if operation.operation_type == "query"
42
31
  root_type = query.schema.query
43
32
  execution_strategy_class = query.schema.query_execution_strategy
@@ -57,16 +46,6 @@ module GraphQL
57
46
 
58
47
  result
59
48
  end
60
-
61
- def find_operation(operation_name, operations)
62
- if operations.length == 1
63
- operations.values.first
64
- elsif !operations.key?(operation_name)
65
- raise OperationNameMissingError, operations.keys
66
- else
67
- operations[operation_name]
68
- end
69
- end
70
49
  end
71
50
  end
72
51
  end
@@ -0,0 +1,89 @@
1
+ module GraphQL
2
+ class Query
3
+ # Turn query string values into something useful for query execution
4
+ class LiteralInput
5
+ attr_reader :variables, :value, :type
6
+ def initialize(type, incoming_value, variables)
7
+ @type = type
8
+ @value = incoming_value
9
+ @variables = variables
10
+ end
11
+
12
+ def graphql_value
13
+ if value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
14
+ variables[value.name] # Already cleaned up with RubyInput
15
+ elsif type.kind.input_object?
16
+ input_values = {}
17
+ inner_type = type.unwrap
18
+ inner_type.input_fields.each do |arg_name, arg_defn|
19
+ ast_arg = value.pairs.find { |ast_arg| ast_arg.name == arg_name }
20
+ raw_value = resolve_argument_value(ast_arg, arg_defn, variables)
21
+ reduced_value = coerce(arg_defn.type, raw_value, variables)
22
+ input_values[arg_name] = reduced_value
23
+ end
24
+ input_values
25
+ elsif type.kind.list?
26
+ inner_type = type.of_type
27
+ value.map { |item| coerce(inner_type, item, variables) }
28
+ elsif type.kind.non_null?
29
+ inner_type = type.of_type
30
+ coerce(inner_type, value, variables)
31
+ elsif type.kind.scalar?
32
+ type.coerce_input!(value)
33
+ elsif type.kind.enum?
34
+ value_name = value.name # it's a Nodes::Enum
35
+ type.coerce_input!(value_name)
36
+ else
37
+ raise "Unknown input #{value} of type #{type}"
38
+ end
39
+ end
40
+
41
+ def self.coerce(type, value, variables)
42
+ input = self.new(type, value, variables)
43
+ input.graphql_value
44
+ end
45
+
46
+ def self.from_arguments(ast_arguments, argument_defns, variables)
47
+ values_hash = {}
48
+ argument_defns.each do |arg_name, arg_defn|
49
+ ast_arg = ast_arguments.find { |ast_arg| ast_arg.name == arg_name }
50
+ arg_value = nil
51
+ if ast_arg
52
+ arg_value = coerce(arg_defn.type, ast_arg.value, variables)
53
+ end
54
+ if arg_value.nil?
55
+ arg_value = arg_defn.default_value
56
+ end
57
+ values_hash[arg_name] = arg_value
58
+ end
59
+ GraphQL::Query::Arguments.new(values_hash)
60
+ end
61
+
62
+ private
63
+
64
+ def coerce(*args)
65
+ self.class.coerce(*args)
66
+ end
67
+
68
+ def resolve_argument_value(*args)
69
+ self.class.resolve_argument_value(*args)
70
+ end
71
+
72
+ # Prefer values in this order:
73
+ # - Literal value from the query string
74
+ # - Variable value from query varibles
75
+ # - Default value from Argument definition
76
+ def self.resolve_argument_value(ast_arg, arg_defn, variables)
77
+ if !ast_arg.nil?
78
+ raw_value = ast_arg.value
79
+ end
80
+
81
+ if raw_value.nil?
82
+ raw_value = arg_defn.default_value
83
+ end
84
+
85
+ raw_value
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,20 @@
1
+ module GraphQL
2
+ class Query
3
+ # Turn Ruby values into something useful for query execution
4
+ class RubyInput
5
+ def initialize(type, incoming_value)
6
+ @type = type
7
+ @incoming_value = incoming_value
8
+ end
9
+
10
+ def graphql_value
11
+ @type.coerce_input!(@incoming_value)
12
+ end
13
+
14
+ def self.coerce(type, value)
15
+ input = self.new(type, value)
16
+ input.graphql_value
17
+ end
18
+ end
19
+ end
20
+ end
@@ -11,7 +11,7 @@ module GraphQL
11
11
  @query = query
12
12
  @execution_strategy = execution_strategy
13
13
  @field = query.schema.get_field(parent_type, ast_node.name) || raise("No field found on #{parent_type.name} '#{parent_type}' for '#{ast_node.name}'")
14
- @arguments = GraphQL::Query::Arguments.new(ast_node.arguments, field.arguments, query.variables)
14
+ @arguments = GraphQL::Query::LiteralInput.from_arguments(ast_node.arguments, field.arguments, query.variables)
15
15
  end
16
16
 
17
17
  def result
@@ -0,0 +1,39 @@
1
+ module GraphQL
2
+ class Query
3
+ # Read-only access to query variables, applying default values if needed.
4
+ class Variables
5
+ def initialize(schema, ast_variables, provided_variables)
6
+ @schema = schema
7
+ @provided_variables = provided_variables
8
+ @storage = ast_variables.each_with_object({}) do |ast_variable, memo|
9
+ variable_name = ast_variable.name
10
+ memo[variable_name] = get_graphql_value(ast_variable)
11
+ end
12
+ end
13
+
14
+ def [](key)
15
+ @storage.fetch(key)
16
+ end
17
+
18
+ private
19
+
20
+ # Find the right value for this variable:
21
+ # - First, use the value provided at runtime
22
+ # - Then, fall back to the default value from the query string
23
+ # If it's still nil, raise an error if it's required.
24
+ def get_graphql_value(ast_variable)
25
+ variable_type = @schema.type_from_ast(ast_variable.type)
26
+ variable_name = ast_variable.name
27
+ default_value = ast_variable.default_value
28
+ provided_value = @provided_variables[variable_name]
29
+ if !provided_value.nil?
30
+ graphql_value = GraphQL::Query::RubyInput.coerce(variable_type, provided_value)
31
+ elsif !default_value.nil?
32
+ graphql_value = GraphQL::Query::LiteralInput.coerce(variable_type, default_value, {})
33
+ elsif variable_type.kind.non_null?
34
+ raise GraphQL::Query::VariableMissingError.new(variable_name, variable_type)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,16 +1,38 @@
1
1
  module GraphQL
2
2
  # The parent type for scalars, eg {GraphQL::STRING_TYPE}, {GraphQL::INT_TYPE}
3
3
  #
4
+ # @example defining a type for Time
5
+ # TimeType = GraphQL::ObjectType.define do
6
+ # name "Time"
7
+ # description "Time since epoch in seconds"
8
+ #
9
+ # coerce_input ->(value) { Time.at(Float(value)) }
10
+ # coerce_result ->(value) { value.to_f }
11
+ # end
12
+ #
4
13
  class ScalarType < GraphQL::BaseType
5
- defined_by_config :name, :coerce, :description
14
+ defined_by_config :name, :coerce, :coerce_input, :coerce_result, :description
6
15
  attr_accessor :name, :description
7
16
 
8
- def coerce(value)
9
- @coerce_proc.call(value)
17
+ def coerce=(proc)
18
+ self.coerce_input = proc
19
+ self.coerce_result = proc
10
20
  end
11
21
 
12
- def coerce=(proc)
13
- @coerce_proc = proc
22
+ def coerce_input(value)
23
+ @coerce_input_proc.call(value)
24
+ end
25
+
26
+ def coerce_input=(proc)
27
+ @coerce_input_proc = proc unless proc.nil?
28
+ end
29
+
30
+ def coerce_result(value)
31
+ @coerce_result_proc.call(value)
32
+ end
33
+
34
+ def coerce_result=(proc)
35
+ @coerce_result_proc = proc unless proc.nil?
14
36
  end
15
37
 
16
38
  def kind
@@ -58,6 +58,10 @@ class GraphQL::Schema
58
58
  end
59
59
  end
60
60
 
61
+ def type_from_ast(ast_node)
62
+ GraphQL::Schema::TypeExpression.new(self, ast_node).type
63
+ end
64
+
61
65
  class InvalidTypeError < StandardError
62
66
  def initialize(type, errors)
63
67
  super("Type #{type.respond_to?(:name) ? type.name : "Unnamed type" } is invalid: #{errors.join(", ")}")
@@ -70,6 +74,7 @@ require 'graphql/schema/field_validator'
70
74
  require 'graphql/schema/implementation_validator'
71
75
  require 'graphql/schema/middleware_chain'
72
76
  require 'graphql/schema/rescue_middleware'
77
+ require 'graphql/schema/type_expression'
73
78
  require 'graphql/schema/type_reducer'
74
79
  require 'graphql/schema/type_map'
75
80
  require 'graphql/schema/type_validator'
@@ -0,0 +1,28 @@
1
+ module GraphQL
2
+ class Schema
3
+ class TypeExpression
4
+ def initialize(schema, ast_node)
5
+ @schema = schema
6
+ @ast_node = ast_node
7
+ end
8
+ def type
9
+ @type ||= build_type(@schema, @ast_node)
10
+ end
11
+
12
+ private
13
+
14
+ def build_type(schema, ast_node)
15
+ if ast_node.is_a?(GraphQL::Language::Nodes::TypeName)
16
+ type_name = ast_node.name
17
+ schema.types[type_name]
18
+ elsif ast_node.is_a?(GraphQL::Language::Nodes::NonNullType)
19
+ ast_inner_type = ast_node.of_type
20
+ build_type(schema, ast_inner_type).to_non_null_type
21
+ elsif ast_node.is_a?(GraphQL::Language::Nodes::ListType)
22
+ ast_inner_type = ast_node.of_type
23
+ build_type(schema, ast_inner_type).to_list_type
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -7,9 +7,9 @@ class GraphQL::StaticValidation::LiteralValidator
7
7
  item_type = type.of_type
8
8
  ast_value.all? { |val| validate(val, item_type) }
9
9
  elsif type.kind.scalar?
10
- !type.coerce(ast_value).nil?
10
+ !type.coerce_input(ast_value).nil?
11
11
  elsif type.kind.enum? && ast_value.is_a?(GraphQL::Language::Nodes::Enum)
12
- !type.coerce(ast_value.name).nil?
12
+ !type.coerce_input(ast_value.name).nil?
13
13
  elsif type.kind.input_object? && ast_value.is_a?(GraphQL::Language::Nodes::InputObject)
14
14
  fields = type.input_fields
15
15
  ast_value.pairs.all? do |value|
@@ -17,6 +17,9 @@ class GraphQL::StaticValidation::LiteralValidator
17
17
  present_if_required = field_type.kind.non_null? ? !value.nil? : true
18
18
  present_if_required && validate(value.value, field_type)
19
19
  end
20
+ elsif ast_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
21
+ # Todo: somehow pass in the document's variable definitions and validate this
22
+ true
20
23
  else
21
24
  false
22
25
  end
@@ -1,8 +1,6 @@
1
1
  class GraphQL::StaticValidation::FieldsAreDefinedOnType
2
2
  include GraphQL::StaticValidation::Message::MessageHelper
3
3
 
4
- IS_FIELD = Proc.new {|f| f.is_a?(GraphQL::Language::Nodes::Field) }
5
-
6
4
  def validate(context)
7
5
  visitor = context.visitor
8
6
  visitor[GraphQL::Language::Nodes::Field] << -> (node, parent) {