graphql-client 0.16.0 → 0.19.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: 6810301cbdd84362dc4b8584e24deec3aec4248f4471734748cf1a33feaa5232
4
- data.tar.gz: bb68c48d70c276c0443da6620506b37dbe152e20fb931a2ecf3a0cba45050f96
3
+ metadata.gz: 3d469dea9c19c2b7b70f57554d66dab3374c496ef09bc013d9464509faac1243
4
+ data.tar.gz: 7fdc9c16119e205cc1e200ee4fc5d882776bbd94c1294dc075963428d7d9f7e4
5
5
  SHA512:
6
- metadata.gz: 6b7f114d4330ae5168669d7a663a0afdbd508ba056aa252a206efca9071f558c8749ecfd809ca0677ab9e3a5e95cc90eec3a6bcc2e4020875aab6621d9059dd8
7
- data.tar.gz: b1a24192dc4ee004ae6d546d4d724d0c49da4a17a4fb86a1a7e81d1aefbe16618f32ed27a63a8c2297bd4074ec8dd91794ab057d3826d08ccfefe21d689e7e76
6
+ metadata.gz: 51ad31a0fd1faf1275037177e7b22f29f597ab9c03868831422de07f563a1c2911a32372832f45053c2430648a267067a721b66df5697fbc1e8735fefddf78b1
7
+ data.tar.gz: 6162d16ba9cefdd82356c89fd799744bcb0777716c0a379c403f519c4aa5fb1c1d2a0a15955852f31d76326fcb6ceebb24fc861c85d211078e5b492bbbc95e0f
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
@@ -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)
@@ -132,41 +147,58 @@ module GraphQL
132
147
  # Internal: Nodes AST indexes.
133
148
  def indexes
134
149
  @indexes ||= begin
135
- visitor = GraphQL::Language::Visitor.new(document)
136
- definitions = index_node_definitions(visitor)
137
- spreads = index_spreads(visitor)
150
+ visitor = DefinitionVisitor.new(document)
138
151
  visitor.visit
139
- { definitions: definitions, spreads: spreads }
152
+ { definitions: visitor.definitions, spreads: visitor.spreads }
140
153
  end
141
154
  end
142
155
 
143
- private
156
+ class DefinitionVisitor < GraphQL::Language::Visitor
157
+ attr_reader :spreads, :definitions
144
158
 
145
- def cast_object(obj)
146
- 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}"
149
- end
150
- schema_class.cast(obj.to_h, obj.errors)
151
- else
152
- raise TypeError, "unexpected #{obj.class}"
153
- end
159
+ def initialize(doc)
160
+ super
161
+ @spreads = {}
162
+ @definitions = {}
163
+ @current_definition = nil
154
164
  end
155
165
 
156
- EMPTY_SET = Set.new.freeze
166
+ def on_field(node, parent)
167
+ @definitions[node] = @current_definition
168
+ @spreads[node] = get_spreads(node)
169
+ super
170
+ end
157
171
 
158
- def index_spreads(visitor)
159
- spreads = {}
160
- on_node = ->(node, _parent) do
161
- node_spreads = flatten_spreads(node).map(&:name)
162
- spreads[node] = node_spreads.empty? ? EMPTY_SET : Set.new(node_spreads).freeze
163
- end
172
+ def on_fragment_definition(node, parent)
173
+ @current_definition = node
174
+ @definitions[node] = @current_definition
175
+ @spreads[node] = get_spreads(node)
176
+ super
177
+ ensure
178
+ @current_definition = nil
179
+ end
164
180
 
165
- visitor[GraphQL::Language::Nodes::Field] << on_node
166
- visitor[GraphQL::Language::Nodes::FragmentDefinition] << on_node
167
- visitor[GraphQL::Language::Nodes::OperationDefinition] << on_node
181
+ def on_operation_definition(node, parent)
182
+ @current_definition = node
183
+ @definitions[node] = @current_definition
184
+ @spreads[node] = get_spreads(node)
185
+ super
186
+ ensure
187
+ @current_definition = nil
188
+ end
168
189
 
