graphql-client 0.15.0 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f7ced5b5ef5fb8e496d8c221867165c4629670a5f8ecf1e44f8c523e5774ae2
4
- data.tar.gz: 49453ec41a714dca5774c32b24139527beb7623a8c99a2368e789f9b1443c79a
3
+ metadata.gz: 82b89599e02036868b2249fbd3ac2aeb89b6043b19040bdaeaf66f31b5f1b28e
4
+ data.tar.gz: 2e168f61452bd5041aa01dce3a09c99959b629afeb2880ca629d1034f22fb0d9
5
5
  SHA512:
6
- metadata.gz: b69902cbed392a6ad77d5e124db8196439b17750ecb369fc3189a93593857f5e1cf80ade820edb5abc0e11cade056832c733726e2e4202207496d7435eb6ebfa
7
- data.tar.gz: a918d43558b2f75ce3627a7dd003ba76e083b9c087f15c36e4df7e571d5eef71a4760fe929a7d6d93eb78a22b4f310c8ef75f9737a719a516c659eb70117026e
6
+ metadata.gz: 3f14f21ff57e8dc29dbef6e5fb2ee6d2fb552075986ef57c8f89a3d7ce5b2faa8e2965b20a5b8f7735394b3ee13cdc8e55923bda0dd02fa8625f7e2b824dab7b
7
+ data.tar.gz: ee04576697c258bdc3959366d0cccef720e24eb2358ecbb28c020d912c5b241ac7ba195a8240656c6f825109c23a321ef2449892b4d893e29802c03b6580f557
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # graphql-client [![Gem Version](https://badge.fury.io/rb/graphql-client.svg)](https://badge.fury.io/rb/graphql-client) [![Build Status](https://travis-ci.org/github/graphql-client.svg?branch=master)](https://travis-ci.org/github/graphql-client)
1
+ # graphql-client [![Gem Version](https://badge.fury.io/rb/graphql-client.svg)](https://badge.fury.io/rb/graphql-client) [![CI](https://github.com/github/graphql-client/workflows/CI/badge.svg)](https://github.com/github/graphql-client/actions?query=workflow)
2
2
 
3
3
  GraphQL Client is a Ruby library for declaring, composing and executing GraphQL queries.
4
4
 
@@ -12,6 +12,8 @@ module GraphQL
12
12
 
13
13
  # Enforcements collocated object access best practices.
14
14
  module CollocatedEnforcement
15
+ extend self
16
+
15
17
  # Public: Ignore collocated caller enforcement for the scope of the block.
16
18
  def allow_noncollocated_callers
17
19
  Thread.current[:query_result_caller_location_ignore] = true
@@ -20,6 +22,23 @@ module GraphQL
20
22
  Thread.current[:query_result_caller_location_ignore] = nil
21
23
  end
22
24
 
25
+ def verify_collocated_path(location, path, method = "method")
26
+ return yield if Thread.current[:query_result_caller_location_ignore]
27
+
28
+ if (location.path != path) && !(WHITELISTED_GEM_NAMES.any? { |g| location.path.include?("gems/#{g}") })
29
+ error = NonCollocatedCallerError.new("#{method} was called outside of '#{path}' https://git.io/v1syX")
30
+ error.set_backtrace(caller(2))
31
+ raise error
32
+ end
33
+
34
+ begin
35
+ Thread.current[:query_result_caller_location_ignore] = true
36
+ yield
37
+ ensure
38
+ Thread.current[:query_result_caller_location_ignore] = nil
39
+ end
40
+ end
41
+
23
42
  # Internal: Decorate method with collocated caller enforcement.
24
43
  #
25
44
  # mod - Target Module/Class
