graphql 0.2.0 → 0.3.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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graph_ql/{types → definition_helpers}/argument_definer.rb +0 -0
  3. data/lib/graph_ql/definition_helpers/definable.rb +18 -0
  4. data/lib/graph_ql/{types → definition_helpers}/field_definer.rb +0 -0
  5. data/lib/graph_ql/definition_helpers/forwardable.rb +10 -0
  6. data/lib/graph_ql/definition_helpers/non_null_with_bang.rb +9 -0
  7. data/lib/graph_ql/definition_helpers/string_named_hash.rb +12 -0
  8. data/lib/graph_ql/{types → definition_helpers}/type_definer.rb +1 -0
  9. data/lib/graph_ql/directive.rb +9 -5
  10. data/lib/graph_ql/directives/directive_chain.rb +1 -1
  11. data/lib/graph_ql/field.rb +1 -5
  12. data/lib/graph_ql/parser/transform.rb +1 -3
  13. data/lib/graph_ql/query.rb +14 -3
  14. data/lib/graph_ql/query/arguments.rb +6 -5
  15. data/lib/graph_ql/query/field_resolution_strategy.rb +3 -3
  16. data/lib/graph_ql/{types → scalars}/boolean_type.rb +0 -0
  17. data/lib/graph_ql/{types → scalars}/float_type.rb +1 -1
  18. data/lib/graph_ql/scalars/id_type.rb +6 -0
  19. data/lib/graph_ql/{types → scalars}/int_type.rb +0 -0
  20. data/lib/graph_ql/{types → scalars}/scalar_type.rb +0 -4
  21. data/lib/graph_ql/{types → scalars}/string_type.rb +0 -0
  22. data/lib/graph_ql/static_validation.rb +7 -10
  23. data/lib/graph_ql/static_validation/all_rules.rb +23 -0
  24. data/lib/graph_ql/static_validation/literal_validator.rb +0 -3
  25. data/lib/graph_ql/static_validation/{argument_literals_are_compatible.rb → rules/argument_literals_are_compatible.rb} +0 -0
  26. data/lib/graph_ql/static_validation/{arguments_are_defined.rb → rules/arguments_are_defined.rb} +5 -0
  27. data/lib/graph_ql/static_validation/{directives_are_defined.rb → rules/directives_are_defined.rb} +0 -0
  28. data/lib/graph_ql/static_validation/{fields_are_defined_on_type.rb → rules/fields_are_defined_on_type.rb} +0 -0
  29. data/lib/graph_ql/static_validation/{fields_have_appropriate_selections.rb → rules/fields_have_appropriate_selections.rb} +0 -0
  30. data/lib/graph_ql/static_validation/{fields_will_merge.rb → rules/fields_will_merge.rb} +13 -5
  31. data/lib/graph_ql/static_validation/rules/fragment_spreads_are_possible.rb +50 -0
  32. data/lib/graph_ql/static_validation/{fragment_types_exist.rb → rules/fragment_types_exist.rb} +0 -0
  33. data/lib/graph_ql/static_validation/rules/fragments_are_finite.rb +23 -0
  34. data/lib/graph_ql/static_validation/rules/fragments_are_on_composite_types.rb +27 -0
  35. data/lib/graph_ql/static_validation/{fragments_are_used.rb → rules/fragments_are_used.rb} +0 -0
  36. data/lib/graph_ql/static_validation/{required_arguments_are_present.rb → rules/required_arguments_are_present.rb} +0 -0
  37. data/lib/graph_ql/static_validation/rules/variable_default_values_are_correctly_typed.rb +24 -0
  38. data/lib/graph_ql/static_validation/rules/variable_usages_are_allowed.rb +47 -0
  39. data/lib/graph_ql/static_validation/rules/variables_are_input_types.rb +27 -0
  40. data/lib/graph_ql/static_validation/rules/variables_are_used_and_defined.rb +31 -0
  41. data/lib/graph_ql/static_validation/type_stack.rb +13 -1
  42. data/lib/graph_ql/static_validation/validator.rb +14 -18
  43. data/lib/graph_ql/type_kinds.rb +8 -4
  44. data/lib/graph_ql/{enum.rb → types/enum.rb} +7 -6
  45. data/lib/graph_ql/types/input_object_type.rb +3 -10
  46. data/lib/graph_ql/{interface.rb → types/interface.rb} +0 -4
  47. data/lib/graph_ql/types/list_type.rb +0 -4
  48. data/lib/graph_ql/types/non_null_type.rb +0 -4
  49. data/lib/graph_ql/types/object_type.rb +28 -13
  50. data/lib/graph_ql/{union.rb → types/union.rb} +0 -4
  51. data/lib/graph_ql/version.rb +1 -1
  52. data/lib/graphql.rb +9 -40
  53. data/readme.md +26 -20
  54. data/spec/graph_ql/directive_spec.rb +4 -4
  55. data/spec/graph_ql/introspection/directive_type_spec.rb +2 -2
  56. data/spec/graph_ql/introspection/schema_type_spec.rb +3 -2
  57. data/spec/graph_ql/introspection/type_type_spec.rb +7 -7
  58. data/spec/graph_ql/parser/parser_spec.rb +6 -2
  59. data/spec/graph_ql/query_spec.rb +26 -8
  60. data/spec/graph_ql/scalars/id_type_spec.rb +24 -0
  61. data/spec/graph_ql/schema/type_reducer_spec.rb +1 -0
  62. data/spec/graph_ql/schema/type_validator_spec.rb +1 -1
  63. data/spec/graph_ql/static_validation/{argument_literals_are_compatible_spec.rb → rules/argument_literals_are_compatible_spec.rb} +1 -1
  64. data/spec/graph_ql/static_validation/{arguments_are_defined_spec.rb → rules/arguments_are_defined_spec.rb} +1 -1
  65. data/spec/graph_ql/static_validation/{directives_are_defined_spec.rb → rules/directives_are_defined_spec.rb} +1 -1
  66. data/spec/graph_ql/static_validation/{fields_are_defined_on_type_spec.rb → rules/fields_are_defined_on_type_spec.rb} +1 -1
  67. data/spec/graph_ql/static_validation/{fields_have_appropriate_selections_spec.rb → rules/fields_have_appropriate_selections_spec.rb} +1 -1
  68. data/spec/graph_ql/static_validation/{fields_will_merge_spec.rb → rules/fields_will_merge_spec.rb} +1 -1
  69. data/spec/graph_ql/static_validation/rules/fragment_spreads_are_possible_spec.rb +46 -0
  70. data/spec/graph_ql/static_validation/{fragment_types_exist_spec.rb → rules/fragment_types_exist_spec.rb} +1 -1
  71. data/spec/graph_ql/static_validation/rules/fragments_are_finite_spec.rb +43 -0
  72. data/spec/graph_ql/static_validation/rules/fragments_are_on_composite_types_spec.rb +48 -0
  73. data/spec/graph_ql/static_validation/{fragments_are_used_spec.rb → rules/fragments_are_used_spec.rb} +1 -1
  74. data/spec/graph_ql/static_validation/{required_arguments_are_present_spec.rb → rules/required_arguments_are_present_spec.rb} +1 -1
  75. data/spec/graph_ql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +43 -0
  76. data/spec/graph_ql/static_validation/rules/variable_usages_are_allowed_spec.rb +45 -0
  77. data/spec/graph_ql/static_validation/rules/variables_are_input_types_spec.rb +36 -0
  78. data/spec/graph_ql/static_validation/rules/variables_are_used_and_defined_spec.rb +44 -0
  79. data/spec/graph_ql/static_validation/type_stack_spec.rb +2 -2
  80. data/spec/graph_ql/static_validation/validator_spec.rb +44 -5
  81. data/spec/graph_ql/types/enum_spec.rb +10 -0
  82. data/spec/graph_ql/{interface_spec.rb → types/interface_spec.rb} +1 -1
  83. data/spec/graph_ql/types/object_type_spec.rb +13 -0
  84. data/spec/graph_ql/{union_spec.rb → types/union_spec.rb} +0 -0
  85. data/spec/support/dummy_app.rb +7 -6
  86. data/spec/support/dummy_data.rb +3 -3
  87. metadata +74 -46
  88. data/lib/graph_ql/types/non_null_with_bang.rb +0 -5
  89. data/spec/graph_ql/enum_spec.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 45c3f35aae5e941e7bad2109eb9405e6afc081cc