169
- spreads
190
+ def on_inline_fragment(node, parent)
191
+ @definitions[node] = @current_definition
192
+ super
193
+ end
194
+
195
+ private
196
+
197
+ EMPTY_SET = Set.new.freeze
198
+
199
+ def get_spreads(node)
200
+ node_spreads = flatten_spreads(node).map(&:name)
201
+ node_spreads.empty? ? EMPTY_SET : Set.new(node_spreads).freeze
170
202
  end
171
203
 
172
204
  def flatten_spreads(node)
@@ -183,25 +215,20 @@ module GraphQL
183
215
  end
184
216
  spreads
185
217
  end
218
+ end
219
+
220
+ private
186
221
 
187
- def index_node_definitions(visitor)
188
- current_definition = nil
189
- enter_definition = ->(node, _parent) { current_definition = node }
190
- leave_definition = ->(node, _parent) { current_definition = nil }
191
-
192
- visitor[GraphQL::Language::Nodes::FragmentDefinition].enter << enter_definition
193
- visitor[GraphQL::Language::Nodes::FragmentDefinition].leave << leave_definition
194
- visitor[GraphQL::Language::Nodes::OperationDefinition].enter << enter_definition
195
- visitor[GraphQL::Language::Nodes::OperationDefinition].leave << leave_definition
196
-
197
- definitions = {}
198
- on_node = ->(node, _parent) { definitions[node] = current_definition }
199
- visitor[GraphQL::Language::Nodes::Field] << on_node
200
- visitor[GraphQL::Language::Nodes::FragmentDefinition] << on_node
201
- visitor[GraphQL::Language::Nodes::InlineFragment] << on_node
202
- visitor[GraphQL::Language::Nodes::OperationDefinition] << on_node
203
- definitions
222
+ def cast_object(obj)
223
+ if obj.class.is_a?(GraphQL::Client::Schema::ObjectType)
224
+ unless obj._spreads.include?(definition_node.name)
225
+ raise TypeError, "#{definition_node.name} is not included in #{obj.source_definition.name}"
226
+ end
227
+ schema_class.cast(obj.to_h, obj.errors)
228
+ else
229
+ raise TypeError, "unexpected #{obj.class}"
204
230
  end
231
+ end
205
232
  end
206
233
  end
207
234
  end
@@ -24,26 +24,33 @@ module GraphQL
24
24
 
25
25
  sliced_document = GraphQL::Language::DefinitionSlice.slice(document, definition_name)
26
26
 
27
- visitor = GraphQL::Language::Visitor.new(sliced_document)
28
- type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor)
27
+ visitor = VariablesVisitor.new(sliced_document, schema: schema)
28
+ visitor.visit
29
+ visitor.variables
30
+ end
31
+
32
+ class VariablesVisitor < GraphQL::Language::Visitor
33
+ prepend GraphQL::Client::TypeStack
34
+
35
+ def initialize(*_args, **_kwargs)
36
+ super
37
+ @variables = {}
38
+ end
29
39
 
30
- variables = {}
40
+ attr_reader :variables
31
41
 
32
- visitor[GraphQL::Language::Nodes::VariableIdentifier] << ->(node, parent) do
33
- if definition = type_stack.argument_definitions.last
34
- existing_type = variables[node.name.to_sym]
42
+ def on_variable_identifier(node, parent)
43
+ if definition = @argument_definitions.last
44
+ existing_type = @variables[node.name.to_sym]
35
45
 
36
46
  if existing_type && existing_type.unwrap != definition.type.unwrap
37
47
  raise GraphQL::Client::ValidationError, "$#{node.name} was already declared as #{existing_type.unwrap}, but was #{definition.type.unwrap}"
