graphql 0.7.1 → 0.8.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.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