@@ -31,21 +50,9 @@ module GraphQL
31
50
  mod.prepend(Module.new do
32
51
  methods.each do |method|
33
52
  define_method(method) do |*args, &block|
34
- return super(*args, &block) if Thread.current[:query_result_caller_location_ignore]
35
-
36
- locations = caller_locations(1, 1)
37
-
38
- if (locations.first.path != path) && !(caller_locations.any? { |cl| WHITELISTED_GEM_NAMES.any? { |g| cl.path.include?("gems/#{g}") } })
39
- error = NonCollocatedCallerError.new("#{method} was called outside of '#{path}' https://git.io/v1syX")
40
- error.set_backtrace(caller(1))
41
- raise error
42
- end
43
-
44
- begin
45
- Thread.current[:query_result_caller_location_ignore] = true
53
+ location = caller_locations(1, 1)[0]
54
+ CollocatedEnforcement.verify_collocated_path(location, path, method) do
46
55
  super(*args, &block)
47
- ensure
48
- Thread.current[:query_result_caller_location_ignore] = nil
49
56
  end
50
57
  end
51
58
  end
@@ -45,7 +45,7 @@ module GraphQL
45
45
  raise "Unexpected operation_type: #{ast_node.operation_type}"
46
46
  end
47
47
  when GraphQL::Language::Nodes::FragmentDefinition
48
- @client.schema.types[ast_node.type.name]
48
+ @client.get_type(ast_node.type.name)
49
49
  else
50
50
  raise "Unexpected ast_node: #{ast_node}"
51
51
  end
@@ -115,9 +115,24 @@ module GraphQL
115
115
  else
116
116
  cast_object(obj)
117
117
  end
118
+ when GraphQL::Client::Schema::ObjectType::WithDefinition
119
+ case obj
120
+ when schema_class.klass
121
+ if obj._definer == schema_class
122
+ obj
123
+ else
124
+ cast_object(obj)
125
+ end
126
+ when nil
127
+ nil
128
+ when Hash
129
+ schema_class.new(obj, errors)
130
+ else
131
+ cast_object(obj)
132
+ end
118
133
  when GraphQL::Client::Schema::ObjectType
119
134
  case obj
120
- when NilClass, schema_class
135
+ when nil, schema_class
121
136
  obj
122
137
  when Hash
123
138
  schema_class.new(obj, errors)
@@ -144,8 +159,8 @@ module GraphQL
144
159
 
145
160
  def cast_object(obj)
146
161
  if obj.class.is_a?(GraphQL::Client::Schema::ObjectType)
147
- unless obj.class._spreads.include?(definition_node.name)
148
- raise TypeError, "#{definition_node.name} is not included in #{obj.class.source_definition.name}"
162
+ unless obj._spreads.include?(definition_node.name)
163
+ raise TypeError, "#{definition_node.name} is not included in #{obj.source_definition.name}"
149
164
  end
150
165
  schema_class.cast(obj.to_h, obj.errors)
151
166
  else
@@ -170,16 +185,18 @@ module GraphQL
170
185
  end
171
186
 
172
187
  def flatten_spreads(node)
173
- node.selections.flat_map do |selection|
188
+ spreads = []
189
+ node.selections.each do |selection|
174
190
  case selection
175
191
  when Language::Nodes::FragmentSpread
176
- selection
192
+ spreads << selection
177
193
  when Language::Nodes::InlineFragment
178
- flatten_spreads(selection)
194
+ spreads.concat(flatten_spreads(selection))
179
195
  else
180
- []
196
+ # Do nothing, not a spread
181
197
  end
182
198
  end
199
+ spreads
183
200
  end
184
201
 
185
202
  def index_node_definitions(visitor)
@@ -14,7 +14,7 @@ module GraphQL
14
14
  #
15
15
  # Returns a Hash[Symbol] to GraphQL::Type objects.
16
16
  def self.variables(schema, document, definition_name = nil)
17
- unless schema.is_a?(GraphQL::Schema)
17
+ unless schema.is_a?(GraphQL::Schema) || (schema.is_a?(Class) && schema < GraphQL::Schema)
18
18
  raise TypeError, "expected schema to be a GraphQL::Schema, but was #{schema.class}"
19
19
  end
20
20
 
@@ -35,7 +35,7 @@ module GraphQL
35
35
 
36
36
  if existing_type && existing_type.unwrap != definition.type.unwrap
37
37
  raise GraphQL::Client::ValidationError, "$#{node.name} was already declared as #{existing_type.unwrap}, but was #{definition.type.unwrap}"
38
- elsif !existing_type.is_a?(GraphQL::NonNullType)
38
+ elsif !(existing_type && existing_type.kind.non_null?)
39
39
  variables[node.name.to_sym] = definition.type
40
40
  end
41
41
  end
@@ -66,13 +66,13 @@ module GraphQL
66
66
  #
67
67
  # Returns GraphQL::Language::Nodes::Type.
68
68
  def self.variable_node(type)
69
- case type
70
- when GraphQL::NonNullType
69
+ case type.kind.name
70
+ when "NON_NULL"
71
71
  GraphQL::Language::Nodes::NonNullType.new(of_type: variable_node(type.of_type))
72
- when GraphQL::ListType
72
+ when "LIST"
73
73
  GraphQL::Language::Nodes::ListType.new(of_type: variable_node(type.of_type))
74
74
  else
75
- GraphQL::Language::Nodes::TypeName.new(name: type.name)
75
+ GraphQL::Language::Nodes::TypeName.new(name: type.graphql_name)
76
76
  end
77
77
  end
78
78
  end
@@ -12,7 +12,7 @@ module GraphQL
12
12
  #
13
13
  # Returns a Hash[Language::Nodes::Node] to GraphQL::Type objects.
14
14
  def self.analyze_types(schema, document)
15
- unless schema.is_a?(GraphQL::Schema)
15
+ unless schema.is_a?(GraphQL::Schema) || (schema.is_a?(Class) && schema < GraphQL::Schema)
16
16
  raise TypeError, "expected schema to be a GraphQL::Schema, but was #{schema.class}"
17
17
  end
18
18
 
@@ -40,7 +40,10 @@ module GraphQL
40
40
  visitor.visit
41
41
 
42
42
  fields
43
- rescue StandardError
43
+ rescue StandardError => err
44
+ if err.is_a?(TypeError)
45
+ raise
46
+ end
44
47
  # FIXME: TypeStack my crash on invalid documents
45
48
  fields
46
49
  end
@@ -7,7 +7,7 @@ module GraphQL
7
7
  class Client
8
8
  # Public: Basic HTTP network adapter.
9
9
  #
10
- # GraphQL::Client::Client.new(
10
+ # GraphQL::Client.new(
11
11
  # execute: GraphQL::Client::HTTP.new("http://graphql-swapi.parseapp.com/")
12
12
  # )
13
13
  #
@@ -28,8 +28,8 @@ module GraphQL
28
28
  type = @types[node]
29
29
  type = type && type.unwrap
30
30
 
31
- if (node.selections.any? && (type.nil? || type.is_a?(GraphQL::InterfaceType) || type.is_a?(GraphQL::UnionType))) ||
32
- (node.selections.none? && type.is_a?(GraphQL::ObjectType))
31
+ if (node.selections.any? && (type.nil? || type.kind.interface? || type.kind.union?)) ||
32
+ (node.selections.none? && (type && type.kind.object?))
33
33
  names = QueryTypename.node_flatten_selections(node.selections).map { |s| s.respond_to?(:name) ? s.name : nil }
34
34
  names = Set.new(names.compact)
35
35
 
@@ -41,8 +41,8 @@ module GraphQL
41
41
  #
42
42
  # type - GraphQL::EnumType instance
43
43
  def initialize(type)
44
- unless type.is_a?(GraphQL::EnumType)
45
- raise "expected type to be a GraphQL::EnumType, but was #{type.class}"
44
+ unless type.kind.enum?
45
+ raise "expected type to be an Enum, but was #{type.class}"
46
46
  end
47
47
 
48
48
  @type = type
@@ -9,8 +9,8 @@ module GraphQL
9
9
  include BaseType
10
10
 
11
11
  def initialize(type)
12
- unless type.is_a?(GraphQL::InterfaceType)
13
- raise "expected type to be a GraphQL::InterfaceType, but was #{type.class}"
12
+ unless type.kind.interface?
13
+ raise "expected type to be an Interface, but was #{type.class}"
14
14
  end
15
15
 
16
16
  @type = type
@@ -21,7 +21,7 @@ module GraphQL
21
21
  end
22
22
 
23
23
  def define_class(definition, ast_nodes)
24
- possible_type_names = definition.client.schema.possible_types(type).map(&:graphql_name)
24
+ possible_type_names = definition.client.possible_types(type).map(&:graphql_name)
25
25
  possible_types = possible_type_names.map { |concrete_type_name|
26
26
  schema_module.get_class(concrete_type_name).define_class(definition, ast_nodes)
27
27
  }
@@ -16,6 +16,53 @@ module GraphQL
16
16
 
17
17
  define_singleton_method(:type) { type }
18
18
  define_singleton_method(:fields) { fields }
19
+
20
+ const_set(:READERS, {})
21
+ const_set(:PREDICATES, {})
22
+ end
23
+ end
24
+
25
+ class WithDefinition
26
+ include BaseType
27
+ include ObjectType
28
+
29
+ EMPTY_SET = Set.new.freeze
30
+
31
+ attr_reader :klass, :defined_fields, :definition
32
+
33
+ def type
34
+ @klass.type
35
+ end
36
+
37
+ def fields
38
+ @klass.fields
39
+ end
40
+
41
+ def spreads
42
+ if defined?(@spreads)
43
+ @spreads
44
+ else
45
+ EMPTY_SET
46
+ end
47
+ end
48
+
49
+ def initialize(klass, defined_fields, definition, spreads)
50
+ @klass = klass
51
+ @defined_fields = defined_fields.map do |k, v|
52
+ [-k.to_s, v]
53
+ end.to_h
54
+ @definition = definition
55
+ @spreads = spreads unless spreads.empty?
56
+
57
+ @defined_fields.keys.each do |attr|
58
+ name = ActiveSupport::Inflector.underscore(attr)
59
+ @klass::READERS[:"#{name}"] ||= attr
60
+ @klass::PREDICATES[:"#{name}?"] ||= attr
61
+ end
62
+ end
63
+
64
+ def new(data = {}, errors = Errors.new)
65
+ @klass.new(data, errors, self)
19
66
  end
20
67
  end
21
68
 
@@ -41,61 +88,14 @@ module GraphQL
41
88
  field_nodes.each do |result_name, field_ast_nodes|
42
89
  # `result_name` might be an alias, so make sure to get the proper name
43
90
  field_name = field_ast_nodes.first.name
44
- field_definition = definition.client.schema.get_field(type.name, field_name)
91
+ field_definition = definition.client.schema.get_field(type.graphql_name, field_name)
45
92
  field_return_type = field_definition.type
46
93
  field_classes[result_name.to_sym] = schema_module.define_class(definition, field_ast_nodes, field_return_type)
47
94
  end
48
95
 
49
- klass = Class.new(self)
50
- klass.define_fields(field_classes)
51
- klass.instance_variable_set(:@source_definition, definition)
52
- klass.instance_variable_set(:@_spreads, definition.indexes[:spreads][ast_nodes.first])
53
-
54
- if definition.client.enforce_collocated_callers
55
- keys = field_classes.keys.map { |key| ActiveSupport::Inflector.underscore(key) }
56
- Client.enforce_collocated_callers(klass, keys, definition.source_location[0])
57
- end
58
-
59
- klass
60
- end
61
-
62
- PREDICATE_CACHE = Hash.new { |h, name|
63
- h[name] = -> { @data[name] ? true : false }
64
- }
96
+ spreads = definition.indexes[:spreads][ast_nodes.first]
65
97
 
66
- METHOD_CACHE = Hash.new { |h, key|
67
- h[key] = -> {
68
- name = key.to_s
69
- type = self.class::FIELDS[key]
70
- @casted_data.fetch(name) do
71
- @casted_data[name] = type.cast(@data[name], @errors.filter_by_path(name))
72
- end
73
- }
74
- }
75
-
76
- MODULE_CACHE = Hash.new do |h, fields|
77
- h[fields] = Module.new do
78
- fields.each do |name|
79
- GraphQL::Client::Schema::ObjectType.define_cached_field(name, self)
80
- end
81
- end
82
- end
83
-
84
- FIELDS_CACHE = Hash.new { |h, k| h[k] = k }
85
-
86
- def define_fields(fields)
87
- const_set :FIELDS, FIELDS_CACHE[fields]
88
- mod = MODULE_CACHE[fields.keys.sort]
89
- include mod
90
- end
91
-
92
- def self.define_cached_field(name, ctx)
93
- key = name
94
- name = -name.to_s
95
- method_name = ActiveSupport::Inflector.underscore(name)
96
-
97
- ctx.send(:define_method, method_name, &METHOD_CACHE[key])
98
- ctx.send(:define_method, "#{method_name}?", &PREDICATE_CACHE[name])
98
+ WithDefinition.new(self, field_classes, definition, spreads)
99
99
  end
100
100
 
101
101
  def define_field(name, type)
@@ -134,9 +134,8 @@ module GraphQL
134
134
  continue_selection = if selected_ast_node.type.nil?
135
135
  true
136
136
  else
137
- schema = definition.client.schema
138
- type_condition = schema.types[selected_ast_node.type.name]
139
- applicable_types = schema.possible_types(type_condition)
137
+ type_condition = definition.client.get_type(selected_ast_node.type.name)
138
+ applicable_types = definition.client.possible_types(type_condition)
140
139
  # continue if this object type is one of the types matching the fragment condition
141
140
  applicable_types.include?(type)
142
141
  end
@@ -150,10 +149,8 @@ module GraphQL
150
149
  fragment_definition = definition.document.definitions.find do |defn|
151
150
  defn.is_a?(GraphQL::Language::Nodes::FragmentDefinition) && defn.name == selected_ast_node.name
152
151
  end
153
-
154
- schema = definition.client.schema
155
- type_condition = schema.types[fragment_definition.type.name]
156
- applicable_types = schema.possible_types(type_condition)
152
+ type_condition = definition.client.get_type(fragment_definition.type.name)
153
+ applicable_types = definition.client.possible_types(type_condition)
157
154
  # continue if this object type is one of the types matching the fragment condition
158
155
  continue_selection = applicable_types.include?(type)
159
156
 
@@ -177,17 +174,16 @@ module GraphQL
177
174
  end
178
175
 
179
176
  class ObjectClass
180
- module ClassMethods
181
- attr_reader :source_definition
182
- attr_reader :_spreads
183
- end
184
-
185
- extend ClassMethods
186
-
187
- def initialize(data = {}, errors = Errors.new)
177
+ def initialize(data = {}, errors = Errors.new, definer = nil)
188
178
  @data = data
189
179
  @casted_data = {}
190
180
  @errors = errors
181
+
182
+ # If we are not provided a definition, we can use this empty default
183
+ definer ||= ObjectType::WithDefinition.new(self.class, {}, nil, [])
184
+
185
+ @definer = definer
186
+ @enforce_collocated_callers = source_definition && source_definition.client.enforce_collocated_callers
191
187
  end
192
188
 
193
189
  # Public: Returns the raw response data
@@ -197,41 +193,85 @@ module GraphQL
197
193
  @data
198
194
  end
199
195
 
200
- # Public: Return errors associated with data.
201
- #
202
- # Returns Errors collection.
203
- attr_reader :errors
196
+ def _definer
197
+ @definer
198
+ end
204
199
 
205
- def method_missing(*args)
206
- super
207
- rescue NoMethodError => e
208
- type = self.class.type
200
+ def _spreads
201
+ @definer.spreads
202
+ end
209
203
 
210
- if ActiveSupport::Inflector.underscore(e.name.to_s) != e.name.to_s
211
- raise e
212
- end
204
+ def source_definition
205
+ @definer.definition
206
+ end
213
207
 
214
- field = type.all_fields.find do |f|
215
- f.name == e.name.to_s || ActiveSupport::Inflector.underscore(f.name) == e.name.to_s
208
+ def respond_to_missing?(name, priv)
209
+ if (attr = self.class::READERS[name]) || (attr = self.class::PREDICATES[name])
210
+ @definer.defined_fields.key?(attr) || super
211
+ else
212
+ super
216
213
  end
214
+ end
217
215
 
218
- unless field
219
- raise UnimplementedFieldError, "undefined field `#{e.name}' on #{type} type. https://git.io/v1y3m"
216
+ # Public: Return errors associated with data.
217
+ #
218
+ # It's possible to define "errors" as a field. Ideally this shouldn't
219
+ # happen, but if it does we should prefer the field rather than the
220
+ # builtin error type.
221
+ #
222
+ # Returns Errors collection.
223
+ def errors
224
+ if type = @definer.defined_fields["errors"]
225
+ read_attribute("errors", type)
226
+ else
227
+ @errors
220
228
  end
229
+ end
221
230
 
222
- if @data.key?(field.name)
223
- error_class = ImplicitlyFetchedFieldError
224
- message = "implicitly fetched field `#{field.name}' on #{type} type. https://git.io/v1yGL"
231
+ def method_missing(name, *args)
232
+ if (attr = self.class::READERS[name]) && (type = @definer.defined_fields[attr])
233
+ if @enforce_collocated_callers
234
+ verify_collocated_path do
235
+ read_attribute(attr, type)
236
+ end
237
+ else
238
+ read_attribute(attr, type)
239
+ end
240
+ elsif (attr = self.class::PREDICATES[name]) && @definer.defined_fields[attr]
241
+ has_attribute?(attr)
225
242
  else
226
- error_class = UnfetchedFieldError
227
- message = "unfetched field `#{field.name}' on #{type} type. https://git.io/v1y3U"
228
- end
243
+ begin
244
+ super
245
+ rescue NoMethodError => e
246
+ type = self.class.type
229
247
 
230
- raise error_class, message
248
+ if ActiveSupport::Inflector.underscore(e.name.to_s) != e.name.to_s
249
+ raise e
250
+ end
251
+
252
+ all_fields = type.respond_to?(:all_fields) ? type.all_fields : type.fields.values
253
+ field = all_fields.find do |f|
254
+ f.name == e.name.to_s || ActiveSupport::Inflector.underscore(f.name) == e.name.to_s
255
+ end
256
+
257
+ unless field
258
+ raise UnimplementedFieldError, "undefined field `#{e.name}' on #{type.graphql_name} type. https://git.io/v1y3m"
259
+ end
260
+
261
+ if @data.key?(field.name)
262
+ raise ImplicitlyFetchedFieldError, "implicitly fetched field `#{field.name}' on #{type} type. https://git.io/v1yGL"
263
+ else
264
+ raise UnfetchedFieldError, "unfetched field `#{field.name}' on #{type} type. https://git.io/v1y3U"
265
+ end
266
+ end
267
+ end
231
268
  end
232
269
 
233
270
  def inspect
234
- parent = self.class.ancestors.select { |m| m.is_a?(ObjectType) }.last
271
+ parent = self.class
272
+ until parent.superclass == ObjectClass
273
+ parent = parent.superclass
274
+ end
235
275
 
236
276
  ivars = @data.map { |key, value|
237
277
  if value.is_a?(Hash) || value.is_a?(Array)
@@ -246,6 +286,26 @@ module GraphQL
246
286
  buf << ">"
247
287
  buf
248
288
  end
289
+
290
+ private
291
+
292
+ def verify_collocated_path
293
+ location = caller_locations(2, 1)[0]
294
+
295
+ CollocatedEnforcement.verify_collocated_path(location, source_definition.source_location[0]) do
296
+ yield
297
+ end
298
+ end
299
+
300
+ def read_attribute(attr, type)
301
+ @casted_data.fetch(attr) do
302
+ @casted_data[attr] = type.cast(@data[attr], @errors.filter_by_path(attr))
303
+ end
304
+ end
305
+
306
+ def has_attribute?(attr)
307
+ !!@data[attr]
308
+ end
249
309
  end
250
310
  end
251
311
  end
@@ -22,7 +22,7 @@ module GraphQL
22
22
  unless klass.is_a?(ObjectType)
23
23
  raise TypeError, "expected type to be #{ObjectType}, but was #{type.class}"
24
24
  end
25
- @possible_types[klass.type.name] = klass
25
+ @possible_types[klass.type.graphql_name] = klass
26
26
  end
27
27
  end
28
28
 
@@ -12,8 +12,8 @@ module GraphQL
12
12
  #
13
13
  # type - GraphQL::BaseType instance
14
14
  def initialize(type)
15
- unless type.is_a?(GraphQL::ScalarType)
16
- raise "expected type to be a GraphQL::ScalarType, but was #{type.class}"
15
+ unless type.kind.scalar?
16
+ raise "expected type to be a Scalar, but was #{type.class}"
17
17
  end
18
18
 
19
19
  @type = type
@@ -9,8 +9,8 @@ module GraphQL
9
9
  include BaseType
10
10
 
11
11
  def initialize(type)
12
- unless type.is_a?(GraphQL::UnionType)
13
- raise "expected type to be a GraphQL::UnionType, but was #{type.class}"
12
+ unless type.kind.union?
13
+ raise "expected type to be a Union, but was #{type.class}"
14
14
  end
15
15
 
16
16
  @type = type
@@ -21,7 +21,7 @@ module GraphQL
21
21
  end
22
22
 
23
23
  def define_class(definition, ast_nodes)
24
- possible_type_names = definition.client.schema.possible_types(type).map(&:graphql_name)
24
+ possible_type_names = definition.client.possible_types(type).map(&:graphql_name)
25
25
  possible_types = possible_type_names.map { |concrete_type_name|
26
26
  schema_module.get_class(concrete_type_name).define_class(definition, ast_nodes)
27
27
  }
@@ -16,13 +16,13 @@ module GraphQL
16
16
  module Schema
17
17
  module ClassMethods
18
18
  def define_class(definition, ast_nodes, type)
19
- type_class = case type
20
- when GraphQL::NonNullType
19
+ type_class = case type.kind.name
20
+ when "NON_NULL"
21
21
  define_class(definition, ast_nodes, type.of_type).to_non_null_type
22
- when GraphQL::ListType
22
+ when "LIST"
23
23
  define_class(definition, ast_nodes, type.of_type).to_list_type
24
24
  else
25
- get_class(type.name).define_class(definition, ast_nodes)
25
+ get_class(type.graphql_name).define_class(definition, ast_nodes)
26
26
  end
27
27
 
28
28
  ast_nodes.each do |ast_node|
@@ -62,7 +62,7 @@ module GraphQL
62
62
  private
63
63
 
64
64
  def normalize_type_name(type_name)
65
- type_name =~ /\A[A-Z]/ ? type_name : type_name.camelize
65
+ /\A[A-Z]/.match?(type_name) ? type_name : type_name.camelize
66
66
  end
67
67
  end
68
68
 
@@ -85,18 +85,18 @@ module GraphQL
85
85
  def self.class_for(schema, type, cache)
86
86
  return cache[type] if cache[type]
87
87
 
88
- case type
89
- when GraphQL::InputObjectType
88
+ case type.kind.name
89
+ when "INPUT_OBJECT"
90
90
  nil
91
- when GraphQL::ScalarType
91
+ when "SCALAR"
92
92
  cache[type] = ScalarType.new(type)
93
- when GraphQL::EnumType
93
+ when "ENUM"
94
94
  cache[type] = EnumType.new(type)
95
- when GraphQL::ListType
95
+ when "LIST"
96
96
  cache[type] = class_for(schema, type.of_type, cache).to_list_type
97
- when GraphQL::NonNullType
97
+ when "NON_NULL"
98
98
  cache[type] = class_for(schema, type.of_type, cache).to_non_null_type
99
- when GraphQL::UnionType
99
+ when "UNION"
100
100
  klass = cache[type] = UnionType.new(type)
101
101
 
102
102
  type.possible_types.each do |possible_type|
@@ -105,22 +105,23 @@ module GraphQL
105
105
  end
106
106
 
107
107
  klass
108
- when GraphQL::InterfaceType
108
+ when "INTERFACE"
109
109
  cache[type] = InterfaceType.new(type)
110
- when GraphQL::ObjectType
110
+ when "OBJECT"
111
111
  klass = cache[type] = ObjectType.new(type)
112
112
 
113
113
  type.interfaces.each do |interface|
114
114
  klass.send :include, class_for(schema, interface, cache)
115
115
  end
116
-
117
- type.all_fields.each do |field|
116
+ # Legacy objects have `.all_fields`
117
+ all_fields = type.respond_to?(:all_fields) ? type.all_fields : type.fields.values
118
+ all_fields.each do |field|
118
119
  klass.fields[field.name.to_sym] = class_for(schema, field.type, cache)
119
120
  end
120
121
 
121
122
  klass
122
123
  else
123
- raise TypeError, "unexpected #{type.class}"
124
+ raise TypeError, "unexpected #{type.class} (#{type.inspect})"
124
125
  end
125
126
  end
126
127
  end
@@ -133,7 +133,7 @@ module GraphQL
133
133
 
134
134
  remove_const(name) if placeholder
135
135
  const_set(name, mod)
136
- mod.unloadable
136
+ mod.unloadable if mod.respond_to?(:unloadable)
137
137
  mod
138
138
  end
139
139
 
@@ -46,15 +46,15 @@ module GraphQL
46
46
 
47
47
  def self.load_schema(schema)
48
48
  case schema
49
- when GraphQL::Schema
49
+ when GraphQL::Schema, Class
50
50
  schema
51
51
  when Hash
52
- GraphQL::Schema::Loader.load(schema)
52
+ GraphQL::Schema.from_introspection(schema)
53
53
  when String
54
54
  if schema.end_with?(".json") && File.exist?(schema)
55
55
  load_schema(File.read(schema))
56
56
  elsif schema =~ /\A\s*{/
57
- load_schema(JSON.parse(schema))
57
+ load_schema(JSON.parse(schema, freeze: true))
58
58
  end
59
59
  else
60
60
  if schema.respond_to?(:execute)
@@ -97,10 +97,31 @@ module GraphQL
97
97
  @document_tracking_enabled = false
98
98
  @allow_dynamic_queries = false
99
99
  @enforce_collocated_callers = enforce_collocated_callers
100
-
100
+ if schema.is_a?(Class)
101
+ @possible_types = schema.possible_types
102
+ end
101
103
  @types = Schema.generate(@schema)
102
104
  end
103
105
 
106
+ # A cache of the schema's merged possible types
107
+ # @param type_condition [Class, String] a type definition or type name
108
+ def possible_types(type_condition = nil)
109
+ if type_condition
110
+ if defined?(@possible_types)
111
+ if type_condition.respond_to?(:graphql_name)
112
+ type_condition = type_condition.graphql_name
113
+ end
114
+ @possible_types[type_condition]
115
+ else
116
+ @schema.possible_types(type_condition)
117
+ end
118
+ elsif defined?(@possible_types)
119
+ @possible_types
120
+ else
121
+ @schema.possible_types(type_condition)
122
+ end
123
+ end
124
+
104
125
  def parse(str, filename = nil, lineno = nil)
105
126
  if filename.nil? && lineno.nil?
106
127
  location = caller_locations(1, 1).first
@@ -135,11 +156,7 @@ module GraphQL
135
156
  # which corresponds to the spread.
136
157
  # We depend on ActiveSupport to either find the already-loaded
137
158
  # constant, or to load the constant by name
138
- begin
139
- fragment = ActiveSupport::Inflector.constantize(const_name)
140
- rescue NameError
141
- fragment = nil
142
- end
159
+ fragment = ActiveSupport::Inflector.safe_constantize(const_name)
143
160
 
144
161
  case fragment
145
162
  when FragmentDefinition
@@ -173,12 +190,8 @@ module GraphQL
173
190
 
174
191
  doc.definitions.each do |node|
175
192
  if node.name.nil?
176
- if node.respond_to?(:merge) # GraphQL 1.9 +
177
- node_with_name = node.merge(name: "__anonymous__")
178
- doc = doc.replace_child(node, node_with_name)
179
- else
180
- node.name = "__anonymous__"
181
- end
193
+ node_with_name = node.merge(name: "__anonymous__")
194
+ doc = doc.replace_child(node, node_with_name)
182
195
  end
183
196
  end
184
197
 
@@ -200,32 +213,13 @@ module GraphQL
200
213
  raise error
201
214
  end
202
215
 
203
- definitions = {}
204
- doc.definitions.each do |node|
205
- sliced_document = Language::DefinitionSlice.slice(document_dependencies, node.name)
206
- definition = Definition.for(
207
- client: self,
208
- ast_node: node,
209
- document: sliced_document,
210
- source_document: doc,
211
- source_location: source_location
212
- )
213
- definitions[node.name] = definition
214
- end
216
+ definitions = sliced_definitions(document_dependencies, doc, source_location: source_location)
215
217
 
216
- name_hook = RenameNodeHook.new(definitions)
217
- visitor = Language::Visitor.new(document_dependencies)
218
- visitor[Language::Nodes::FragmentDefinition].leave << name_hook.method(:rename_node)
219
- visitor[Language::Nodes::OperationDefinition].leave << name_hook.method(:rename_node)
220
- visitor[Language::Nodes::FragmentSpread].leave << name_hook.method(:rename_node)
218
+ visitor = RenameNodeVisitor.new(document_dependencies, definitions: definitions)
221
219
  visitor.visit
222
220
 
223
221
  if document_tracking_enabled
224
- if @document.respond_to?(:merge) # GraphQL 1.9+
225
- @document = @document.merge(definitions: @document.definitions + doc.definitions)
226
- else
227
- @document.definitions.concat(doc.definitions)
228
- end
222
+ @document = @document.merge(definitions: @document.definitions + doc.definitions)
229
223
  end
230
224
 
231
225
  if definitions["__anonymous__"]
@@ -239,12 +233,30 @@ module GraphQL
239
233
  end
240
234
  end
241
235
 
242
- class RenameNodeHook
243
- def initialize(definitions)
236
+ class RenameNodeVisitor < GraphQL::Language::Visitor
237
+ def initialize(document, definitions:)
238
+ super(document)
244
239
  @definitions = definitions
245
240
  end
246
241
 
247
- def rename_node(node, _parent)
242
+ def on_fragment_definition(node, _parent)
243
+ rename_node(node)
244
+ super
245
+ end
246
+
247
+ def on_operation_definition(node, _parent)
248
+ rename_node(node)
249
+ super
250
+ end
251
+
252
+ def on_fragment_spread(node, _parent)
253
+ rename_node(node)
254
+ super
255
+ end
256
+
257
+ private
258
+
259
+ def rename_node(node)
248
260
  definition = @definitions[node.name]
249
261
  if definition
250
262
  node.extend(LazyName)
@@ -253,6 +265,10 @@ module GraphQL
253
265
  end
254
266
  end
255
267
 
268
+ # Public: A wrapper to use the more-efficient `.get_type` when it's available from GraphQL-Ruby (1.10+)
269
+ def get_type(type_name)
270
+ @schema.get_type(type_name)
271
+ end
256
272
 
257
273
  # Public: Create operation definition from a fragment definition.
258
274
  #
@@ -288,15 +304,15 @@ module GraphQL
288
304
  variables = GraphQL::Client::DefinitionVariables.operation_variables(self.schema, fragment.document, fragment.definition_name)
289
305
  type_name = fragment.definition_node.type.name
290
306
 
291
- if schema.query && type_name == schema.query.name
307
+ if schema.query && type_name == schema.query.graphql_name
292
308
  operation_type = "query"
293
- elsif schema.mutation && type_name == schema.mutation.name
309
+ elsif schema.mutation && type_name == schema.mutation.graphql_name
294
310
  operation_type = "mutation"
295
- elsif schema.subscription && type_name == schema.subscription.name
311
+ elsif schema.subscription && type_name == schema.subscription.graphql_name
296
312
  operation_type = "subscription"
297
313
  else
298
314
  types = [schema.query, schema.mutation, schema.subscription].compact
299
- raise Error, "Fragment must be defined on #{types.map(&:name).join(", ")}"
315
+ raise Error, "Fragment must be defined on #{types.map(&:graphql_name).join(", ")}"
300
316
  end
301
317
 
302
318
  doc_ast = GraphQL::Language::Nodes::Document.new(definitions: [
@@ -379,6 +395,44 @@ module GraphQL
379
395
 
380
396
  private
381
397
 
398
+ def sliced_definitions(document_dependencies, doc, source_location:)
399
+ dependencies = document_dependencies.definitions.map do |node|
400
+ [node.name, find_definition_dependencies(node)]
401
+ end.to_h
402
+
403
+ doc.definitions.map do |node|
404
+ deps = Set.new
405
+ definitions = document_dependencies.definitions.map { |x| [x.name, x] }.to_h
406
+
407
+ queue = [node.name]
408
+ while name = queue.shift
409
+ next if deps.include?(name)
410
+ deps.add(name)
411
+ queue.concat dependencies[name]
412
+ end
413
+
414
+ definitions = document_dependencies.definitions.select { |x| deps.include?(x.name) }
415
+ sliced_document = Language::Nodes::Document.new(definitions: definitions)
416
+ definition = Definition.for(
417
+ client: self,
418
+ ast_node: node,
419
+ document: sliced_document,
420
+ source_document: doc,
421
+ source_location: source_location
422
+ )
423
+
424
+ [node.name, definition]
425
+ end.to_h
426
+ end
427
+
428
+ def find_definition_dependencies(node)
429
+ names = []
430
+ visitor = Language::Visitor.new(node)
431
+ visitor[Language::Nodes::FragmentSpread] << -> (node, parent) { names << node.name }
432
+ visitor.visit
433
+ names.uniq
434
+ end
435
+
382
436
  def deep_freeze_json_object(obj)
383
437
  case obj
384
438
  when String
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-01 00:00:00.000000000 Z
11
+ date: 2022-05-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: graphql
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.8'
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '1.8'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: actionpack
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -115,6 +115,9 @@ dependencies:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0.10'
118
+ - - "<="
119
+ - !ruby/object:Gem::Version
120
+ version: 0.16.0
118
121
  type: :development
119
122
  prerelease: false
120
123
  version_requirements: !ruby/object:Gem::Requirement
@@ -122,6 +125,9 @@ dependencies:
122
125
  - - "~>"
123
126
  - !ruby/object:Gem::Version
124
127
  version: '0.10'
128
+ - - "<="
129
+ - !ruby/object:Gem::Version
130
+ version: 0.16.0
125
131
  - !ruby/object:Gem::Dependency
126
132
  name: rubocop
127
133
  requirement: !ruby/object:Gem::Requirement
@@ -198,7 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
198
204
  - !ruby/object:Gem::Version
199
205
  version: '0'
200
206
  requirements: []
201
- rubygems_version: 3.0.3
207
+ rubygems_version: 3.2.9
202
208
  signing_key:
203
209
  specification_version: 4
204
210
  summary: GraphQL Client