graphql-client 0.3.0 → 0.4.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
  SHA1:
3
- metadata.gz: 490142029326d682bd6956d1f825d9429aa49389
4
- data.tar.gz: 3cc776110140c024c801f8b3a9eb52bf72cc924f
3
+ metadata.gz: 88afea7eaa70819f11b2a000886ef3924e2b0e12
4
+ data.tar.gz: 62a8706efe7fe20a73da2affaa3e362dce686970
5
5
  SHA512:
6
- metadata.gz: 2e3cba4f02d0fff593cadcbd0d39bcdecb0576f087bb9088f2dc5902426bb54b7829ea3eb7d8d96c86fae155eb6ba248c5414ba24006834c8a4d806063e1be4c
7
- data.tar.gz: bb8b0a56f1f1d0be7a4315aa626af65a08e7285adb03fb02e382a0fb6fdb53e67326ed6654f171709f267897141e396813ac2245f6c45097157b78f492dba7da
6
+ metadata.gz: b9774d7f5229a0202924ff253b251076ed7b5f71dd3e32f0707467663868faace9a331b79878e70159058ebbdde59b477892b13c662c1e3f9492fa31e6901551
7
+ data.tar.gz: 0d4e18520e7b23a28012edf77a98913358ef7793e70f46ddbdf881ad63c51eafc83325ef7a388d37b752914d55b0f6e9d23dc50efbb23afc98bf1f5aebd25be0
@@ -2,6 +2,7 @@
2
2
  require "active_support/inflector"
3
3
  require "active_support/notifications"
4
4
  require "graphql"
5
+ require "graphql/client/collocated_enforcement"
5
6
  require "graphql/client/error"
6
7
  require "graphql/client/errors"
7
8
  require "graphql/client/query_result"
@@ -22,10 +23,15 @@ module GraphQL
22
23
  class NotImplementedError < Error; end
23
24
  class ValidationError < Error; end
24
25
 
26
+ extend CollocatedEnforcement
27
+
25
28
  attr_reader :schema, :execute
26
29
 
27
30
  attr_accessor :document_tracking_enabled
28
31
 
32
+ # Public: Check if collocated caller enforcement is enabled.
33
+ attr_reader :enforce_collocated_callers
34
+
29
35
  # Deprecated: Allow dynamically generated queries to be passed to
30
36
  # Client#query.
31
37
  #
@@ -72,12 +78,13 @@ module GraphQL
72
78
  result
73
79
  end
74
80
 
75
- def initialize(schema: nil, execute: nil)
81
+ def initialize(schema: nil, execute: nil, enforce_collocated_callers: false)
76
82
  @schema = self.class.load_schema(schema)
77
83
  @execute = execute
78
84
  @document = GraphQL::Language::Nodes::Document.new(definitions: [])
79
85
  @document_tracking_enabled = false
80
86
  @allow_dynamic_queries = false
87
+ @enforce_collocated_callers = enforce_collocated_callers
81
88
  end
82
89
 
83
90
  # Definitions are constructed by Client.parse and wrap a parsed AST of the
@@ -97,11 +104,13 @@ module GraphQL
97
104
  end
98
105
  end
99
106
 
100
- def initialize(node:, document:, schema:, document_types:)
107
+ def initialize(node:, document:, schema:, document_types:, source_location:, enforce_collocated_callers:)
101
108
  @definition_node = node
102
109
  @document = document
103
110
  @schema = schema
104
111
  @document_types = document_types
112
+ @source_location = source_location
113
+ @enforce_collocated_callers = enforce_collocated_callers
105
114
  end
106
115
 
107
116
  # Internal: Get underlying operation or fragment defintion AST node for
@@ -134,15 +143,26 @@ module GraphQL
134
143
  # and any FragmentDefinition dependencies.
135
144
  attr_reader :document
136
145
 
146
+ # Internal: Mapping of document nodes to schema types.
147
+ attr_reader :document_types
148
+
137
149
  attr_reader :schema
138
150
 
151
+ # Public: Returns the Ruby source filename and line number containing this
152
+ # definition was not defined in Ruby.
153
+ #
154
+ # Returns Array pair of [String, Fixnum].
155
+ attr_reader :source_location
156
+
157
+ attr_reader :enforce_collocated_callers
158
+
139
159
  def new(*args)
140
160
  type.new(*args)
141
161
  end
142
162
 
143
163
  def type
144
164
  # TODO: Fix type indirection
145
- @type ||= GraphQL::Client::QueryResult.wrap(definition_node, name: "#{name}.type", types: @document_types)
165
+ @type ||= GraphQL::Client::QueryResult.wrap(self, definition_node, name: "#{name}.type")
146
166
  end
147
167
  end
148
168
 
@@ -158,11 +178,22 @@ module GraphQL
158
178
  end
159
179
 
160
180
  def parse(str, filename = nil, lineno = nil)
