graphql 0.7.1 → 0.8.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.rb +2 -0
  3. data/lib/graphql/base_type.rb +79 -0
  4. data/lib/graphql/enum_type.rb +1 -3
  5. data/lib/graphql/input_object_type.rb +2 -2
  6. data/lib/graphql/interface_type.rb +3 -22
  7. data/lib/graphql/list_type.rb +5 -3
  8. data/lib/graphql/non_null_type.rb +4 -2
  9. data/lib/graphql/object_type.rb +1 -34
  10. data/lib/graphql/query.rb +10 -5
  11. data/lib/graphql/query/arguments.rb +1 -1
  12. data/lib/graphql/query/base_execution.rb +21 -2
  13. data/lib/graphql/query/base_execution/value_resolution.rb +82 -0
  14. data/lib/graphql/query/executor.rb +7 -5
  15. data/lib/graphql/query/parallel_execution.rb +101 -0
  16. data/lib/graphql/query/serial_execution/field_resolution.rb +18 -9
  17. data/lib/graphql/scalar_type.rb +3 -3
  18. data/lib/graphql/schema.rb +4 -0
  19. data/lib/graphql/schema/type_map.rb +34 -0
  20. data/lib/graphql/schema/type_reducer.rb +14 -16
  21. data/lib/graphql/static_validation/arguments_validator.rb +8 -8
  22. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +6 -8
  23. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +7 -11
  24. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +1 -1
  25. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +1 -1
  26. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +1 -1
  27. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +25 -4
  28. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +2 -2
  29. data/lib/graphql/static_validation/type_stack.rb +3 -3
  30. data/lib/graphql/type_kinds.rb +0 -19
  31. data/lib/graphql/union_type.rb +3 -13
  32. data/lib/graphql/version.rb +1 -1
  33. data/readme.md +0 -11
  34. data/spec/graphql/base_type_spec.rb +24 -0
  35. data/spec/graphql/object_type_spec.rb +0 -20
  36. data/spec/graphql/query/parallel_execution_spec.rb +29 -0
  37. data/spec/graphql/query_spec.rb +23 -6
  38. data/spec/graphql/schema/type_reducer_spec.rb +25 -0
  39. data/spec/graphql/static_validation/validator_spec.rb +4 -3
  40. data/spec/spec_helper.rb +1 -1
  41. data/spec/support/dairy_app.rb +6 -2
  42. data/spec/support/parallel_schema.rb +31 -0
  43. metadata +26 -3
  44. data/lib/graphql/query/value_resolution.rb +0 -76
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 361de5aace2ae0ad425b1c59da22dbc53128020e
4
- data.tar.gz: d2927200177cdc6c5d26f82feb195180d9e631da
3
+ metadata.gz: 9893aa36baed8d8eef998b94e2a7e1e160f6bc30
4
+ data.tar.gz: 838caa42e67801cf03761deae983200af1eccb71
5
5
  SHA512:
6
- metadata.gz: a9b4d287ff8fe030fdb5b7693b1e418cbdbd8f4c2f50c99f6e2daef14501a39b60afbdaba2ec1a6958cf4f7ef6cf9cd13028779565f34cc81ffc08df01ceffd8
7
- data.tar.gz: d775e65e4736ab0ed9fb9502c4f42d0a59ea5703efc3c9174e68a9f25300377b3c6236e7f818967e6a6cccd83ad5316c1b9259c70313972a313bc93aed2c90f8
6
+ metadata.gz: b54e1f57376d148e3c8f6df3043bb4cde33f27374b02c3022ce96ebd39d5fc3bda70e60737501c29963ec320e510c1c2f593b904f651dfcd1603e24c074aa3a6
7
+ data.tar.gz: f3086c08018736a123143ae7221fc4b7ea7df30430eabd43d154bace39364235ad1383ca55022389ec8b115fe6944e6d416340e9dfb88c2e25bf13d5a1b06850
@@ -1,3 +1,4 @@
1
+ require "celluloid/current"
1
2
  require "json"
2
3
  require "parslet"
3
4
  require "singleton"
@@ -20,6 +21,7 @@ end
20
21
  # Order matters for these:
21
22
 
22
23
  require 'graphql/definition_helpers'
24
+ require 'graphql/base_type'
23
25
  require 'graphql/object_type'
24
26
 
25
27
  require 'graphql/enum_type'
@@ -0,0 +1,79 @@
1
+ module GraphQL
2
+ # The parent for all type classes.
3
+ class BaseType
4
+ include GraphQL::DefinitionHelpers::NonNullWithBang
5
+ include GraphQL::DefinitionHelpers::DefinedByConfig
6
+
7
+ # @param other [GraphQL::BaseType] compare to this object
8
+ # @return [Boolean] are these types equivalent? (incl. non-null, list)
9
+ def ==(other)
10
+ if other.is_a?(GraphQL::BaseType)
11
+ self.to_s == other.to_s
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ # If this type is modifying an underlying type,
18
+ # return the underlying type. (Otherwise, return `self`.)
19
+ def unwrap
20
+ self
21
+ end
22
+
23
+ module ModifiesAnotherType
24
+ def unwrap
25
+ self.of_type.unwrap
26
+ end
27
+ end
28
+
29
+ # Find out which possible type to use for `value`.
30
+ # Returns self if there are no possible types (ie, not Union or Interface)
31
+ def resolve_type(value)
32
+ self
33
+ end
34
+
35
+ module HasPossibleTypes
36
+ # Return the implementing type for `object`.
37
+ # The default implementation assumes that there's a type with the same name as `object.class.name`.
38
+ # Maybe you'll need to override this in your own interfaces!
39
+ #
40
+ # @param object [Object] the object which needs a type to expose it
41
+ # @return [GraphQL::ObjectType] the type which should expose `object`
42
+ def resolve_type(object)
43
+ instance_exec(object, &@resolve_type_proc)
44
+ end
45
+
46
+ # The default implementation of {#resolve_type} gets `object.class.name`
47
+ # and finds a type with the same name
48
+ DEFAULT_RESOLVE_TYPE = -> (object) {
49
+ type_name = object.class.name
50
+ possible_types.find {|t| t.name == type_name}
51
+ }
52
+
53
+ def resolve_type=(new_proc)
54
+ @resolve_type_proc = new_proc || DEFAULT_RESOLVE_TYPE
55
+ end
56
+ end
57
+
58
+ # Print the human-readable name of this type
59
+ def to_s
60
+ Printer.instance.print(self)
61
+ end
62
+
63
+ alias :inspect :to_s
64
+
65
+ # Print a type, using the query-style naming pattern
66
+ class Printer
67
+ include Singleton
68
+ def print(type)
69
+ if type.kind.non_null?
70
+ "#{print(type.of_type)}!"
71
+ elsif type.kind.list?
72
+ "[#{print(type.of_type)}]"
73
+ else
74
+ type.name
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -10,9 +10,7 @@
10
10
  # value("RUBY", "A very dynamic language aimed at programmer happiness")
11
11
  # value("JAVASCRIPT", "Accidental lingua franca of the web")
12
12
  # end
13
- class GraphQL::EnumType
14
- include GraphQL::DefinitionHelpers::NonNullWithBang
15
- include GraphQL::DefinitionHelpers::DefinedByConfig
13
+ class GraphQL::EnumType < GraphQL::BaseType
16
14
  attr_accessor :name, :description, :values
17
15
  defined_by_config :name, :description, :values
18
16
 
@@ -7,8 +7,8 @@
7
7
  # input_field :number, !types.Int
8
8
  # end
9
9
  #
10
- class GraphQL::InputObjectType < GraphQL::ObjectType
11
- attr_accessor :input_fields
10
+ class GraphQL::InputObjectType < GraphQL::BaseType
11
+ attr_accessor :name, :description, :input_fields
12
12
  defined_by_config :name, :description, :input_fields
13
13
 
14
14
  def input_fields=(new_fields)
@@ -10,15 +10,10 @@
10
10
  # field :release_year, types.Int
11
11
  # end
12
12
  #
13
- class GraphQL::InterfaceType < GraphQL::ObjectType
13
+ class GraphQL::InterfaceType < GraphQL::BaseType
14
+ include GraphQL::BaseType::HasPossibleTypes
14
15
  defined_by_config :name, :description, :fields, :resolve_type
15
-
16
- # The default implementation of {#resolve_type} gets `object.class.name`
17
- # and finds a type with the same name
18
- DEFAULT_RESOLVE_TYPE = -> (object) {
19
- type_name = object.class.name
20
- possible_types.find {|t| t.name == type_name}
21
- }
16
+ attr_accessor :name, :description, :fields
22
17
 
23
18
  def kind
24
19
  GraphQL::TypeKinds::INTERFACE
@@ -28,18 +23,4 @@ class GraphQL::InterfaceType < GraphQL::ObjectType
28
23
  def possible_types
29
24
  @possible_types ||= []
30
25
  end
31
-
32
- # Return the implementing type for `object`.
33
- # The default implementation assumes that there's a type with the same name as `object.class.name`.
34
- # Maybe you'll need to override this in your own interfaces!
35
- #
36
- # @param object [Object] the object which needs a type to expose it
37
- # @return [GraphQL::ObjectType] the type which should expose `object`
38
- def resolve_type(object)
39
- instance_exec(object, &@resolve_type_proc)
40
- end
41
-
42
- def resolve_type=(new_proc)
43
- @resolve_type_proc = new_proc || DEFAULT_RESOLVE_TYPE
44
- end
45
26
  end
@@ -1,12 +1,14 @@
1
1
  # A list type wraps another type.
2
2
  #
3
- # See {TypeKind#unwrap} for accessing the modified type
4
- class GraphQL::ListType < GraphQL::ObjectType
5
- attr_reader :of_type
3
+ # Get the underlying type with {#unwrap}
4
+ class GraphQL::ListType < GraphQL::BaseType
5
+ include GraphQL::BaseType::ModifiesAnotherType
6
+ attr_reader :of_type, :name
6
7
  def initialize(of_type:)
7
8
  @name = "List"
8
9
  @of_type = of_type
9
10
  end
11
+
10
12
  def kind
11
13
  GraphQL::TypeKinds::LIST
12
14
  end
@@ -1,7 +1,9 @@
1
1
  # A non-null type wraps another type.
2
2
  #
3
- # See {TypeKind#unwrap} for accessing the modified type
4
- class GraphQL::NonNullType < GraphQL::ObjectType
3
+ # Get the underlying type with {#unwrap}
4
+ class GraphQL::NonNullType < GraphQL::BaseType
5
+ include GraphQL::BaseType::ModifiesAnotherType
6
+
5
7
  attr_reader :of_type
6
8
  def initialize(of_type:)
7
9
  @of_type = of_type
@@ -19,9 +19,7 @@
19
19
  # end
20
20
  # end
21
21
  #
22
- class GraphQL::ObjectType
23
- include GraphQL::DefinitionHelpers::NonNullWithBang
24
- include GraphQL::DefinitionHelpers::DefinedByConfig
22
+ class GraphQL::ObjectType < GraphQL::BaseType
25
23
  defined_by_config :name, :description, :interfaces, :fields
26
24
  attr_accessor :name, :description, :interfaces, :fields
27
25
 
@@ -43,35 +41,4 @@ class GraphQL::ObjectType
43
41
  def kind
44
42
  GraphQL::TypeKinds::OBJECT
45
43
  end
46
-
47
- # Print the human-readable name of this type
48
- def to_s
49
- Printer.instance.print(self)
50
- end
51
-
52
- alias :inspect :to_s
53
-
54
- # @param other [GraphQL::ObjectType] compare to this object
55
- # @return [Boolean] are these types equivalent? (incl. non-null, list)
56
- def ==(other)
57
- if other.is_a?(GraphQL::ObjectType)
58
- self.to_s == other.to_s
59
- else
60
- super
61
- end
62
- end
63
-
64
- # Print a type, using the query-style naming pattern
65
- class Printer
66
- include Singleton
67
- def print(type)
68
- if type.kind.non_null?
69
- "#{print(type.of_type)}!"
70
- elsif type.kind.list?
71
- "[#{print(type.of_type)}]"
72
- else
73
- type.name
74
- end
75
- end
76
- end
77
44
  end
@@ -16,7 +16,7 @@ class GraphQL::Query
16
16
  def initialize(schema, query_string, context: nil, variables: {}, debug: true, validate: true, operation_name: nil)
17
17
  @schema = schema
18
18
  @debug = debug
19
- @context = Context.new(context)
19
+ @context = Context.new(values: context)
20
20
 
21
21
  @variables = variables
22
22
  @validate = validate
@@ -52,12 +52,17 @@ class GraphQL::Query
52
52
  # Expose some query-specific info to field resolve functions.
53
53
  # It delegates `[]` to the hash that's passed to `GraphQL::Query#initialize`.
54
54
  class Context
55
- def initialize(arbitrary_hash)
56
- @arbitrary_hash = arbitrary_hash
55
+ attr_accessor :execution_strategy, :ast_node
56
+ def initialize(values:)
57
+ @values = values
57
58
  end
58
59
 
59
60
  def [](key)
60
- @arbitrary_hash[key]
61
+ @values[key]
62
+ end
63
+
64
+ def async(&block)
65
+ execution_strategy.async(block)
61
66
  end
62
67
  end
63
68
  end
@@ -65,7 +70,7 @@ end
65
70
  require 'graphql/query/arguments'
66
71
  require 'graphql/query/base_execution'
67
72
  require 'graphql/query/serial_execution'
68
- require 'graphql/query/value_resolution'
73
+ require 'graphql/query/parallel_execution'
69
74
  require 'graphql/query/type_resolver'
70
75
  require 'graphql/query/directive_chain'
71
76
  require 'graphql/query/executor'
@@ -27,7 +27,7 @@ class GraphQL::Query::Arguments
27
27
  elsif value.is_a?(GraphQL::Language::Nodes::Enum)
28
28
  value = arg_defn.type.coerce(value.name)
29
29
  elsif value.is_a?(GraphQL::Language::Nodes::InputObject)
30
- wrapped_type = arg_defn.type.kind.unwrap(arg_defn.type)
30
+ wrapped_type = arg_defn.type.unwrap
31
31
  value = self.class.new(value.pairs, wrapped_type.input_fields, variables)
32
32
  else
33
33
  value
@@ -1,6 +1,21 @@
1
+ require 'graphql/query/base_execution/selected_object_resolution'
2
+ require 'graphql/query/base_execution/value_resolution'
3
+
1
4
  module GraphQL
2
5
  class Query
3
6
  class BaseExecution
7
+ # This is the only required method for an Execution strategy.
8
+ # You could create a custom execution strategy and configure your schema to
9
+ # use that custom strategy instead.
10
+ #
11
+ # @param ast_operation [GraphQL::Language::Nodes::OperationDefinition] The operation definition to run
12
+ # @param root_type [GraphQL::ObjectType] either the query type or the mutation type
13
+ # @param query_obj [GraphQL::Query] the query object for this execution
14
+ # @return [Hash] a spec-compliant GraphQL result, as a hash
15
+ def execute(ast_operation, root_type, query_obj)
16
+ resolver = operation_resolution.new(ast_operation, root_type, query_obj, self)
17
+ resolver.result
18
+ end
4
19
 
5
20
  def field_resolution
6
21
  get_class :FieldResolution
@@ -22,6 +37,12 @@ module GraphQL
22
37
  get_class :SelectionResolution
23
38
  end
24
39
 
40
+ # ParallelExecution overrides this to provide
41
+ # real async behavior
42
+ def async(&block)
43
+ block.call
44
+ end
45
+
25
46
  private
26
47
 
27
48
  def get_class(class_name)
@@ -30,5 +51,3 @@ module GraphQL
30
51
  end
31
52
  end
32
53
  end
33
-
34
- require 'graphql/query/base_execution/selected_object_resolution'
@@ -0,0 +1,82 @@
1
+ module GraphQL
2
+ class Query
3
+ class BaseExecution
4
+ module ValueResolution
5
+ def self.get_strategy_for_kind(kind)
6
+ TYPE_KIND_STRATEGIES[kind] || raise("No value resolution strategy for #{kind}!")
7
+ end
8
+
9
+ class BaseResolution
10
+ attr_reader :value, :field_type, :target, :parent_type,
11
+ :ast_field, :query, :execution_strategy
12
+ def initialize(value, field_type, target, parent_type, ast_field, query, execution_strategy)
13
+ @value = value
14
+ @field_type = field_type
15
+ @target = target
16
+ @parent_type = parent_type
17
+ @ast_field = ast_field
18
+ @query = query
19
+ @execution_strategy = execution_strategy
20
+ end
21
+
22
+ def result
23
+ raise NotImplementedError, "Should return a value based on initialization params"
24
+ end
25
+
26
+ def get_strategy_for_kind(*args)
27
+ GraphQL::Query::BaseExecution::ValueResolution.get_strategy_for_kind(*args)
28
+ end
29
+ end
30
+
31
+ class ScalarResolution < BaseResolution
32
+ def result
33
+ field_type.coerce(value)
34
+ end
35
+ end
36
+
37
+ class ListResolution < BaseResolution
38
+ def result
39
+ wrapped_type = field_type.of_type
40
+ value.map do |item|
41
+ resolved_type = wrapped_type.resolve_type(item)
42
+ strategy_class = get_strategy_for_kind(resolved_type.kind)
43
+ inner_strategy = strategy_class.new(item, resolved_type, target, parent_type, ast_field, query, execution_strategy)
44
+ inner_strategy.result
45
+ end
46
+ end
47
+ end
48
+
49
+ class ObjectResolution < BaseResolution
50
+ def result
51
+ resolver = execution_strategy.selection_resolution.new(value, field_type, ast_field.selections, query, execution_strategy)
52
+ resolver.result
53
+ end
54
+ end
55
+
56
+ class EnumResolution < BaseResolution
57
+ def result
58
+ value.to_s
59
+ end
60
+ end
61
+
62
+ class NonNullResolution < BaseResolution
63
+ def result
64
+ wrapped_type = field_type.of_type
65
+ resolved_type = wrapped_type.resolve_type(value)
66
+ strategy_class = get_strategy_for_kind(resolved_type.kind)
67
+ inner_strategy = strategy_class.new(value, resolved_type, target, parent_type, ast_field, query, execution_strategy)
68
+ inner_strategy.result
69
+ end
70
+ end
71
+
72
+ TYPE_KIND_STRATEGIES = {
73
+ GraphQL::TypeKinds::SCALAR => ScalarResolution,
74
+ GraphQL::TypeKinds::LIST => ListResolution,
75
+ GraphQL::TypeKinds::OBJECT => ObjectResolution,
76
+ GraphQL::TypeKinds::ENUM => EnumResolution,
77
+ GraphQL::TypeKinds::NON_NULL => NonNullResolution,
78
+ }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -30,13 +30,15 @@ module GraphQL
30
30
  return {} if query.operations.none?
31
31
  operation = find_operation(operation_name, query.operations)
32
32
  if operation.operation_type == "query"
33
- root = query.schema.query
33
+ root_type = query.schema.query
34
+ execution_strategy_class = query.schema.query_execution_strategy || GraphQL::Query::ParallelExecution
34
35
  elsif operation.operation_type == "mutation"
35
- root = query.schema.mutation
36
+ root_type = query.schema.mutation
37
+ execution_strategy_class = query.schema.mutation_execution_strategy || GraphQL::Query::SerialExecution
36
38
  end
37
- execution_strategy = GraphQL::Query::SerialExecution.new
38
- resolver = execution_strategy.operation_resolution.new(operation, root, query, execution_strategy)
39
- resolver.result
39
+ execution_strategy = execution_strategy_class.new
40
+ query.context.execution_strategy = execution_strategy
41
+ result = execution_strategy.execute(operation, root_type, query)
40
42
  end
41
43
 
42
44
  def find_operation(operation_name, operations)
@@ -0,0 +1,101 @@
1
+ module GraphQL
2
+ class Query
3
+ # Utilize Celluloid::Future to run field resolution in parallel.
4
+ class ParallelExecution < GraphQL::Query::BaseExecution
5
+ def initialize
6
+ # Why isn't `require "celluloid/current"` enough here?
7
+ Celluloid.boot unless Celluloid.running?
8
+ @has_futures = false
9
+ end
10
+
11
+ def pool
12
+ @pool ||= ExecutionPool.run!
13
+ @pool[:execution_worker_pool]
14
+ end
15
+
16
+ def async(block)
17
+ @has_futures ||= true
18
+ pool.future.resolve(block)
19
+ end
20
+
21
+ def has_futures?
22
+ @has_futures
23
+ end
24
+
25
+ class OperationResolution < GraphQL::Query::SerialExecution::OperationResolution
26
+ def result
27
+ result_futures = super
28
+ if execution_strategy.has_futures?
29
+ finished_result = finish_all_futures(result_futures)
30
+ else
31
+ # Don't bother re-traversing the result if there are no futures.
32
+ finished_result = result_futures
33
+ end
34
+ ensure
35
+ execution_strategy.pool.terminate
36
+ finished_result
37
+ end
38
+
39
+ # Recurse over `result_object`, finding any futures and
40
+ # getting their finished values.
41
+ def finish_all_futures(result_object)
42
+ if result_object.is_a?(FutureFieldResolution)
43
+ resolved_value = finish_all_futures(result_object.result)
44
+ elsif result_object.is_a?(Hash)
45
+ result_object.each do |key, value|
46
+ result_object[key] = finish_all_futures(value)
47
+ end
48
+ resolved_value = result_object
49
+ elsif result_object.is_a?(Array)
50
+ resolved_value = result_object.map { |v| finish_all_futures(v) }
51
+ else
52
+ resolved_value = result_object
53
+ end
54
+ end
55
+ end
56
+
57
+ class SelectionResolution < GraphQL::Query::SerialExecution::SelectionResolution
58
+ end
59
+
60
+ class FieldResolution < GraphQL::Query::SerialExecution::FieldResolution
61
+ def get_finished_value(raw_value)
62
+ if raw_value.is_a?(Celluloid::Future)
63
+ GraphQL::Query::ParallelExecution::FutureFieldResolution.new(field_resolution: self, future: raw_value)
64
+ else
65
+ super
66
+ end
67
+ end
68
+ end
69
+
70
+ class InlineFragmentResolution < GraphQL::Query::SerialExecution::InlineFragmentResolution
71
+ end
72
+
73
+ class FragmentSpreadResolution < GraphQL::Query::SerialExecution::FragmentSpreadResolution
74
+ end
75
+
76
+ class ExecutionWorker
77
+ include Celluloid
78
+ def resolve(proc)
79
+ proc.call
80
+ end
81
+ end
82
+
83
+ class ExecutionPool < Celluloid::Supervision::Container
84
+ pool ExecutionWorker, as: :execution_worker_pool
85
+ end
86
+
87
+ class FutureFieldResolution
88
+ def initialize(field_resolution:, future:)
89
+ @field_resolution = field_resolution
90
+ @future = future
91
+ end
92
+
93
+ def result
94
+ resolved_value = @future.value
95
+ result_value = @field_resolution.get_finished_value(resolved_value)
96
+ result_value
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end