graphql-client 0.15.0 → 0.18.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.
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