161
- if filename.nil? || lineno.nil?
162
- filename, lineno, = caller(1, 1).first.split(":", 3)
163
- lineno = lineno.to_i
181
+ if filename.nil? && lineno.nil?
182
+ location = caller_locations(1, 1).first
183
+ filename = location.path
184
+ lineno = location.lineno
164
185
  end
165
186
 
187
+ unless filename.is_a?(String)
188
+ raise TypeError, "expected filename to be a String, but was #{filename.class}"
189
+ end
190
+
191
+ unless lineno.is_a?(Integer)
192
+ raise TypeError, "expected lineno to be a Integer, but was #{lineno.class}"
193
+ end
194
+
195
+ source_location = [filename, lineno].freeze
196
+
166
197
  definition_dependencies = Set.new
167
198
 
168
199
  str = str.gsub(/\.\.\.([a-zA-Z0-9_]+(::[a-zA-Z0-9_]+)+)/) do
@@ -186,9 +217,7 @@ module GraphQL
186
217
  end
187
218
 
188
219
  error = ValidationError.new(message)
189
- if filename && lineno
190
- error.set_backtrace(["#{filename}:#{lineno + match.pre_match.count("\n") + 1}"] + caller)
191
- end
220
+ error.set_backtrace(["#{filename}:#{lineno + match.pre_match.count("\n") + 1}"] + caller)
192
221
  raise error
193
222
  end
194
223
  end
@@ -211,7 +240,7 @@ module GraphQL
211
240
  error_hash = error.to_h
212
241
  validation_line = error_hash["locations"][0]["line"]
213
242
  error = ValidationError.new(error_hash["message"])
214
- error.set_backtrace(["#{filename}:#{lineno + validation_line}"] + caller) if filename && lineno
243
+ error.set_backtrace(["#{filename}:#{lineno + validation_line}"] + caller)
215
244
  raise error
216
245
  end
217
246
 
@@ -230,7 +259,9 @@ module GraphQL
230
259
  schema: @schema,
231
260
  node: node,
232
261
  document: sliced_document,
233
- document_types: document_types
262
+ document_types: document_types,
263
+ source_location: source_location,
264
+ enforce_collocated_callers: enforce_collocated_callers
234
265
  )
235
266
  definitions[node.name] = definition
236
267
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ require "graphql/client/error"
3
+
4
+ module GraphQL
5
+ class Client
6
+ # Raised when method is called from outside the expected file scope.
7
+ class NonCollocatedCallerError < Error; end
8
+
9
+ # Enforcements collocated object access best practices.
10
+ module CollocatedEnforcement
11
+ # Public: Ignore collocated caller enforcement for the scope of the block.
12
+ def allow_noncollocated_callers
13
+ Thread.current[:query_result_caller_location_ignore] = true
14
+ yield
15
+ ensure
16
+ Thread.current[:query_result_caller_location_ignore] = nil
17
+ end
18
+
19
+ # Internal: Decorate method with collocated caller enforcement.
20
+ #
21
+ # mod - Target Module/Class
22
+ # methods - Array of Symbol method names
23
+ # path - String filename to assert calling from
24
+ #
25
+ # Returns nothing.
26
+ def enforce_collocated_callers(mod, methods, path)
27
+ mod.prepend(Module.new do
28
+ methods.each do |method|
29
+ define_method(method) do |*args, &block|
30
+ return super(*args, &block) if Thread.current[:query_result_caller_location_ignore]
31
+
32
+ locations = caller_locations(1)
33
+ if locations.first.path != path
34
+ error = NonCollocatedCallerError.new("#{method} was called outside of '#{path}' https://git.io/v1syX")
35
+ error.set_backtrace(locations.map(&:to_s))
36
+ raise error
37
+ end
38
+
39
+ begin
40
+ Thread.current[:query_result_caller_location_ignore] = true
41
+ super(*args, &block)
42
+ ensure
43
+ Thread.current[:query_result_caller_location_ignore] = nil
44
+ end
45
+ end
46
+ end
47
+ end)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -17,7 +17,7 @@ module GraphQL
17
17
  # Internal: Get QueryResult class for result of query.
18
18
  #
19
19
  # Returns subclass of QueryResult or nil.
20
- def self.wrap(node, name: nil, types: {})
20
+ def self.wrap(source_definition, node, name: nil)
21
21
  fields = {}
22
22
 
23
23
  node.selections.each do |selection|
@@ -27,40 +27,74 @@ module GraphQL
27
27
  when Language::Nodes::Field
28
28
  field_name = selection.alias || selection.name
29
29
  field_klass = nil
30
- field_klass = wrap(selection, name: "#{name}[:#{field_name}]", types: types) if selection.selections.any?
30
+ if selection.selections.any?
31
+ field_klass = wrap(source_definition, selection, name: "#{name}[:#{field_name}]")
32
+ end
31
33
  fields[field_name] ? fields[field_name] |= field_klass : fields[field_name] = field_klass
