graphql 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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) {