38
48
  elsif !(existing_type && existing_type.kind.non_null?)
39
- variables[node.name.to_sym] = definition.type
49
+ @variables[node.name.to_sym] = definition.type
40
50
  end
41
51
  end
52
+ super
42
53
  end
43
-
44
- visitor.visit
45
-
46
- variables
47
54
  end
48
55
 
49
56
  # Internal: Detect all variables used in a given operation or fragment
@@ -1,10 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
  require "graphql"
3
+ require "graphql/client/type_stack"
3
4
 
4
5
  module GraphQL
5
6
  class Client
6
7
  # Internal: Use schema to detect definition and field types.
7
8
  module DocumentTypes
9
+ class AnalyzeTypesVisitor < GraphQL::Language::Visitor
10
+ prepend GraphQL::Client::TypeStack
11
+ attr_reader :fields
12
+
13
+ def initialize(*a, **kw)
14
+ @fields = {}
15
+ super
16
+ end
17
+
18
+ def on_operation_definition(node, _parent)
19
+ @fields[node] = @object_types.last
20
+ super
21
+ end
22
+
23
+ def on_fragment_definition(node, _parent)
24
+ @fields[node] = @object_types.last
25
+ super
26
+ end
27
+
28
+ def on_inline_fragment(node, _parent)
29
+ @fields[node] = @object_types.last
30
+ super
31
+ end
32
+
33
+ def on_field(node, _parent)
34
+ @fields[node] = @field_definitions.last.type
35
+ super
36
+ end
37
+ end
38
+
8
39
  # Internal: Detect all types used in a given document
9
40
  #
10
41
  # schema - A GraphQL::Schema
@@ -20,32 +51,15 @@ module GraphQL
20
51
  raise TypeError, "expected schema to be a GraphQL::Language::Nodes::Document, but was #{document.class}"
21
52
  end
22
53
 
23
- visitor = GraphQL::Language::Visitor.new(document)
24
- type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor)
25
-
26
- fields = {}
27
-
28
- visitor[GraphQL::Language::Nodes::OperationDefinition] << ->(node, _parent) do
29
- fields[node] = type_stack.object_types.last
30
- end
31
- visitor[GraphQL::Language::Nodes::FragmentDefinition] << ->(node, _parent) do
32
- fields[node] = type_stack.object_types.last
33
- end
34
- visitor[GraphQL::Language::Nodes::InlineFragment] << ->(node, _parent) do
35
- fields[node] = type_stack.object_types.last
36
- end
37
- visitor[GraphQL::Language::Nodes::Field] << ->(node, _parent) do
38
- fields[node] = type_stack.field_definitions.last.type
39
- end
54
+ visitor = AnalyzeTypesVisitor.new(document, schema: schema)
40
55
  visitor.visit
41
-
42
- fields
56
+ visitor.fields
43
57
  rescue StandardError => err
44
58
  if err.is_a?(TypeError)
45
59
  raise
46
60
  end
47
61
  # FIXME: TypeStack my crash on invalid documents
48
- fields
62
+ visitor.fields
49
63
  end
50
64
  end
51
65
  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
  #
@@ -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
 
@@ -46,56 +93,9 @@ module GraphQL
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
96
+ spreads = definition.indexes[:spreads][ast_nodes.first]
58
97
 
59
- klass
60
- end
61
-
62
- PREDICATE_CACHE = Hash.new { |h, name|
63
- h[name] = -> { @data[name] ? true : false }
64
- }
65
-
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)
@@ -132,14 +132,13 @@ module GraphQL
132
132
  case selected_ast_node
133
133
  when GraphQL::Language::Nodes::InlineFragment
134
134
  continue_selection = if selected_ast_node.type.nil?