4
- data.tar.gz: 3cd3d8dd85a88e341552d0709aff11361937d157
3
+ metadata.gz: 9cf6add0a41fbbf194c90bd12da917467ae612d7
4
+ data.tar.gz: b0e7b7f03eb0e6a9def2f7402f3349e69d07ec33
5
5
  SHA512:
6
- metadata.gz: 274f08f78112de84f94dcb9fcdcc62da63a36a29000aad2055cf5e353327bb9ba3f673fe3e83c3a3c6405b360847f17104ab97c66a08323400f6df3f27f6044a
7
- data.tar.gz: 82f47b290d9779b1214bb59857edb079369932d08686a8ac1af47c15ab1f3e549e4c8f9f269947836fe2093fc49badc6986e397466cb2d1010520bc3a765e7ff
6
+ metadata.gz: f5d0f097d277cdd8e40976ce87ef5d57edf365c2fadf821de5c90d7dad9f9d7819a21291c6b01a83ab05e1d7850617bbbe0382c492776b04e5fa619e186e8caa
7
+ data.tar.gz: ee252ea72993ee9c07ce9d69f6f25c0b6d13e9f7f2bf7cbbb419d8d2b01f48a9c7e57310c6cc05e051c833ee72b42494399bd5252813a4641d1ba3e88be17d16
@@ -0,0 +1,18 @@
1
+ # Define attributes which can be assigned & read, or
2
+ # "defined", by passing the new value as an argument
3
+ #
4
+ # @example defining an object's name
5
+ # object.name("New name")
6
+ #
7
+ module GraphQL::Definable
8
+ def attr_definable(*names)
9
+ attr_accessor(*names)
10
+ names.each do |name|
11
+ ivar_name = "@#{name}".to_sym
12
+ define_method(name) do |new_value=nil|
13
+ new_value && self.instance_variable_set(ivar_name, new_value)
14
+ instance_variable_get(ivar_name)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ # Get `delegate` like Rails has
2
+ module GraphQL::Forwardable
3
+ def delegate(*methods, to:)
4
+ methods.each do |method_name|
5
+ define_method(method_name) do |*args|
6
+ self.public_send(to).public_send(method_name, *args)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # Wrap the object in NonNullType in response to `!`
2
+ # @example required Int type
3
+ # !GraphQL::INT_TYPE
4
+ #
5
+ module GraphQL::NonNullWithBang
6
+ def !
7
+ GraphQL::NonNullType.new(of_type: self)
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ # Accepts a hash with symbol keys.
2
+ # - convert keys to strings
3
+ # - if the value responds to `name=`, then assign the hash key as `name`
4
+ class GraphQL::StringNamedHash
5
+ attr_reader :to_h
6
+ def initialize(input_hash)
7
+ @to_h = input_hash
8
+ .reduce({}) { |memo, (key, value)| memo[key.to_s] = value; memo }
9
+ # Set the name of the value based on its key
10
+ @to_h.each {|k, v| v.respond_to?("name=") && v.name = k }
11
+ end
12
+ end
@@ -5,6 +5,7 @@ class GraphQL::TypeDefiner
5
5
  def String; GraphQL::STRING_TYPE; end
6
6
  def Float; GraphQL::FLOAT_TYPE; end
7
7
  def Boolean; GraphQL::BOOLEAN_TYPE; end
8
+ def ID; GraphQL::ID_TYPE; end
8
9
 
9
10
  def [](type)
10
11
  GraphQL::ListType.new(of_type: type)
@@ -1,4 +1,7 @@
1
- class GraphQL::Directive < GraphQL::ObjectType
1
+ class GraphQL::Directive
2
+ extend GraphQL::Definable
3
+ attr_definable :on, :arguments, :name, :description
4
+
2
5
  LOCATIONS = [
3
6
  ON_OPERATION = :on_operation?,
4
7
  ON_FRAGMENT = :on_fragment?,
@@ -8,7 +11,6 @@ class GraphQL::Directive < GraphQL::ObjectType
8
11
  define_method(location) { self.on.include?(location) }
9
12
  end
10
13
 
11
- attr_definable :on, :arguments
12
14
 
13
15
  def initialize
14
16
  @arguments = {}
@@ -27,12 +29,14 @@ class GraphQL::Directive < GraphQL::ObjectType
27
29
 
28
30
  def arguments(new_arguments=nil)
29
31
  if !new_arguments.nil?
30
- @arguments = new_arguments
31
- .reduce({}) {|memo, (k, v)| memo[k.to_s] = v; memo}
32
- .each { |k, v| v.respond_to?("name=") && v.name = k}
32
+ @arguments = GraphQL::StringNamedHash.new(new_arguments).to_h
33
33
  end
34
34
  @arguments
35
35
  end
36
+
37
+ def to_s
38
+ "<GraphQL::Directive #{name}>"
39
+ end
36
40
  end
37
41
 
38
42
  require 'graph_ql/directives/directive_chain'
@@ -24,7 +24,7 @@ class GraphQL::DirectiveChain
24
24
  @result = block.call
25
25
  else
26
26
  applicable_directives.map do |(ast_directive, directive)|
27
- args = GraphQL::Query::Arguments.new(ast_directive.arguments, operation_resolver.variables).to_h
27
+ args = GraphQL::Query::Arguments.new(ast_directive.arguments, directive.arguments, operation_resolver.variables).to_h
28
28
  @result = directive.resolve(args, block)
29
29
  end
30
30
  @result ||= {}
@@ -16,11 +16,7 @@ class GraphQL::Field
16
16
  end
17
17
 
18
18
  def arguments=(new_arguments)
19
- stringified_arguments = new_arguments
20
- .reduce({}) { |memo, (key, value)| memo[key.to_s] = value; memo }
21
- # Set the name from its context on this type:
22
- stringified_arguments.each {|k, v| v.respond_to?("name=") && v.name = k }
23
- @arguments = stringified_arguments
19
+ @arguments = GraphQL::StringNamedHash.new(new_arguments).to_h
24
20
  end
25
21
 
26
22
 
@@ -1,9 +1,7 @@
1
1
  # {Transform} is a [parslet](http://kschiess.github.io/parslet/) transform for for turning the AST into objects in {GraphQL::Nodes} objects.
2
2
  class GraphQL::Transform < Parslet::Transform
3
3
  # Get syntax classes by shallow name:
4
- def self.const_missing(constant_name)
5
- GraphQL::Nodes.const_get(constant_name)
6
- end
4
+ include GraphQL::Nodes
7
5
 
8
6
  def self.optional_sequence(name)
9
7
  rule(name => simple(:val)) { [] }
@@ -9,7 +9,7 @@ class GraphQL::Query
9
9
  @schema = schema
10
10
  @debug = debug
11
11
  @query_string = query_string
12
- @context = context
12
+ @context = Context.new(context)
13
13
  @params = params
14
14
  @validate = validate
15
15
  @fragments = {}
@@ -48,8 +48,7 @@ class GraphQL::Query
48
48
  def execute
49
49
  @operations.reduce({}) do |memo, (name, operation)|
50
50
  resolver = OperationResolver.new(operation, self)
51
- memo[name] = resolver.result
52
- memo
51
+ memo.merge(resolver.result)
53
52
  end
54
53
  end
55
54
 
@@ -62,6 +61,18 @@ class GraphQL::Query
62
61
  end
63
62
  end
64
63
  end
64
+
65
+ # Expose some query-specific info to field resolve functions.
66
+ # It delegates `[]` to the hash that's passed to `GraphQL::Query#initialize`.
67
+ class Context
68
+ def initialize(arbitrary_hash)
69
+ @arbitrary_hash = arbitrary_hash
70
+ end
71
+
72
+ def [](key)
73
+ @arbitrary_hash[key]
74
+ end
75
+ end
65
76
  end
66
77
 
67
78
  require 'graph_ql/query/arguments'
@@ -1,9 +1,10 @@
1
1
  # Creates a plain hash out of arguments, looking up variables if necessary
2
2
  class GraphQL::Query::Arguments
3
3
  attr_reader :to_h
4
- def initialize(ast_arguments, variables)
4
+ def initialize(ast_arguments, argument_hash, variables)
5
5
  @to_h = ast_arguments.reduce({}) do |memo, arg|
6
- value = reduce_value(arg.value, variables)
6
+ arg_defn = argument_hash[arg.name]
7
+ value = reduce_value(arg.value, arg_defn, variables)
7
8
  memo[arg.name] = value
8
9
  memo
9
10
  end
@@ -11,13 +12,13 @@ class GraphQL::Query::Arguments
11
12
 
12
13
  private
13
14
 
14
- def reduce_value(value, variables)
15
+ def reduce_value(value, arg_defn, variables)
15
16
  if value.is_a?(GraphQL::Nodes::VariableIdentifier)
16
17
  value = variables[value.name]
17
18
  elsif value.is_a?(GraphQL::Nodes::Enum)
18
- value = value.name
19
+ value = arg_defn.type.coerce(value.name)
19
20
  elsif value.is_a?(GraphQL::Nodes::InputObject)
20
- value = self.class.new(value.pairs, variables).to_h
21
+ value = self.class.new(value.pairs, arg_defn.type.input_fields, variables).to_h
21
22
  else
22
23
  value
23
24
  end
@@ -2,9 +2,9 @@ class GraphQL::Query::FieldResolutionStrategy
2
2
  attr_reader :result, :result_value
3
3
 
4
4
  def initialize(ast_field, parent_type, target, operation_resolver)
5
- arguments = GraphQL::Query::Arguments.new(ast_field.arguments, operation_resolver.variables).to_h
6
5
  field_name = ast_field.name
7
6
  field = parent_type.fields[field_name] || raise("No field found on #{parent_type.name} '#{parent_type}' for '#{field_name}'")
7
+ arguments = GraphQL::Query::Arguments.new(ast_field.arguments, field.arguments, operation_resolver.variables).to_h
8
8
  value = field.resolve(target, arguments, operation_resolver.context)
9
9
  if value.nil?
10
10
  @result_value = value
@@ -40,9 +40,9 @@ class GraphQL::Query::FieldResolutionStrategy
40
40
  attr_reader :result
41
41
  def initialize(value, field_type, target, parent_type, ast_field, operation_resolver)
42
42
  wrapped_type = field_type.of_type
43
- resolved_type = wrapped_type.kind.resolve(wrapped_type, value)
44
- strategy_class = GraphQL::Query::FieldResolutionStrategy.get_strategy_for_kind(resolved_type.kind)
45
43
  @result = value.map do |item|
44
+ resolved_type = wrapped_type.kind.resolve(wrapped_type, item)
45
+ strategy_class = GraphQL::Query::FieldResolutionStrategy.get_strategy_for_kind(resolved_type.kind)
46
46
  inner_strategy = strategy_class.new(item, resolved_type, target, parent_type, ast_field, operation_resolver)
47
47
  inner_strategy.result
48
48
  end
File without changes
@@ -1,6 +1,6 @@
1
1
  GraphQL::FLOAT_TYPE = GraphQL::ScalarType.new do |t|
2
2
  t.name "Float"
3
3
  def t.coerce(value)
4
- value.to_f
4
+ value.respond_to?(:to_f) ? value.to_f : nil
5
5
  end
6
6
  end
@@ -0,0 +1,6 @@
1
+ GraphQL::ID_TYPE = GraphQL::ScalarType.new do |t|
2
+ t.name "ID"
3
+ def t.coerce(value)
4
+ value.to_s
5
+ end
6
+ end
File without changes
@@ -2,8 +2,4 @@ class GraphQL::ScalarType < GraphQL::ObjectType
2
2
  def kind
3
3
  GraphQL::TypeKinds::SCALAR
4
4
  end
5
-
6
- def to_s
7
- "<GraphQL::ScalarType #{name} >"
8
- end
9
5
  end
File without changes
@@ -3,16 +3,13 @@ end
3
3
 
4
4
  require 'graph_ql/static_validation/message'
5
5
  require 'graph_ql/static_validation/arguments_validator'
6
-
7
- require 'graph_ql/static_validation/argument_literals_are_compatible'
8
- require 'graph_ql/static_validation/arguments_are_defined'
9
- require 'graph_ql/static_validation/required_arguments_are_present'
10
- require 'graph_ql/static_validation/fragment_types_exist'
11
- require 'graph_ql/static_validation/directives_are_defined'
12
- require 'graph_ql/static_validation/fields_are_defined_on_type'
13
- require 'graph_ql/static_validation/fields_have_appropriate_selections'
14
- require 'graph_ql/static_validation/fields_will_merge'
15
- require 'graph_ql/static_validation/fragments_are_used'
16
6
  require 'graph_ql/static_validation/type_stack'
17
7
  require 'graph_ql/static_validation/validator'
18
8
  require 'graph_ql/static_validation/literal_validator'
9
+
10
+ rules_glob = File.expand_path("../static_validation/rules/*.rb", __FILE__)
11
+ Dir.glob(rules_glob).each do |file|
12
+ require(file)
13
+ end
14
+
15
+ require 'graph_ql/static_validation/all_rules'
@@ -0,0 +1,23 @@
1
+ # Default rules for {GraphQL::StaticValidation::Validator}
2
+ #
3
+ # Order is important here. Some validators return {GraphQL::Visitor::SKIP}
4
+ # which stops the visit on that node. That way it doesn't try to find fields on types that
5
+ # don't exist, etc.
6
+ GraphQL::StaticValidation::ALL_RULES = [
7
+ GraphQL::StaticValidation::DirectivesAreDefined,
8
+ GraphQL::StaticValidation::FragmentsAreFinite,
9
+ GraphQL::StaticValidation::FragmentTypesExist,
10
+ GraphQL::StaticValidation::FragmentsAreOnCompositeTypes,
11
+ GraphQL::StaticValidation::FragmentSpreadsArePossible,
12
+ GraphQL::StaticValidation::FragmentsAreUsed,
13
+ GraphQL::StaticValidation::FieldsAreDefinedOnType,
14
+ GraphQL::StaticValidation::FieldsWillMerge,
15
+ GraphQL::StaticValidation::FieldsHaveAppropriateSelections,
16
+ GraphQL::StaticValidation::ArgumentsAreDefined,
17
+ GraphQL::StaticValidation::ArgumentLiteralsAreCompatible,
18
+ GraphQL::StaticValidation::RequiredArgumentsArePresent,
19
+ GraphQL::StaticValidation::VariablesAreInputTypes,
20
+ GraphQL::StaticValidation::VariableDefaultValuesAreCorrectlyTyped,
21
+ GraphQL::StaticValidation::VariablesAreUsedAndDefined,
22
+ GraphQL::StaticValidation::VariableUsagesAreAllowed,
23
+ ]
@@ -1,8 +1,5 @@
1
1
  # Test whether `ast_value` is a valid input for `type`
2
2
  class GraphQL::StaticValidation::LiteralValidator
3
- include GraphQL::StaticValidation::Message::MessageHelper
4
-
5
- attr_reader :errors
6
3
  def validate(ast_value, type)
7
4
  if type.kind.non_null?
8
5
  (!ast_value.nil?) && validate(ast_value, type.of_type)
@@ -1,10 +1,15 @@
1
1
  class GraphQL::StaticValidation::ArgumentsAreDefined < GraphQL::StaticValidation::ArgumentsValidator
2
2
  def validate_node(node, defn, context)
3
+ skip = nil
4
+
3
5
  node.arguments.each do |argument|
4
6
  argument_defn = defn.arguments[argument.name]
5
7
  if argument_defn.nil?
6
8
  context.errors << message("#{node.class.name.split("::").last} '#{node.name}' doesn't accept argument #{argument.name}", node)
9
+ skip = GraphQL::Visitor::SKIP
7
10
  end
8
11
  end
12
+
13
+ skip
9
14
  end
10
15
  end
@@ -10,7 +10,7 @@ class GraphQL::StaticValidation::FieldsWillMerge
10
10
  }
11
11
  visitor[GraphQL::Nodes::Document].leave << -> (node, parent) {
12
12
  has_selections.each { |node|
13
- field_map = gather_fields_by_name(node.selections, {}, context)
13
+ field_map = gather_fields_by_name(node.selections, {}, [], context)
14
14
  find_conflicts(field_map, context)
15
15
  }
16
16
  }
@@ -23,18 +23,26 @@ class GraphQL::StaticValidation::FieldsWillMerge
23
23
  comparison = FieldDefinitionComparison.new(name, ast_fields)
24
24
  context.errors.push(*comparison.errors)
25
25
 
26
- subfield_map = ast_fields.reduce({}) do |memo, defn|
27
- gather_fields_by_name(defn.selections, memo, context)
26
+
27
+ subfield_map = {}
28
+ visited_fragments = []
29
+ ast_fields.each do |defn|
30
+ gather_fields_by_name(defn.selections, subfield_map, visited_fragments, context)
28
31
  end
29
32
  find_conflicts(subfield_map, context)
30
33
  end
31
34
  end
32
35
 
33
- def gather_fields_by_name(fields, field_map, context)
36
+ def gather_fields_by_name(fields, field_map, visited_fragments, context)
34
37
  fields.each do |field|
35
38
  if field.is_a?(GraphQL::Nodes::InlineFragment)
