graphql-remote_loader 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []