artemis 0.1.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +36 -0
  5. data/Appraisals +32 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +10 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +181 -0
  10. data/Rakefile +14 -0
  11. data/artemis.gemspec +28 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/gemfiles/.bundle/config +2 -0
  15. data/gemfiles/rails_42.gemfile +12 -0
  16. data/gemfiles/rails_50.gemfile +11 -0
  17. data/gemfiles/rails_51.gemfile +11 -0
  18. data/gemfiles/rails_52.gemfile +11 -0
  19. data/gemfiles/rails_edge.gemfile +14 -0
  20. data/lib/artemis.rb +3 -0
  21. data/lib/artemis/adapters.rb +25 -0
  22. data/lib/artemis/adapters/abstract_adapter.rb +46 -0
  23. data/lib/artemis/adapters/curb_adapter.rb +56 -0
  24. data/lib/artemis/adapters/net_http_adapter.rb +51 -0
  25. data/lib/artemis/adapters/net_http_persistent_adapter.rb +46 -0
  26. data/lib/artemis/adapters/test_adapter.rb +29 -0
  27. data/lib/artemis/client.rb +245 -0
  28. data/lib/artemis/exceptions.rb +19 -0
  29. data/lib/artemis/graphql_endpoint.rb +54 -0
  30. data/lib/artemis/railtie.rb +50 -0
  31. data/lib/artemis/version.rb +3 -0
  32. data/lib/generators/artemis/USAGE +11 -0
  33. data/lib/generators/artemis/install_generator.rb +59 -0
  34. data/lib/generators/artemis/templates/.gitkeep +0 -0
  35. data/lib/generators/artemis/templates/client.rb +10 -0
  36. data/lib/generators/artemis/templates/graphql.yml +19 -0
  37. data/lib/tasks/artemis.rake +47 -0
  38. data/spec/adapters_spec.rb +106 -0
  39. data/spec/autoloading_spec.rb +146 -0
  40. data/spec/callbacks_spec.rb +46 -0
  41. data/spec/client_spec.rb +161 -0
  42. data/spec/endpoint_spec.rb +45 -0
  43. data/spec/fixtures/metaphysics.rb +2 -0
  44. data/spec/fixtures/metaphysics/_artist_fragment.graphql +4 -0
  45. data/spec/fixtures/metaphysics/artist.graphql +7 -0
  46. data/spec/fixtures/metaphysics/artists.graphql +8 -0
  47. data/spec/fixtures/metaphysics/artwork.graphql +8 -0
  48. data/spec/fixtures/metaphysics/schema.json +49811 -0
  49. data/spec/spec_helper.rb +48 -0
  50. metadata +219 -0
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -0,0 +1,12 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "pry"
6
+ gem "pry-byebug", platforms: :mri
7
+ gem "rails", "~> 4.2"
8
+ gem "railties", "~> 4.2"
9
+ gem "activesupport", "~> 4.2"
10
+ gem "minitest", "5.10.3"
11
+
12
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "pry"
6
+ gem "pry-byebug", platforms: :mri
7
+ gem "rails", "~> 5.0"
8
+ gem "railties", "~> 5.0"
9
+ gem "activesupport", "~> 5.0"
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "pry"
6
+ gem "pry-byebug", platforms: :mri
7
+ gem "rails", "~> 5.1"
8
+ gem "railties", "~> 5.1"
9
+ gem "activesupport", "~> 5.1"
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "pry"
6
+ gem "pry-byebug", platforms: :mri
7
+ gem "rails", "~> 5.2"
8
+ gem "railties", "~> 5.2"
9
+ gem "activesupport", "~> 5.2"
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,14 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git "git://github.com/rails/rails.git" do
6
+ gem "rails"
7
+ gem "railties"
8
+ gem "activesupport"
9
+ end
10
+
11
+ gem "pry"
12
+ gem "pry-byebug", platforms: :mri
13
+
14
+ gemspec path: "../"
@@ -0,0 +1,3 @@
1
+ require "artemis/version"
2
+ require "artemis/client"
3
+ require "artemis/railtie" if defined?(Rails)
@@ -0,0 +1,25 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'active_support/dependencies/autoload'
4
+
5
+ module Artemis
6
+ module Adapters
7
+ extend ActiveSupport::Autoload
8
+
9
+ autoload :CurbAdapter
10
+ autoload :NetHttpAdapter
11
+ autoload :NetHttpPersistentAdapter
12
+ autoload :TestAdapter
13
+
14
+ class << self
15
+ ##
16
+ # Returns the constant for the specified adapter name.
17
+ #
18
+ # Artemis::Adapters.lookup(:net_http)
19
+ # # => Artemis::Adapters::NetHttpAdapter
20
+ def lookup(name)
21
+ const_get("#{name.to_s.camelize}Adapter")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql/client/http'
4
+
5
+ module Artemis
6
+ module Adapters
7
+ class AbstractAdapter < ::GraphQL::Client::HTTP
8
+ attr_reader :service_name, :timeout, :pool_size
9
+
10
+ EMPTY_HEADERS = {}.freeze
11
+
12
+ def initialize(uri, service_name: , timeout: , pool_size: )
13
+ super(uri) # Do not pass in the block to avoid getting #headers and #connection overridden.
14
+
15
+ @service_name = service_name.to_s
16
+ @timeout = timeout
17
+ @pool_size = pool_size
18
+ end
19
+
20
+ # Public: Extension point for subclasses to set custom request headers.
21
+ #
22
+ # Returns Hash of String header names and values.
23
+ def headers(_context)
24
+ _context[:headers] || EMPTY_HEADERS
25
+ end
26
+
27
+ # Public: Make an HTTP request for GraphQL query.
28
+ #
29
+ # A subclass that inherits from +AbstractAdapter+ can override this method if it needs more flexibility for how
30
+ # it makes a request.
31
+ #
32
+ # For more details, see +GraphQL::Client::HTTP#execute+.
33
+ def execute(*)
34
+ super
35
+ end
36
+
37
+ # Public: Extension point for subclasses to customize the Net:HTTP client
38
+ #
39
+ # A subclass that inherits from +AbstractAdapter+ should returns a Net::HTTP object or an object that responds
40
+ # to +request+ that is given a Net::HTTP request object.
41
+ def connection
42
+ raise "AbstractAdapter is an abstract class that can not be instantiated!"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+ require 'json'
5
+
6
+ require 'curb'
7
+
8
+ require 'artemis/adapters/abstract_adapter'
9
+ require 'artemis/exceptions'
10
+
11
+ module Artemis
12
+ module Adapters
13
+ class CurbAdapter < AbstractAdapter
14
+ attr_reader :multi
15
+
16
+ def initialize(uri, service_name: , timeout: , pool_size: )
17
+ super
18
+
19
+ @multi = Curl::Multi.new
20
+ @multi.pipeline = Curl::CURLPIPE_MULTIPLEX if defined?(Curl::CURLPIPE_MULTIPLEX)
21
+ end
22
+
23
+ def execute(document:, operation_name: nil, variables: {}, context: {})
24
+ easy = Curl::Easy.new(uri.to_s)
25
+
26
+ body = {}
27
+ body["query"] = document.to_query_string
28
+ body["variables"] = variables if variables.any?
29
+ body["operationName"] = operation_name if operation_name
30
+
31
+ easy.timeout = timeout
32
+ easy.multi = multi
33
+ easy.headers = headers(context) || {}
34
+ easy.post_body = JSON.generate(body)
35
+
36
+ if defined?(Curl::CURLPIPE_MULTIPLEX)
37
+ # This ensures libcurl waits for the connection to reveal if it is
38
+ # possible to pipeline/multiplex on before it continues.
39
+ easy.setopt(Curl::CURLOPT_PIPEWAIT, 1)
40
+ easy.version = Curl::HTTP_2_0
41
+ end
42
+
43
+ easy.http_post
44
+
45
+ case easy.response_code
46
+ when 200, 400
47
+ JSON.parse(easy.body)
48
+ when 500..599
49
+ raise Artemis::GraphQLServerError, "Received server error status #{easy.response_code}: #{easy.body}"
50
+ else
51
+ { "errors" => [{ "message" => "#{easy.response_code} #{easy.body}" }] }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+
6
+ require 'artemis/adapters/abstract_adapter'
7
+ require 'artemis/exceptions'
8
+
9
+ module Artemis
10
+ module Adapters
11
+ class NetHttpAdapter < AbstractAdapter
12
+ # Makes an HTTP request for GraphQL query.
13
+ def execute(document:, operation_name: nil, variables: {}, context: {})
14
+ request = Net::HTTP::Post.new(uri.request_uri)
15
+
16
+ request.basic_auth(uri.user, uri.password) if uri.user || uri.password
17
+
18
+ request["Accept"] = "application/json"
19
+ request["Content-Type"] = "application/json"
20
+ headers(context).each { |name, value| request[name] = value }
21
+
22
+ body = {}
23
+ body["query"] = document.to_query_string
24
+ body["variables"] = variables if variables.any?
25
+ body["operationName"] = operation_name if operation_name
26
+ request.body = JSON.generate(body)
27
+
28
+ response = connection.request(request)
29
+
30
+ case response.code.to_i
31
+ when 200, 400
32
+ JSON.parse(response.body)
33
+ when 500..599
34
+ raise Artemis::GraphQLServerError, "Received server error status #{response.code}: #{response.body}"
35
+ else
36
+ { "errors" => [{ "message" => "#{response.code} #{response.message}" }] }
37
+ end
38
+ end
39
+
40
+ # Returns a fresh Net::HTTP object that creates a new connection.
41
+ def connection
42
+ Net::HTTP.new(uri.host, uri.port).tap do |client|
43
+ client.use_ssl = uri.scheme == "https"
44
+ client.open_timeout = timeout
45
+ client.read_timeout = timeout
46
+ client.write_timeout = timeout if client.respond_to?(:write_timeout=)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ require 'net/http/persistent'
6
+
7
+ require 'artemis/adapters/net_http_adapter'
8
+
9
+ module Artemis
10
+ module Adapters
11
+ class NetHttpPersistentAdapter < NetHttpAdapter
12
+ attr_reader :_connection, :raw_connection
13
+
14
+ def initialize(uri, service_name: , timeout: , pool_size: )
15
+ super
16
+
17
+ @raw_connection = Net::HTTP::Persistent.new(name: service_name, pool_size: pool_size)
18
+ @raw_connection.open_timeout = timeout
19
+ @raw_connection.read_timeout = timeout
20
+
21
+ @_connection = ConnectionWrapper.new(@raw_connection, uri)
22
+ end
23
+
24
+ # Public: Extension point for subclasses to customize the Net:HTTP client
25
+ #
26
+ # Returns a Net::HTTP object
27
+ def connection
28
+ _connection
29
+ end
30
+
31
+ class ConnectionWrapper < SimpleDelegator
32
+ def initialize(obj, url)
33
+ super(obj)
34
+
35
+ @url = url
36
+ end
37
+
38
+ def request(req)
39
+ __getobj__.request(@url, req)
40
+ end
41
+ end
42
+
43
+ private_constant :ConnectionWrapper
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/module/attribute_accessors'
4
+
5
+ module Artemis
6
+ module Adapters
7
+ class TestAdapter
8
+ cattr_accessor :requests
9
+ self.requests = []
10
+
11
+ Request = Struct.new(:document, :operation_name, :variables, :context)
12
+
13
+ private_constant :Request
14
+
15
+ def initialize(*)
16
+ end
17
+
18
+ def execute(**arguments)
19
+ self.requests << Request.new(*arguments.values_at(:document, :operation_name, :variables, :context))
20
+
21
+ {
22
+ 'data' => { 'test' => 'data' },
23
+ 'errors' => [],
24
+ 'extensions' => {}
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ require 'active_support/configurable'
6
+ require 'active_support/core_ext/hash/deep_merge'
7
+ require 'active_support/core_ext/string/inflections'
8
+
9
+ require 'artemis/graphql_endpoint'
10
+ require 'artemis/exceptions'
11
+
12
+ module Artemis
13
+ class Client
14
+ include ActiveSupport::Configurable
15
+
16
+ # The paths in which the Artemis client looks for files that have the +.graphql+ extension.
17
+ # In a rails app, this value will be set to +["app/operations"]+ by Artemis' +Artemis::Railtie+.
18
+ config.query_paths = []
19
+
20
+ # Default context that is appended to every GraphQL request for the client.
21
+ config.default_context = {}
22
+
23
+ # List of before callbacks that get invoked in every +execute+ call.
24
+ #
25
+ # @api private
26
+ config.before_callbacks = []
27
+
28
+ # List of after callbacks that get invoked in every +execute+ call.
29
+ #
30
+ # @api private
31
+ config.after_callbacks = []
32
+
33
+ # Returns a plain +GraphQL::Client+ object. For more details please refer to the official documentation for
34
+ # {the +graphql-client+ gem}[https://github.com/github/graphql-client].
35
+ attr_reader :client
36
+
37
+ # Creates a new instance of the GraphQL client for the service.
38
+ #
39
+ # # app/operations/github/user.graphql
40
+ # query($id: String!) {
41
+ # user(login: $id) {
42
+ # name
43
+ # }
44
+ # }
45
+ #
46
+ # # app/operations/github.rb
47
+ # class GitHub < Artemis::Client
48
+ # end
49
+ #
50
+ # github = GitHub.new
51
+ # github.user(id: 'yuki24').data.user.name # => "Yuki Nishijima"
52
+ #
53
+ # github = GitHub.new(context: { headers: { Authorization: "bearer ..." } })
54
+ # github.user(id: 'yuki24').data.user.name # => "Yuki Nishijima"
55
+ #
56
+ def initialize(context = {})
57
+ @client = self.class.instantiate_client(context)
58
+ end
59
+
60
+ class << self
61
+ delegate :query_paths, :default_context, :query_paths=, :default_context=, to: :config
62
+
63
+ alias with_context new
64
+
65
+ def endpoint
66
+ Artemis::GraphQLEndpoint.lookup(name)
67
+ end
68
+
69
+ def instantiate_client(context = {})
70
+ ::GraphQL::Client.new(schema: endpoint.schema, execute: connection(context))
71
+ end
72
+
73
+ # Defines a callback that will get called right before the
74
+ # client's execute method is executed.
75
+ #
76
+ # class GitHub < Artemis::Client
77
+ #
78
+ # before_execute do |document, operation_name, variables, context|
79
+ # Analytics.log(operation_name, variables, context[:user_id])
80
+ # end
81
+ #
82
+ # ...
83
+ # end
84
+ #
85
+ def before_execute(&block)
86
+ config.before_callbacks << block
87
+ end
88
+
89
+ # Defines a callback that will get called right after the
90
+ # client's execute method has finished.
91
+ #
92
+ # class GitHub < Artemis::Client
93
+ #
94
+ # after_execute do |data, errors, extensions|
95
+ # if errors.present?
96
+ # Rails.logger.error(errors.to_json)
97
+ # end
98
+ # end
99
+ #
100
+ # ...
101
+ # end
102
+ #
103
+ def after_execute(&block)
104
+ config.after_callbacks << block
105
+ end
106
+
107
+ def resolve_graphql_file_path(filename, fragment: false)
108
+ namespace = name.underscore
109
+ filename = filename.to_s.underscore
110
+
111
+ graphql_file_paths.detect do |path|
112
+ path.end_with?("#{namespace}/#{filename}.graphql") ||
113
+ (fragment && filename.end_with?('fragment') && path.end_with?("#{namespace}/_#{filename}.graphql"))
114
+ end
115
+ end
116
+
117
+ def graphql_file_paths
118
+ @graphql_file_paths ||= query_paths.flat_map {|path| Dir["#{path}/#{name.underscore}/*.graphql"] }
119
+ end
120
+
121
+ def preload!
122
+ graphql_file_paths.each do |path|
123
+ load_constant(File.basename(path, File.extname(path)).camelize)
124
+ end
125
+ end
126
+
127
+ def load_constant(const_name)
128
+ graphql_file = resolve_graphql_file_path(const_name.to_s.underscore, fragment: true)
129
+
130
+ if graphql_file
131
+ graphql = File.open(graphql_file).read
132
+ ast = instantiate_client.parse(graphql)
133
+
134
+ const_set(const_name, ast)
135
+ end
136
+ end
137
+ alias load_query load_constant
138
+
139
+ # @api private
140
+ def connection(context = {})
141
+ Executor.new(endpoint.connection, callbacks, default_context.deep_merge(context))
142
+ end
143
+
144
+ private
145
+
146
+ # @api private
147
+ def const_missing(const_name)
148
+ load_constant(const_name) || super
149
+ end
150
+
151
+ # @api private
152
+ def method_missing(method_name, *arguments, &block)
153
+ if resolve_graphql_file_path(method_name)
154
+ new(default_context).public_send(method_name, *arguments, &block)
155
+ else
156
+ super
157
+ end
158
+ end
159
+
160
+ # @api private
161
+ def respond_to_missing?(method_name, *_, &block)
162
+ resolve_graphql_file_path(method_name) || super
163
+ end
164
+
165
+ Callbacks = Struct.new(:before_callbacks, :after_callbacks)
166
+
167
+ private_constant :Callbacks
168
+
169
+ # @api private
170
+ def callbacks
171
+ Callbacks.new(config.before_callbacks, config.after_callbacks)
172
+ end
173
+ end
174
+
175
+ private
176
+
177
+ # @api private
178
+ def method_missing(method_name, context: {}, **arguments)
179
+ if self.class.resolve_graphql_file_path(method_name)
180
+ const_name = method_name.to_s.camelize
181
+
182
+ # This check will be unnecessary once we drop support for Ruby 2.4 and earlier
183
+ if !self.class.const_get(const_name).is_a?(GraphQL::Client::OperationDefinition)
184
+ self.class.load_constant(const_name)
185
+ end
186
+
187
+ client.query(
188
+ self.class.const_get(const_name),
189
+ variables: arguments.deep_transform_keys {|key| key.to_s.camelize(:lower) },
190
+ context: context
191
+ )
192
+ else
193
+ super
194
+ end
195
+ end
196
+
197
+ # @api private
198
+ def respond_to_missing?(method_name, *_, &block)
199
+ self.class.resolve_graphql_file_path(method_name) || super
200
+ end
201
+
202
+ # @api private
203
+ def compile_query_method!(method_name)
204
+ const_name = method_name.to_s.camelize
205
+
206
+ self.class.send(:class_eval, <<-RUBY, __FILE__, __LINE__ + 1)
207
+ def #{method_name}(context: {}, **arguments)
208
+ client.query(
209
+ self.class::#{const_name},
210
+ variables: arguments.deep_transform_keys {|key| key.to_s.camelize(:lower) },
211
+ context: context
212
+ )
213
+ end
214
+ RUBY
215
+ end
216
+ end
217
+
218
+ # @api private
219
+ class Executor < SimpleDelegator
220
+ def initialize(connection, callbacks, default_context)
221
+ super(connection)
222
+
223
+ @callbacks = callbacks
224
+ @default_context = default_context
225
+ end
226
+
227
+ def execute(document:, operation_name: nil, variables: {}, context: {})
228
+ _context = @default_context.deep_merge(context)
229
+
230
+ @callbacks.before_callbacks.each do |callback|
231
+ callback.call(document, operation_name, variables, _context)
232
+ end
233
+
234
+ response = __getobj__.execute(document: document, operation_name: operation_name, variables: variables, context: _context)
235
+
236
+ @callbacks.after_callbacks.each do |callback|
237
+ callback.call(response['data'], response['errors'], response['extensions'])
238
+ end
239
+
240
+ response
241
+ end
242
+ end
243
+
244
+ private_constant :Executor
245
+ end