graphql-remote_loader 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 19a40470289cc44591c9356bf76c842507b9ac83
4
+ data.tar.gz: 043fe953bb41e7b8c19568463ef0679e00b35995
5
+ SHA512:
6
+ metadata.gz: 90d83694e6182d95af2f20291def1f80edf8535eebf5738250585f9e1e6cb5e6c212e1ea9f27efe76bef9e79092863b1647b4122f6ad009ca91f23c4e9ef27bf
7
+ data.tar.gz: 69db4b3fa095275f946a23b4ce6edbe0d97bd2461d0282e283540338543bba77dd43d1669af299c786776ef8feefda7fb8f9e63e594d103fb7f56f8b68baedb4
@@ -0,0 +1 @@
1
+ .byebug_history
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
@@ -0,0 +1,52 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ graphql-remote_loader (0.0.1)
5
+ graphql (~> 1.6)
6
+ graphql-batch (~> 0.3)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ byebug (9.1.0)
12
+ coderay (1.1.2)
13
+ diff-lcs (1.3)
14
+ graphql (1.7.14)
15
+ graphql-batch (0.3.9)
16
+ graphql (>= 0.8, < 2)
17
+ promise.rb (~> 0.7.2)
18
+ method_source (0.9.0)
19
+ promise.rb (0.7.4)
20
+ pry (0.11.3)
21
+ coderay (~> 1.1.0)
22
+ method_source (~> 0.9.0)
23
+ pry-byebug (3.5.0)
24
+ byebug (~> 9.1)
25
+ pry (~> 0.10)
26
+ rake (10.4.2)
27
+ rspec (3.6.0)
28
+ rspec-core (~> 3.6.0)
29
+ rspec-expectations (~> 3.6.0)
30
+ rspec-mocks (~> 3.6.0)
31
+ rspec-core (3.6.0)
32
+ rspec-support (~> 3.6.0)
33
+ rspec-expectations (3.6.0)
34
+ diff-lcs (>= 1.2.0, < 2.0)
35
+ rspec-support (~> 3.6.0)
36
+ rspec-mocks (3.6.0)
37
+ diff-lcs (>= 1.2.0, < 2.0)
38
+ rspec-support (~> 3.6.0)
39
+ rspec-support (3.6.0)
40
+
41
+ PLATFORMS
42
+ ruby
43
+
44
+ DEPENDENCIES
45
+ bundler (~> 1.14)
46
+ graphql-remote_loader!
47
+ pry-byebug (~> 3.4)
48
+ rake (~> 10.0)
49
+ rspec
50
+
51
+ BUNDLED WITH
52
+ 1.15.4
@@ -0,0 +1,9 @@
1
+ require "graphql"
2
+ require "graphql/batch"
3
+
4
+ require_relative "remote_loader/loader"
5
+
6
+ module GraphQL
7
+ module RemoteLoader
8
+ end
9
+ end
@@ -0,0 +1,66 @@
1
+ require "prime"
2
+ require_relative "query_merger"
3
+
4
+ module GraphQL
5
+ module RemoteLoader
6
+ class Loader < GraphQL::Batch::Loader
7
+ # Delegates to GraphQL::Batch::Loader#load
8
+ # We include a unique prime as part of the batch key to use as part
9
+ # of the alias on all fields. This is used to
10
+ # a) Avoid name collisions in the generated query
11
+ # b) Determine which fields in the result JSON should be
12
+ # handed fulfilled to each promise
13
+ def self.load(query)
14
+ @index ||= 1
15
+ @index += 1
16
+
17
+ prime = Prime.take(@index - 1).last
18
+
19
+ self.for.load([query, prime])
20
+ end
21
+
22
+ # Given a query string, return a response JSON
23
+ def query(query_string)
24
+ raise NotImplementedError,
25
+ "RemoteLoader::Loader should be subclassed and #query must be defined"
26
+ end
27
+
28
+ private
29
+
30
+ def perform(queries_and_primes)
31
+ query_string = QueryMerger.merge(queries_and_primes)
32
+ response = query(query_string)
33
+
34
+ queries_and_primes.each do |query, prime|
35
+ fulfill([query, prime], filter_keys_on_response(response.to_h, prime))
36
+ end
37
+ end
38
+
39
+ def filter_keys_on_response(obj, prime)
40
+ return obj unless obj.is_a? Hash
41
+ filtered_results = {}
42
+
43
+ # Select field keys on the results hash
44
+ fields = obj.keys.select { |k| k.match? /\Ap[0-9]+.*[^?]\z/ }
45
+
46
+ # Filter methods that were not requested in this sub-query
47
+ fields = fields.select do |field|
48
+ prime_factor = field.match(/\Ap([0-9]+)/)[1].to_i
49
+ (prime_factor % prime) == 0
50
+ end
51
+
52
+ # redefine methods on new obj, recursively filter sub-selections
53
+ fields.each do |method|
54
+ method_name = method.match(/\Ap[0-9]+(.*)/)[1]
55
+
56
+ method_value = obj[method]
57
+ filtered_value = filter_keys_on_response(method_value, prime)
58
+
59
+ filtered_results[method_name.underscore] = filtered_value
60
+ end
61
+
62
+ filtered_results
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,301 @@
1
+ module GraphQL
2
+ module RemoteLoader
3
+
4
+ # Given a list of queries and their prime UIDs, generate the merged and labeled
5
+ # GraphQL query to be sent off to the remote backend.
6
+ class QueryMerger
7
+ class << self
8
+ def merge(queries_and_primes)
9
+ new_ast = []
10
+
11
+ queries_and_primes.each do |query, prime|
12
+ parsed_query = [parse(query)]
13
+ attach_primes!(parsed_query, prime)
14
+ merge_query(parsed_query, new_ast)
15
+ end
16
+
17
+ query_string_from_ast(new_ast)
18
+ end
19
+
20
+ private
21
+
22
+ def merge_query(query, ast)
23
+ query.each do |query_field|
24
+ matching_field = ast.find do |ast_field|
25
+ query_field[:field] == ast_field[:field] &&
26
+ query_field[:arguments] == ast_field[:arguments]
27
+ end
28
+
29
+ if matching_field
30
+ matching_field[:primes] *= query_field[:primes]
31
+ merge_query(query_field[:body], matching_field[:body])
32
+ else
33
+ ast << query_field
34
+ end
35
+ end
36
+ end
37
+
38
+ def attach_primes!(query, prime)
39
+ query.each do |field|
40
+ field[:primes] = prime
41
+ attach_primes!(field[:body], prime)
42
+ end
43
+ end
44
+
45
+ def query_string_from_ast(ast)
46
+ ast_node_strings = ast.map do |ast_node|
47
+ query_string_for_node(ast_node)
48
+ end.join(" ")
49
+ ast_node_strings
50
+ end
51
+
52
+ def query_string_for_node(node)
53
+ field = node[:field]
54
+
55
+ # TODO: This won't work for fields named `query`. Fix once
56
+ # I move to Roberts non-jank parser
57
+ unless field.include?("...") || field == "query"
58
+ # assign preix if not a spread
59
+ field = "p#{node[:primes]}#{field}: #{field}"
60
+ end
61
+
62
+ args = node[:arguments].map do |arg, value|
63
+ value = "\"#{value}\"" if value.is_a? String
64
+
65
+ "#{arg}: #{value}"
66
+ end
67
+
68
+ arg_string = unless args.empty?
69
+ "(#{args.join(",")})"
70
+ else
71
+ ""
72
+ end
73
+
74
+ body_string = unless node[:body].empty?
75
+ str = node[:body].map do |node|
76
+ query_string_for_node(node)
77
+ end
78
+
79
+ "{ #{str.join(" ")} }"
80
+ end
81
+
82
+ "#{field}#{arg_string} #{body_string}".strip
83
+ end
84
+
85
+ def parse(query)
86
+ tokenizer = QueryTokenizer.new("query { #{query} }")
87
+ QueryAST.build(tokenizer)
88
+ end
89
+
90
+ class QueryAST
91
+ class ParseException < Exception; end
92
+
93
+ class << self
94
+ def build(tokenizer)
95
+ result = build_node(tokenizer)
96
+ end
97
+
98
+ def build_node(tokenizer)
99
+ {
100
+ field: get_field(tokenizer),
101
+ arguments: get_args(tokenizer),
102
+ body: build_body(tokenizer)
103
+ }
104
+ end
105
+
106
+ def get_field(tokenizer)
107
+ token = tokenizer.pop
108
+
109
+ if token[:type] == :spread
110
+ validate_token_type(tokenizer.pop, :on)
111
+ spread_type = tokenizer.pop[:string]
112
+
113
+ return "... on #{spread_type}"
114
+ end
115
+
116
+ validate_token_type(token, :field)
117
+
118
+ throw_parse_exception(token, "field") unless (token[:type] == :field)
119
+
120
+ token[:string]
121
+ end
122
+
123
+ def get_args(tokenizer)
124
+ args = {}
125
+ return args unless (tokenizer.peek[:type] == :left_paren)
126
+
127
+ # remove "("
128
+ tokenizer.pop
129
+
130
+ while true
131
+ token = tokenizer.pop
132
+ validate_token_type(token, :key)
133
+
134
+ # Remove :
135
+ key = token[:string].sub(":", "")
136
+
137
+ token = tokenizer.pop
138
+ value = case token[:type]
139
+ when :string
140
+ token[:string][1..-2] # remove ""
141
+ when :int
142
+ Integer(token[:string])
143
+ when :float
144
+ Float(token[:string])
145
+ when :true
146
+ true
147
+ when :false
148
+ false
149
+ else
150
+ raise ParseException, "Expected string, int, float, or boolean, got #{token[:string]}"
151
+ end
152
+
153
+ args[key.to_sym] = value
154
+
155
+ if tokenizer.peek[:type] == :comma
156
+ tokenizer.pop
157
+ elsif tokenizer.peek[:type] == :right_paren
158
+ tokenizer.pop
159
+ break
160
+ else
161
+ raise ParseException, "Expected comma or right paren, got #{tokenizer[:string]}"
162
+ end
163
+ end
164
+
165
+ args
166
+ end
167
+
168
+ def build_body(tokenizer)
169
+ body = []
170
+ return body unless (tokenizer.peek[:type] == :left_brace)
171
+
172
+ # remove "{"
173
+ tokenizer.pop
174
+
175
+ while true
176
+ body << build_node(tokenizer)
177
+ break if (tokenizer.peek[:type] == :right_brace)
178
+ end
179
+
180
+ # remove "}"
181
+ tokenizer.pop
182
+
183
+ body
184
+ end
185
+
186
+ def validate_token_type(token, expected_type)
187
+ unless token[:type] == expected_type
188
+ raise ParseException, "Expected #{expected_type.to_s} token, got #{token[:string]}"
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ class QueryTokenizer
195
+ def initialize(query)
196
+ @query = query.chars
197
+ set_next_token
198
+ end
199
+
200
+ # Get next token, without removing from stream
201
+ def peek
202
+ @next_token
203
+ end
204
+
205
+ # Pop next token
206
+ def pop
207
+ token = @next_token
208
+ set_next_token if token
209
+
210
+ token
211
+ end
212
+
213
+ private
214
+
215
+ # Read @query to set @next_token
216
+ def set_next_token
217
+ next_token_string = extract_token_string_from_query
218
+ @next_token = tokenize_string(next_token_string)
219
+ end
220
+
221
+ def extract_token_string_from_query
222
+ trim_leading_whitespace
223
+
224
+ token_string = ""
225
+ while true
226
+ next_char = @query.first || break
227
+
228
+ if is_brace_or_bracket_or_comma?(next_char)
229
+ if token_string.length == 0
230
+ token_string << @query.shift
231
+ end
232
+
233
+ break
234
+ else
235
+ if !is_whitespace?(@query.first)
236
+ token_string << @query.shift
237
+ else
238
+ break
239
+ end
240
+ end
241
+ end
242
+
243
+ token_string
244
+ end
245
+
246
+ def tokenize_string(next_token_string)
247
+ token = case next_token_string
248
+ when '{'
249
+ :left_brace
250
+ when '}'
251
+ :right_brace
252
+ when '('
253
+ :left_paren
254
+ when ')'
255
+ :right_paren
256
+ when ','
257
+ :comma
258
+ when /\A[[:digit:]]+\z/
259
+ :int
260
+ when /\A[[:digit:]]+\.[[:digit:]]+\z/
261
+ :float
262
+ when 'true'
263
+ :true
264
+ when 'false'
265
+ :false
266
+ when 'on'
267
+ :on
268
+ when /\A".*"\z/
269
+ :string
270
+ when /\A.+:\z/
271
+ :key
272
+ when ''
273
+ next_token_string = "END OF STREAM"
274
+ :empty
275
+ when '...'
276
+ :spread
277
+ else
278
+ :field
279
+ end
280
+
281
+ { type: token, string: next_token_string }
282
+ end
283
+
284
+ def trim_leading_whitespace
285
+ while is_whitespace?(@query.first)
286
+ @query.shift
287
+ end
288
+ end
289
+
290
+ def is_whitespace?(char)
291
+ char == "\n" || char == " "
292
+ end
293
+
294
+ def is_brace_or_bracket_or_comma?(char)
295
+ char == "{" || char == "}" || char == "(" || char == ")" || char == ","
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,5 @@
1
+ module GraphQL
2
+ module RemoteLoader
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'graphql/remote_loader/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "graphql-remote_loader"
8
+ spec.version = GraphQL::RemoteLoader::VERSION
9
+ spec.authors = ["Nathaniel Woodthorpe"]
10
+ spec.email = ["d12@github.com", "njwoodthorpe@gmail.com"]
11
+
12
+ spec.summary = "Performant remote GraphQL queries from within a Ruby GraphQL API."
13
+ spec.description = "GraphQL::RemoteLoader allows performantly fetching data from remote GraphQL APIs in the resolvers of a graphql-ruby API."
14
+ spec.homepage = "http://nwoodthorpe.com"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ # spec.bindir = "bin"
21
+ # spec.executables = [""]
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "graphql", "~> 1.6"
25
+ spec.add_dependency "graphql-batch", "~> 0.3"
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.14"
28
+ spec.add_development_dependency "rake", "~> 10.0"
29
+ spec.add_development_dependency "rspec", "~> 3.6"
30
+ spec.add_development_dependency "pry-byebug", "~> 3.4"
31
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql-remote_loader
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Nathaniel Woodthorpe
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: graphql
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: graphql-batch
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.14'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.14'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.6'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.6'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.4'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.4'
97
+ description: GraphQL::RemoteLoader allows performantly fetching data from remote GraphQL
98
+ APIs in the resolvers of a graphql-ruby API.
99
+ email:
100
+ - d12@github.com
101
+ - njwoodthorpe@gmail.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - Gemfile
108
+ - Gemfile.lock
109
+ - lib/graphql/remote_loader.rb
110
+ - lib/graphql/remote_loader/loader.rb
111
+ - lib/graphql/remote_loader/query_merger.rb
112
+ - lib/graphql/remote_loader/version.rb
113
+ - remote_graphql_loader.gemspec
114
+ homepage: http://nwoodthorpe.com
115
+ licenses:
116
+ - MIT
117
+ metadata: {}
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 2.5.2
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Performant remote GraphQL queries from within a Ruby GraphQL API.
138
+ test_files: []