32
34
  when Language::Nodes::InlineFragment
33
- wrap(selection, name: name, types: types).fields.each do |fragment_name, klass|
35
+ wrap(source_definition, selection, name: name).fields.each do |fragment_name, klass|
34
36
  fields[fragment_name.to_s] ? fields[fragment_name.to_s] |= klass : fields[fragment_name.to_s] = klass
35
37
  end
36
38
  end
37
39
  end
38
40
 
39
- define(name: name, source_node: node, fields: fields, type: types[node] && types[node].unwrap)
41
+ define(name: name, source_definition: source_definition, source_node: node, fields: fields)
42
+ end
43
+
44
+ # :nodoc:
45
+ class Scalar
46
+ def initialize(type)
47
+ @type = type
48
+ end
49
+
50
+ def cast(value, _errors = nil)
51
+ @type.coerce_input(value)
52
+ end
53
+
54
+ def |(_other)
55
+ # XXX: How would scalars merge?
56
+ self
57
+ end
40
58
  end
41
59
 
42
60
  # Internal
43
- def self.define(name:, source_node:, fields: {}, type: nil)
61
+ def self.define(name:, source_definition:, source_node:, fields: {})
62
+ type = source_definition.document_types[source_node]
63
+ type = type.unwrap if type
64
+
44
65
  Class.new(self) do
45
66
  @name = name
46
67
  @type = type
47
68
  @source_node = source_node
69
+ @source_definition = source_definition
48
70
  @fields = {}
49
71
 
72
+ field_readers = Set.new
73
+
50
74
  fields.each do |field, klass|
75
+ if @type.is_a?(GraphQL::ObjectType)
76
+ field_node = @type.fields[field.to_s]
77
+ if field_node && field_node.type.unwrap.is_a?(GraphQL::ScalarType)
78
+ klass = Scalar.new(field_node.type.unwrap)
79
+ end
80
+ end
81
+
51
82
  @fields[field.to_sym] = klass
52
83
 
53
84
  send :attr_reader, field
85
+ field_readers << field.to_sym
54
86
 
55
87
  # Convert GraphQL camelcase to snake case: commitComments -> commit_comments
56
88
  field_alias = ActiveSupport::Inflector.underscore(field)
57
89
  send :alias_method, field_alias, field if field != field_alias
90
+ field_readers << field_alias.to_sym
58
91
 
59
92
  class_eval <<-RUBY, __FILE__, __LINE__
60
93
  def #{field_alias}?
61
94
  #{field_alias} ? true : false
62
95
  end
63
96
  RUBY
97
+ field_readers << "#{field_alias}?".to_sym
64
98
 
65
99
  next unless field == "edges"
66
100
  class_eval <<-RUBY, __FILE__, __LINE__
@@ -70,9 +104,10 @@ module GraphQL
70
104
  self
71
105
  end
72
106
  RUBY
107
+ field_readers << :each_node
73
108
  end
74
109
 
75
- assigns = fields.map do |field, klass|
110
+ assigns = @fields.map do |field, klass|
76
111
  if klass
77
112
  <<-RUBY
78
113
  @#{field} = self.class.fields[:#{field}].cast(@data["#{field}"], @errors.filter_by_path("#{field}"))
@@ -97,12 +132,18 @@ module GraphQL
97
132
  freeze
98
133
  end
99
134
  RUBY
135
+
136
+ if @source_definition.enforce_collocated_callers
137
+ Client.enforce_collocated_callers(self, field_readers, source_definition.source_location[0])
138
+ end
100
139
  end
101
140
  end
102
141
 
103
142
  class << self
104
143
  attr_reader :type
105
144
 
145
+ attr_reader :source_definition
146
+
106
147
  attr_reader :source_node
107
148
 
108
149
  attr_reader :fields
@@ -175,7 +216,7 @@ module GraphQL
175
216
  end
176
217
  end
177
218
  # TODO: Picking first source node seems error prone
178
- define(name: self.name, source_node: source_node, fields: new_fields)
219
+ define(name: self.name, source_definition: source_definition, source_node: source_node, fields: new_fields)
179
220
  end
180
221
 
181
222
  # Public: Return errors associated with data.
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-29 00:00:00.000000000 Z
11
+ date: 2016-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -34,16 +34,16 @@ dependencies:
34
34
  name: graphql
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
37
+ - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.19.2
39
+ version: '1.2'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - ">="
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.19.2
46
+ version: '1.2'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: actionpack
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -115,6 +115,7 @@ files:
115
115
  - LICENSE
116
116
  - README.md
117
117
  - lib/graphql/client.rb
118
+ - lib/graphql/client/collocated_enforcement.rb
118
119
  - lib/graphql/client/document_types.rb
119
120
  - lib/graphql/client/error.rb
120
121
  - lib/graphql/client/errors.rb