135
- true
136
- else
137
- schema = definition.client.schema
138
- type_condition = definition.client.get_type(selected_ast_node.type.name)
139
- applicable_types = schema.possible_types(type_condition)
140
- # continue if this object type is one of the types matching the fragment condition
141
- applicable_types.include?(type)
142
- end
135
+ true
136
+ else
137
+ type_condition = definition.client.get_type(selected_ast_node.type.name)
138
+ applicable_types = definition.client.possible_types(type_condition)
139
+ # continue if this object type is one of the types matching the fragment condition
140
+ applicable_types.include?(type)
141
+ end
143
142
 
144
143
  if continue_selection
145
144
  selected_ast_node.selections.each do |next_selected_ast_node|
@@ -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
152
  type_condition = definition.client.get_type(fragment_definition.type.name)
156
- applicable_types = schema.possible_types(type_condition)
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,42 +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
- all_fields = type.respond_to?(:all_fields) ? type.all_fields : type.fields.values
215
- field = all_fields.find do |f|
216
- 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
217
213
  end
214
+ end
218
215
 
219
- unless field
220
- raise UnimplementedFieldError, "undefined field `#{e.name}' on #{type.graphql_name} 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
221
228
  end
229
+ end
222
230
 
223
- if @data.key?(field.name)
224
- error_class = ImplicitlyFetchedFieldError
225
- 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)
226
242
  else
227
- error_class = UnfetchedFieldError
228
- message = "unfetched field `#{field.name}' on #{type} type. https://git.io/v1y3U"
229
- end
243
+ begin
244
+ super
245
+ rescue NoMethodError => e
246
+ type = self.class.type
230
247
 
231
- 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
232
268
  end
233
269
 
234
270
  def inspect
235
- 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
236
275
 