36
39
  next_fields = field.selections
37
40
  elsif field.is_a?(GraphQL::Nodes::FragmentSpread)
41
+ if visited_fragments.include?(field.name)
42
+ next
43
+ else
44
+ visited_fragments << field.name
45
+ end
38
46
  fragment = context.fragments[field.name]
39
47
  next_fields = fragment.selections
40
48
  else
@@ -43,7 +51,7 @@ class GraphQL::StaticValidation::FieldsWillMerge
43
51
  field_map[name_in_selection].push(field)
44
52
  next_fields = []
45
53
  end
46
- gather_fields_by_name(next_fields, field_map, context)
54
+ gather_fields_by_name(next_fields, field_map, visited_fragments, context)
47
55
  end
48
56
  field_map
49
57
  end
@@ -0,0 +1,50 @@
1
+ class GraphQL::StaticValidation::FragmentSpreadsArePossible
2
+ include GraphQL::StaticValidation::Message::MessageHelper
3
+
4
+ def validate(context)
5
+
6
+ context.visitor[GraphQL::Nodes::InlineFragment] << -> (node, parent) {
7
+ fragment_parent = context.object_types[-2]
8
+ fragment_child = context.object_types.last
9
+ validate_fragment_in_scope(fragment_parent, fragment_child, node, context)
10
+ }
11
+
12
+ spreads_to_validate = []
13
+
14
+ context.visitor[GraphQL::Nodes::FragmentSpread] << -> (node, parent) {
15
+ fragment_parent = context.object_types.last
16
+ spreads_to_validate << [node, fragment_parent]
17
+ }
18
+
19
+ context.visitor[GraphQL::Nodes::Document].leave << -> (node, parent) {
20
+ spreads_to_validate.each do |spread_values|
21
+ node, fragment_parent = spread_values
22
+ fragment_child_name = context.fragments[node.name].type
23
+ fragment_child = context.schema.types[fragment_child_name]
24
+ validate_fragment_in_scope(fragment_parent, fragment_child, node, context)
25
+ end
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ def validate_fragment_in_scope(parent_type, child_type, node, context)
32
+ intersecting_types = get_possible_types(parent_type) & get_possible_types(child_type)
33
+ if intersecting_types.none?
34
+ name = node.respond_to?(:name) ? " #{node.name}" : ""
35
+ context.errors << message("Fragment#{name} on #{child_type.name} can't be spread inside #{parent_type.name}", node)
36
+ end
37
+ end
38
+
39
+ def get_possible_types(type)
40
+ if type.kind.wraps?
41
+ get_possible_types(type.of_type)
42
+ elsif type.kind.object?
43
+ [type]
44
+ elsif type.kind.resolves?
45
+ type.possible_types
46
+ else
47
+ []
48
+ end
49
+ end
50
+ end