237
276
  ivars = @data.map { |key, value|
238
277
  if value.is_a?(Hash) || value.is_a?(Array)
@@ -247,6 +286,26 @@ module GraphQL
247
286
  buf << ">"
248
287
  buf
249
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
250
309
  end
251
310
  end
252
311
  end
@@ -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
  }
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class Client
4
+ module TypeStack
5
+ # @return [GraphQL::Schema] the schema whose types are present in this document
6
+ attr_reader :schema
7
+
8
+ # When it enters an object (starting with query or mutation root), it's pushed on this stack.
9
+ # When it exits, it's popped off.
10
+ # @return [Array<GraphQL::ObjectType, GraphQL::Union, GraphQL::Interface>]
11
+ attr_reader :object_types
12
+
13
+ # When it enters a field, it's pushed on this stack (useful for nested fields, args).
14
+ # When it exits, it's popped off.
15
+ # @return [Array<GraphQL::Field>] fields which have been entered
16
+ attr_reader :field_definitions
17
+
18
+ # Directives are pushed on, then popped off while traversing the tree
19
+ # @return [Array<GraphQL::Node::Directive>] directives which have been entered
20
+ attr_reader :directive_definitions
21
+
22
+ # @return [Array<GraphQL::Node::Argument>] arguments which have been entered
23
+ attr_reader :argument_definitions
24
+
25
+ # @return [Array<String>] fields which have been entered (by their AST name)
26
+ attr_reader :path
27
+
28
+ # @param schema [GraphQL::Schema] the schema whose types to use when climbing this document
29
+ # @param visitor [GraphQL::Language::Visitor] a visitor to follow & watch the types
30
+ def initialize(document, schema:, **rest)
31
+ @schema = schema
32
+ @object_types = []
33
+ @field_definitions = []
34
+ @directive_definitions = []
35
+ @argument_definitions = []
36
+ @path = []
37
+ super(document, **rest)
38
+ end
39
+
40
+ def on_directive(node, parent)
41
+ directive_defn = @schema.directives[node.name]
42
+ @directive_definitions.push(directive_defn)
43
+ super(node, parent)
44
+ ensure
45
+ @directive_definitions.pop
46
+ end
47
+
48
+ def on_field(node, parent)
49
+ parent_type = @object_types.last
50
+ parent_type = parent_type.unwrap
51
+
52
+ field_definition = @schema.get_field(parent_type, node.name)
53
+ @field_definitions.push(field_definition)
54
+ if !field_definition.nil?
55
+ next_object_type = field_definition.type
56
+ @object_types.push(next_object_type)
57
+ else
58
+ @object_types.push(nil)
59
+ end
60
+ @path.push(node.alias || node.name)
61
+ super(node, parent)
62
+ ensure
63
+ @field_definitions.pop
64
+ @object_types.pop
65
+ @path.pop
66
+ end
67
+
68
+ def on_argument(node, parent)
69
+ if @argument_definitions.last
70
+ arg_type = @argument_definitions.last.type.unwrap
71
+ if arg_type.kind.input_object?
72
+ argument_defn = arg_type.arguments[node.name]
73
+ else
74
+ argument_defn = nil
75
+ end
76
+ elsif @directive_definitions.last
77
+ argument_defn = @directive_definitions.last.arguments[node.name]
78
+ elsif @field_definitions.last
79
+ argument_defn = @field_definitions.last.arguments[node.name]
80
+ else
81
+ argument_defn = nil
82
+ end
83
+ @argument_definitions.push(argument_defn)
84
+ @path.push(node.name)
85
+ super(node, parent)
86
+ ensure
87
+ @argument_definitions.pop
88
+ @path.pop
89
+ end
90
+
91
+ def on_operation_definition(node, parent)
92
+ # eg, QueryType, MutationType
93
+ object_type = @schema.root_type_for_operation(node.operation_type)
94
+ @object_types.push(object_type)
95
+ @path.push("#{node.operation_type}#{node.name ? " #{node.name}" : ""}")
96
+ super(node, parent)
97
+ ensure
98
+ @object_types.pop
99
+ @path.pop
100
+ end
101
+
102
+ def on_inline_fragment(node, parent)
103
+ object_type = if node.type
104
+ @schema.get_type(node.type.name)
105
+ else
106
+ @object_types.last
107
+ end
108
+ if !object_type.nil?
109
+ object_type = object_type.unwrap
110
+ end
111
+ @object_types.push(object_type)
112
+ @path.push("...#{node.type ? " on #{node.type.to_query_string}" : ""}")
113
+ super(node, parent)
114
+ ensure
115
+ @object_types.pop
116
+ @path.pop
117
+ end
118
+
119
+ def on_fragment_definition(node, parent)
120
+ object_type = if node.type
121
+ @schema.get_type(node.type.name)
122
+ else
123
+ @object_types.last
124
+ end
125
+ if !object_type.nil?
126
+ object_type = object_type.unwrap
127
+ end
128
+ @object_types.push(object_type)
129
+ @path.push("fragment #{node.name}")
130
+ super(node, parent)
131
+ ensure
132
+ @object_types.pop
133
+ @path.pop
134
+ end
135
+
136
+ def on_fragment_spread(node, parent)
137
+ @path.push("... #{node.name}")
138
+ super(node, parent)
139
+ ensure
140
+ @path.pop
141
+ end
142
+ end
143
+ end
144
+ 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
 
@@ -2,6 +2,7 @@
2
2
  require "active_support/inflector"
3
3
  require "active_support/notifications"
4
4
  require "graphql"
5
+ require "graphql/client/type_stack"
5
6
  require "graphql/client/collocated_enforcement"
6
7
  require "graphql/client/definition_variables"
7
8
  require "graphql/client/definition"
@@ -49,12 +50,12 @@ module GraphQL
49
50
  when GraphQL::Schema, Class
50
51
  schema
51
52
  when Hash
52
- GraphQL::Schema::Loader.load(schema)
53
+ GraphQL::Schema.from_introspection(schema)
53
54
  when String
54
55
  if schema.end_with?(".json") && File.exist?(schema)
55
56
  load_schema(File.read(schema))
56
57
  elsif schema =~ /\A\s*{/
57
- load_schema(JSON.parse(schema))
58
+ load_schema(JSON.parse(schema, freeze: true))
58
59
  end
59
60
  else
60
61
  if schema.respond_to?(:execute)
@@ -97,10 +98,31 @@ module GraphQL
97
98
  @document_tracking_enabled = false
98
99
  @allow_dynamic_queries = false
99
100
  @enforce_collocated_callers = enforce_collocated_callers
100
-
101
+ if schema.is_a?(Class)
102
+ @possible_types = schema.possible_types
103
+ end
101
104
  @types = Schema.generate(@schema)
102
105
  end
103
106
 
107
+ # A cache of the schema's merged possible types
108
+ # @param type_condition [Class, String] a type definition or type name
109
+ def possible_types(type_condition = nil)
110
+ if type_condition
111
+ if defined?(@possible_types)
112
+ if type_condition.respond_to?(:graphql_name)
113
+ type_condition = type_condition.graphql_name
114
+ end
115
+ @possible_types[type_condition]
116
+ else
117
+ @schema.possible_types(type_condition)
118
+ end
119
+ elsif defined?(@possible_types)
120
+ @possible_types
121
+ else
122
+ @schema.possible_types(type_condition)
123
+ end
124
+ end
125
+
104
126
  def parse(str, filename = nil, lineno = nil)
105
127
  if filename.nil? && lineno.nil?
106
128
  location = caller_locations(1, 1).first
@@ -135,11 +157,7 @@ module GraphQL
135
157
  # which corresponds to the spread.
136
158
  # We depend on ActiveSupport to either find the already-loaded
137
159
  # 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
160
+ fragment = ActiveSupport::Inflector.safe_constantize(const_name)
143
161
 
144
162
  case fragment
145
163
  when FragmentDefinition
@@ -173,12 +191,8 @@ module GraphQL
173
191
 
174
192
  doc.definitions.each do |node|
175
193
  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
194
+ node_with_name = node.merge(name: "__anonymous__")
195
+ doc = doc.replace_child(node, node_with_name)
182
196
  end
183
197
  end
184
198
 
@@ -200,37 +214,13 @@ module GraphQL
200
214
  raise error
201
215
  end
202
216
 
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
217
+ definitions = sliced_definitions(document_dependencies, doc, source_location: source_location)
215
218
 
216
- if @document.respond_to?(:merge) # GraphQL 1.9+
217
- visitor = RenameNodeVisitor.new(document_dependencies, definitions: definitions)
218
- visitor.visit
219
- else
220
- name_hook = RenameNodeHook.new(definitions)
221
- visitor = Language::Visitor.new(document_dependencies)
222
- visitor[Language::Nodes::FragmentDefinition].leave << name_hook.method(:rename_node)
223
- visitor[Language::Nodes::OperationDefinition].leave << name_hook.method(:rename_node)
224
- visitor[Language::Nodes::FragmentSpread].leave << name_hook.method(:rename_node)
225
- visitor.visit
226
- end
219
+ visitor = RenameNodeVisitor.new(document_dependencies, definitions: definitions)
220
+ visitor.visit
227
221
 
228
222
  if document_tracking_enabled
229
- if @document.respond_to?(:merge) # GraphQL 1.9+
230
- @document = @document.merge(definitions: @document.definitions + doc.definitions)
231
- else
232
- @document.definitions.concat(doc.definitions)
233
- end
223
+ @document = @document.merge(definitions: @document.definitions + doc.definitions)
234
224
  end
235
225
 
236
226
  if definitions["__anonymous__"]
@@ -276,27 +266,9 @@ module GraphQL
276
266
  end
277
267
  end
278
268
 
279
- class RenameNodeHook
280
- def initialize(definitions)
281
- @definitions = definitions
282
- end
283
-
284
- def rename_node(node, _parent)
285
- definition = @definitions[node.name]
286
- if definition
287
- node.extend(LazyName)
288
- node._definition = definition
289
- end
290
- end
291
- end
292
-
293
269
  # Public: A wrapper to use the more-efficient `.get_type` when it's available from GraphQL-Ruby (1.10+)
294
270
  def get_type(type_name)
295
- if @schema.respond_to?(:get_type)
296
- @schema.get_type(type_name)
297
- else
298
- @schema.types[type_name]
299
- end
271
+ @schema.get_type(type_name)
300
272
  end
301
273
 
302
274
  # Public: Create operation definition from a fragment definition.
@@ -424,6 +396,58 @@ module GraphQL
424
396
 
425
397
  private
426
398
 
399
+ def sliced_definitions(document_dependencies, doc, source_location:)
400
+ dependencies = document_dependencies.definitions.map do |node|
401
+ [node.name, find_definition_dependencies(node)]
402
+ end.to_h
403
+
404
+ doc.definitions.map do |node|
405
+ deps = Set.new
406
+ definitions = document_dependencies.definitions.map { |x| [x.name, x] }.to_h
407
+
408
+ queue = [node.name]
409
+ while name = queue.shift
410
+ next if deps.include?(name)
411
+ deps.add(name)
412
+ queue.concat dependencies[name]
413
+ end
414
+
415
+ definitions = document_dependencies.definitions.select { |x| deps.include?(x.name) }
416
+ sliced_document = Language::Nodes::Document.new(definitions: definitions)
417
+ definition = Definition.for(
418
+ client: self,
419
+ ast_node: node,
420
+ document: sliced_document,
421
+ source_document: doc,
422
+ source_location: source_location
423
+ )
424
+
425
+ [node.name, definition]
426
+ end.to_h
427
+ end
428
+
429
+ class GatherNamesVisitor < GraphQL::Language::Visitor
430
+ def initialize(node)
431
+ @names = []
432
+ super
433
+ end
434
+
435
+ attr_reader :names
436
+
437
+ def on_fragment_spread(node, parent)
438
+ @names << node.name
439
+ super
440
+ end
441
+ end
442
+
443
+ def find_definition_dependencies(node)
444
+ visitor = GatherNamesVisitor.new(node)
445
+ visitor.visit
446
+ names = visitor.names
447
+ names.uniq!
448
+ names
449
+ end
450
+
427
451
  def deep_freeze_json_object(obj)
428
452
  case obj
429
453
  when String
@@ -21,19 +21,12 @@ module RuboCop
21
21
  query, = ::GraphQL::Client::ViewModule.extract_graphql_section(erb)
22
22
  return unless query
23
23
 
24
- aliases = {}
25
- fields = {}
26
- ranges = {}
27
-
28
24
  # TODO: Use GraphQL client parser
29
25
  document = ::GraphQL.parse(query.gsub(/::/, "__"))
30
-
31
- visitor = ::GraphQL::Language::Visitor.new(document)
32
- visitor[::GraphQL::Language::Nodes::Field] << ->(node, _parent) do
33
- name = node.alias || node.name
34
- fields[name] ||= 0
35
- field_aliases(name).each { |n| (aliases[n] ||= []) << name }
36
- ranges[name] ||= source_range(processed_source.buffer, node.line, 0)
26
+ visitor = OverfetchVisitor.new(document) do |line_num|
27
+ # `source_range` is private to this object,
28
+ # so yield back out to it to get this info:
29
+ source_range(processed_source.buffer, line_num, 0)
37
30
  end
38
31
  visitor.visit
39
32
 
@@ -41,30 +34,53 @@ module RuboCop
41
34
  method_names = method_names_for(*node)
42
35
 
43
36
  method_names.each do |method_name|
44
- aliases.fetch(method_name, []).each do |field_name|
45
- fields[field_name] += 1
37
+ visitor.aliases.fetch(method_name, []).each do |field_name|
38
+ visitor.fields[field_name] += 1
46
39
  end
47
40
  end
48
41
  end
49
42
 
50
- fields.each do |field, count|
43
+ visitor.fields.each do |field, count|
51
44
  next if count > 0
52
- add_offense(nil, location: ranges[field], message: "GraphQL field '#{field}' query but was not used in template.")
45
+ add_offense(nil, location: visitor.ranges[field], message: "GraphQL field '#{field}' query but was not used in template.")
53
46
  end
54
47
  end
55
48
 
56
- def field_aliases(name)
57
- names = Set.new
49
+ class OverfetchVisitor < ::GraphQL::Language::Visitor
50
+ def initialize(doc, &range_for_line)
51
+ super(doc)
52
+ @range_for_line = range_for_line
53
+ @fields = {}
54
+ @aliases = {}
55
+ @ranges = {}
56
+ end
57
+
58
+ attr_reader :fields, :aliases, :ranges
59
+
60
+ def on_field(node, parent)
61
+ name = node.alias || node.name
62
+ fields[name] ||= 0
63
+ field_aliases(name).each { |n| (aliases[n] ||= []) << name }
64
+ ranges[name] ||= @range_for_line.call(node.line)
65
+ super
66
+ end
58
67
 
59
- names << name
60
- names << "#{name}?"
68
+ private
61
69
 
62
- names << underscore_name = ActiveSupport::Inflector.underscore(name)
63
- names << "#{underscore_name}?"
70
+ def field_aliases(name)
71
+ names = Set.new
64
72
 
65
- names
73
+ names << name
74
+ names << "#{name}?"
75
+
76
+ names << underscore_name = ActiveSupport::Inflector.underscore(name)
77
+ names << "#{underscore_name}?"
78
+
79
+ names
80
+ end
66
81
  end
67
82
 
83
+
68
84
  def method_names_for(*node)
69
85
  receiver, method_name, *_args = node
70
86
  method_names = []
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.16.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-10-09 00:00:00.000000000 Z
11
+ date: 2024-01-24 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
@@ -100,42 +100,42 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '11.2'
103
+ version: 13.1.0
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '11.2'
110
+ version: 13.1.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rubocop-github
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - "~>"
115
+ - - ">="
116
116
  - !ruby/object:Gem::Version
117
- version: '0.10'
117
+ version: '0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - "~>"
122
+ - - ">="
123
123
  - !ruby/object:Gem::Version
124
- version: '0.10'
124
+ version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: rubocop
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0.55'
131
+ version: 1.57.0
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '0.55'
138
+ version: 1.57.0
139
139
  description: A Ruby library for declaring, composing and executing GraphQL queries
140
140
  email: engineering@github.com
141
141
  executables: []
@@ -176,6 +176,7 @@ files:
176
176
  - lib/graphql/client/schema/scalar_type.rb
177
177
  - lib/graphql/client/schema/skip_directive.rb
178
178
  - lib/graphql/client/schema/union_type.rb
179
+ - lib/graphql/client/type_stack.rb
179
180
  - lib/graphql/client/view_module.rb
180
181
  - lib/rubocop/cop/graphql/heredoc.rb
181
182
  - lib/rubocop/cop/graphql/overfetch.rb
@@ -183,7 +184,7 @@ homepage: https://github.com/github/graphql-client
183
184
  licenses:
184
185
  - MIT
185
186
  metadata: {}
186
- post_install_message:
187
+ post_install_message:
187
188
  rdoc_options: []
188
189
  require_paths:
189
190
  - lib
@@ -198,8 +199,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
198
199
  - !ruby/object:Gem::Version
199
200
  version: '0'
200
201
  requirements: []
201
- rubygems_version: 3.0.3
202
- signing_key:
202
+ rubygems_version: 3.1.6
203
+ signing_key:
203
204
  specification_version: 4
204
205
  summary: GraphQL Client
205
206
  test_files: []