artemis 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +53 -0
  3. data/Appraisals +22 -16
  4. data/CHANGELOG.md +63 -6
  5. data/Gemfile +2 -1
  6. data/README.md +62 -32
  7. data/artemis.gemspec +7 -8
  8. data/banner.png +0 -0
  9. data/gemfiles/rails_50.gemfile +6 -3
  10. data/gemfiles/rails_51.gemfile +5 -3
  11. data/gemfiles/rails_52.gemfile +5 -3
  12. data/gemfiles/rails_60.gemfile +13 -0
  13. data/gemfiles/{rails_42.gemfile → rails_61.gemfile} +5 -4
  14. data/gemfiles/rails_edge.gemfile +2 -1
  15. data/lib/artemis/adapters/abstract_adapter.rb +6 -1
  16. data/lib/artemis/adapters/curb_adapter.rb +2 -2
  17. data/lib/artemis/adapters/multi_domain_adapter.rb +43 -0
  18. data/lib/artemis/adapters/net_http_adapter.rb +1 -3
  19. data/lib/artemis/adapters/net_http_persistent_adapter.rb +1 -1
  20. data/lib/artemis/adapters/test_adapter.rb +1 -1
  21. data/lib/artemis/adapters.rb +1 -0
  22. data/lib/artemis/client.rb +45 -19
  23. data/lib/artemis/graphql_endpoint.rb +24 -6
  24. data/lib/artemis/railtie.rb +15 -13
  25. data/lib/artemis/test_helper.rb +41 -14
  26. data/lib/artemis/version.rb +1 -1
  27. data/lib/artemis.rb +32 -1
  28. data/lib/generators/artemis/install/install_generator.rb +2 -2
  29. data/lib/generators/artemis/mutation/templates/mutation.graphql +1 -1
  30. data/lib/generators/artemis/query/query_generator.rb +13 -1
  31. data/lib/generators/artemis/query/templates/fixture.yml +19 -0
  32. data/lib/generators/artemis/query/templates/query.graphql +1 -1
  33. data/lib/tasks/artemis.rake +3 -3
  34. data/spec/adapters_spec.rb +56 -10
  35. data/spec/callbacks_spec.rb +14 -0
  36. data/spec/client_spec.rb +26 -1
  37. data/spec/fixtures/responses/{artist.yml → metaphysics/artist.yml} +7 -0
  38. data/spec/fixtures/responses/{artwork.json → metaphysics/artwork.json} +0 -0
  39. data/spec/fixtures/responses/spotify_client/artist.yml +5 -0
  40. data/spec/spec_helper.rb +1 -0
  41. data/spec/test_helper_spec.rb +34 -4
  42. metadata +36 -46
  43. data/.travis.yml +0 -34
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- git "git://github.com/rails/rails.git" do
5
+ git "https://github.com/rails/rails.git" do
6
6
  gem "rails"
7
7
  gem "railties"
8
8
  gem "activesupport"
@@ -10,5 +10,6 @@ end
10
10
 
11
11
  gem "pry"
12
12
  gem "pry-byebug", platforms: :mri
13
+ gem "curb", ">= 0.9.6"
13
14
 
14
15
  gemspec path: "../"
@@ -10,7 +10,12 @@ module Artemis
10
10
 
11
11
  EMPTY_HEADERS = {}.freeze
12
12
 
13
- def initialize(uri, service_name: , timeout: , pool_size: )
13
+ DEFAULT_HEADERS = {
14
+ "Accept" => "application/json",
15
+ "Content-Type" => "application/json"
16
+ }.freeze
17
+
18
+ def initialize(uri, service_name: , timeout: , pool_size: , adapter_options: {})
14
19
  raise ArgumentError, "url is required (given `#{uri.inspect}')" if uri.blank?
15
20
 
16
21
  super(uri) # Do not pass in the block to avoid getting #headers and #connection overridden.
@@ -13,7 +13,7 @@ module Artemis
13
13
  class CurbAdapter < AbstractAdapter
14
14
  attr_reader :multi
15
15
 
16
- def initialize(uri, service_name: , timeout: , pool_size: )
16
+ def initialize(uri, service_name: , timeout: , pool_size: , adapter_options: {})
17
17
  super
18
18
 
19
19
  @multi = Curl::Multi.new
@@ -30,7 +30,7 @@ module Artemis
30
30
 
31
31
  easy.timeout = timeout
32
32
  easy.multi = multi
33
- easy.headers = headers(context) || {}
33
+ easy.headers = DEFAULT_HEADERS.merge(headers(context))
34
34
  easy.post_body = JSON.generate(body)
35
35
 
36
36
  if defined?(Curl::CURLPIPE_MULTIPLEX)
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'artemis/adapters/abstract_adapter'
4
+
5
+ module Artemis
6
+ module Adapters
7
+ class MultiDomainAdapter < AbstractAdapter
8
+ attr_reader :adapter
9
+
10
+ def initialize(_uri, service_name: , timeout: , pool_size: , adapter_options: {})
11
+ @connection_by_url = {}
12
+ @service_name = service_name.to_s
13
+ @timeout = timeout
14
+ @pool_size = pool_size
15
+ @adapter = adapter_options[:adapter] || :net_http
16
+ @mutex_for_connection = Mutex.new
17
+ end
18
+
19
+ # Makes an HTTP request for GraphQL query.
20
+ def execute(document:, operation_name: nil, variables: {}, context: {})
21
+ url = context[:url]
22
+
23
+ if url.nil?
24
+ raise ArgumentError, 'The MultiDomain adapter requires a url on every request. Please specify a url with a context: ' \
25
+ 'Client.with_context(url: "https://awesomeshop.domain.conm")'
26
+ end
27
+
28
+ connection_for_url(url).execute(document: document, operation_name: operation_name, variables: variables, context: context)
29
+ end
30
+
31
+ def connection
32
+ raise NotImplementedError, "Calling the #connection method without a URI is not supported. Please use the " \
33
+ "#connection_for_url(uri) instead."
34
+ end
35
+
36
+ def connection_for_url(url)
37
+ @connection_by_url[url.to_s] || @mutex_for_connection.synchronize do
38
+ @connection_by_url[url.to_s] ||= ::Artemis::Adapters.lookup(adapter).new(url, service_name: service_name, timeout: timeout, pool_size: pool_size)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -15,9 +15,7 @@ module Artemis
15
15
 
16
16
  request.basic_auth(uri.user, uri.password) if uri.user || uri.password
17
17
 
18
- request["Accept"] = "application/json"
19
- request["Content-Type"] = "application/json"
20
- headers(context).each { |name, value| request[name] = value }
18
+ DEFAULT_HEADERS.merge(headers(context)).each { |name, value| request[name] = value }
21
19
 
22
20
  body = {}
23
21
  body["query"] = document.to_query_string
@@ -12,7 +12,7 @@ module Artemis
12
12
  class NetHttpPersistentAdapter < NetHttpAdapter
13
13
  attr_reader :_connection, :raw_connection
14
14
 
15
- def initialize(uri, service_name: , timeout: , pool_size: )
15
+ def initialize(uri, service_name: , timeout: , pool_size: , adapter_options: {})
16
16
  super
17
17
 
18
18
  @raw_connection = Net::HTTP::Persistent.new(name: service_name, pool_size: pool_size)
@@ -22,7 +22,7 @@ module Artemis
22
22
  self.requests << Request.new(*arguments.values_at(:document, :operation_name, :variables, :context))
23
23
 
24
24
  response = responses.detect do |mock|
25
- arguments[:operation_name] == mock.operation_name && mock.arguments == :__unspecified__ || arguments[:variables] == mock.arguments
25
+ arguments[:operation_name] == mock.operation_name && (mock.arguments == :__unspecified__ || arguments[:variables] == mock.arguments)
26
26
  end
27
27
 
28
28
  response&.data || {
@@ -7,6 +7,7 @@ module Artemis
7
7
  extend ActiveSupport::Autoload
8
8
 
9
9
  autoload :CurbAdapter
10
+ autoload :MultiDomainAdapter
10
11
  autoload :NetHttpAdapter
11
12
  autoload :NetHttpPersistentAdapter
12
13
  autoload :TestAdapter
@@ -4,6 +4,8 @@ require 'delegate'
4
4
 
5
5
  require 'active_support/configurable'
6
6
  require 'active_support/core_ext/hash/deep_merge'
7
+ require 'active_support/core_ext/module/delegation'
8
+ require 'active_support/core_ext/module/attribute_accessors'
7
9
  require 'active_support/core_ext/string/inflections'
8
10
 
9
11
  require 'artemis/graphql_endpoint'
@@ -15,10 +17,10 @@ module Artemis
15
17
 
16
18
  # The paths in which the Artemis client looks for files that have the +.graphql+ extension.
17
19
  # In a rails app, this value will be set to +["app/operations"]+ by Artemis' +Artemis::Railtie+.
18
- config.query_paths = []
20
+ cattr_accessor :query_paths
19
21
 
20
22
  # Default context that is appended to every GraphQL request for the client.
21
- config.default_context = {}
23
+ config.default_context = nil
22
24
 
23
25
  # List of before callbacks that get invoked in every +execute+ call.
24
26
  #
@@ -58,7 +60,7 @@ module Artemis
58
60
  end
59
61
 
60
62
  class << self
61
- delegate :query_paths, :default_context, :query_paths=, :default_context=, to: :config
63
+ delegate :default_context=, to: :config
62
64
 
63
65
  # Creates a new instance of the GraphQL client for the service.
64
66
  #
@@ -136,7 +138,7 @@ module Artemis
136
138
  # end
137
139
  #
138
140
  def before_execute(&block)
139
- config.before_callbacks << block
141
+ config.before_callbacks = [*config.before_callbacks, block]
140
142
  end
141
143
 
142
144
  # Defines a callback that will get called right after the
@@ -154,7 +156,14 @@ module Artemis
154
156
  # end
155
157
  #
156
158
  def after_execute(&block)
157
- config.after_callbacks << block
159
+ config.after_callbacks = [*config.after_callbacks, block]
160
+ end
161
+
162
+ # Returns the default configured context or an empty hash by default
163
+ #
164
+ # @return [Hash]
165
+ def default_context
166
+ config.default_context || {}
158
167
  end
159
168
 
160
169
  def resolve_graphql_file_path(filename, fragment: false)
@@ -205,6 +214,10 @@ module Artemis
205
214
  Executor.new(endpoint.connection, callbacks, default_context.deep_merge(context))
206
215
  end
207
216
 
217
+ def execute(query, context: {}, **arguments)
218
+ new(default_context).execute(query, context: context, **arguments)
219
+ end
220
+
208
221
  private
209
222
 
210
223
  # Looks up the GraphQL file that matches the given +const_name+ and sets it to a constant. If the files it not
@@ -235,9 +248,9 @@ module Artemis
235
248
  # Github.user # => delegates to Github.new(default_context).user
236
249
  #
237
250
  # @api private
238
- def method_missing(method_name, *arguments, &block)
251
+ def method_missing(method_name, **arguments, &block)
239
252
  if resolve_graphql_file_path(method_name)
240
- new(default_context).public_send(method_name, *arguments, &block)
253
+ new(default_context).public_send(method_name, **arguments, &block)
241
254
  else
242
255
  super
243
256
  end
@@ -255,6 +268,28 @@ module Artemis
255
268
  end
256
269
  end
257
270
 
271
+ # Executes a given query, raises if we didn't define the operation
272
+ #
273
+ # @param [String] operation
274
+ # @param [Hash] context
275
+ # @param [Hash] arguments
276
+ #
277
+ # @return [GraphQL::Client::Response]
278
+ def execute(query, context: {}, **arguments)
279
+ if self.class.resolve_graphql_file_path(query)
280
+ const_name = query.to_s.camelize
281
+
282
+ # This check will be unnecessary once we drop support for Ruby 2.4 and earlier
283
+ if !self.class.const_get(const_name).is_a?(GraphQL::Client::OperationDefinition)
284
+ self.class.load_constant(const_name)
285
+ end
286
+
287
+ client.query(self.class.const_get(const_name), variables: arguments, context: context)
288
+ else
289
+ raise GraphQLFileNotFound.new("Query #{query}.graphql not found in: #{query_paths.join(", ")}")
290
+ end
291
+ end
292
+
258
293
  private
259
294
 
260
295
  # Delegates a method call to a GraphQL call.
@@ -268,18 +303,9 @@ module Artemis
268
303
  #
269
304
  # @api private
270
305
  def method_missing(method_name, context: {}, **arguments)
271
- if self.class.resolve_graphql_file_path(method_name)
272
- const_name = method_name.to_s.camelize
273
-
274
- # This check will be unnecessary once we drop support for Ruby 2.4 and earlier
275
- if !self.class.const_get(const_name).is_a?(GraphQL::Client::OperationDefinition)
276
- self.class.load_constant(const_name)
277
- end
278
-
279
- client.query(self.class.const_get(const_name), variables: arguments, context: context)
280
- else
281
- super
282
- end
306
+ execute(method_name, context: context, **arguments)
307
+ rescue GraphQLFileNotFound
308
+ super
283
309
  end
284
310
 
285
311
  def respond_to_missing?(method_name, *_, &block) #:nodoc:
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'active_support/core_ext/hash/deep_merge'
4
4
  require 'active_support/core_ext/hash/keys'
5
+ require 'active_support/core_ext/class/attribute_accessors'
5
6
  require 'active_support/core_ext/object/blank'
6
7
  require 'active_support/core_ext/string/inflections'
7
8
  require 'graphql/client'
@@ -11,6 +12,13 @@ require 'artemis/exceptions'
11
12
 
12
13
  module Artemis
13
14
  class GraphQLEndpoint
15
+
16
+ # Whether or not to suppress warnings on schema load. Use it with caution.
17
+ #
18
+ # @private
19
+ cattr_accessor :suppress_warnings_on_schema_load
20
+ self.suppress_warnings_on_schema_load = false
21
+
14
22
  # Hash object that holds references to adapter instances.
15
23
  ENDPOINT_INSTANCES = {}
16
24
 
@@ -25,7 +33,7 @@ module Artemis
25
33
  end
26
34
 
27
35
  def register!(service_name, configurations)
28
- ENDPOINT_INSTANCES[service_name.to_s.underscore] = new(service_name.to_s, configurations.symbolize_keys)
36
+ ENDPOINT_INSTANCES[service_name.to_s.underscore] = new(service_name.to_s, **configurations.symbolize_keys)
29
37
  end
30
38
 
31
39
  ##
@@ -36,26 +44,36 @@ module Artemis
36
44
  end
37
45
  end
38
46
 
39
- attr_reader :name, :url, :adapter, :timeout, :schema_path, :pool_size
47
+ attr_reader :name, :url, :adapter, :timeout, :schema_path, :pool_size, :adapter_options
40
48
 
41
- def initialize(name, url: nil, adapter: :net_http, timeout: 10, schema_path: nil, pool_size: 25)
42
- @name, @url, @adapter, @timeout, @schema_path, @pool_size = name.to_s, url, adapter, timeout, schema_path, pool_size
49
+ def initialize(name, url: nil, adapter: :net_http, timeout: 10, schema_path: nil, pool_size: 25, adapter_options: {})
50
+ @name = name.to_s
51
+ @url = url
52
+ @adapter = adapter
53
+ @timeout = timeout
54
+ @schema_path = schema_path
55
+ @pool_size = pool_size
56
+ @adapter_options = adapter_options
43
57
 
44
58
  @mutex_for_schema = Mutex.new
45
59
  @mutex_for_connection = Mutex.new
46
60
  end
47
61
 
48
62
  def schema
63
+ org, $stderr = $stderr, File.new("/dev/null", "w") if self.class.suppress_warnings_on_schema_load
64
+
49
65
  @schema || @mutex_for_schema.synchronize do
50
66
  @schema ||= ::GraphQL::Client.load_schema(schema_path.presence || connection)
51
67
  end
68
+ ensure
69
+ $stderr = org if self.class.suppress_warnings_on_schema_load
52
70
  end
53
71
  alias load_schema! schema
54
72
 
55
73
  def connection
56
74
  @connection || @mutex_for_connection.synchronize do
57
- @connection ||= ::Artemis::Adapters.lookup(adapter).new(url, service_name: name, timeout: timeout, pool_size: pool_size)
75
+ @connection ||= ::Artemis::Adapters.lookup(adapter).new(url, service_name: name, timeout: timeout, pool_size: pool_size, adapter_options: adapter_options)
58
76
  end
59
77
  end
60
78
  end
61
- end
79
+ end
@@ -31,13 +31,15 @@ module Artemis
31
31
  end
32
32
 
33
33
  initializer 'graphql.client.set_reloader', after: 'graphql.client.set_query_paths' do |app|
34
- files_to_watch = Artemis::Client.query_paths.map {|path| [path, config.artemis.graphql_extentions] }.to_h
34
+ if !config.respond_to?(:autoloader) || config.autoloader != :zeitwerk
35
+ files_to_watch = Artemis::Client.query_paths.map {|path| [path, config.artemis.graphql_extentions] }.to_h
35
36
 
36
- app.reloaders << ActiveSupport::FileUpdateChecker.new([], files_to_watch) do
37
- endpoint_names = app.config_for(:graphql).keys
38
- endpoint_names.each do |endpoint_name|
39
- Artemis::Client.query_paths.each do |path|
40
- FileUtils.touch("#{path}/#{endpoint_name}.rb")
37
+ app.reloaders << ActiveSupport::FileUpdateChecker.new([], files_to_watch) do
38
+ endpoint_names = Artemis.config_for_graphql(app).keys
39
+ endpoint_names.each do |endpoint_name|
40
+ Artemis::Client.query_paths.each do |path|
41
+ FileUtils.touch("#{path}/#{endpoint_name}.rb")
42
+ end
41
43
  end
42
44
  end
43
45
  end
@@ -45,18 +47,18 @@ module Artemis
45
47
 
46
48
  initializer 'graphql.client.load_config' do |app|
47
49
  if Pathname.new("#{app.paths["config"].existent.first}/graphql.yml").exist?
48
- app.config_for(:graphql).each do |endpoint_name, options|
50
+ Artemis.config_for_graphql(app).each do |endpoint_name, options|
49
51
  Artemis::GraphQLEndpoint.register!(endpoint_name, {
50
- 'schema_path' => app.root.join(config.artemis.schema_path, "#{endpoint_name}.json").to_s
51
- }.merge(options))
52
+ schema_path: app.root.join(config.artemis.schema_path, "#{endpoint_name}.json").to_s
53
+ }.merge(options.symbolize_keys))
52
54
  end
53
55
  end
54
56
  end
55
57
 
56
58
  initializer 'graphql.client.preload', after: 'graphql.client.load_config' do |app|
57
- if app.config.eager_load
58
- app.config_for(:graphql).keys.each do |endpoint_name|
59
- endpoint_name.to_s.camelize.constantize.preload!
59
+ if app.config.eager_load && app.config.cache_classes
60
+ Artemis::GraphQLEndpoint.registered_services.each do |endpoint_name|
61
+ endpoint_name.camelize.constantize.preload!
60
62
  end
61
63
  end
62
64
  end
@@ -65,4 +67,4 @@ module Artemis
65
67
  load "tasks/artemis.rake"
66
68
  end
67
69
  end
68
- end
70
+ end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'erb'
4
+ require 'yaml'
4
5
 
5
6
  require 'active_support/core_ext/module/attribute_accessors'
7
+ require 'active_support/core_ext/string/inflections'
6
8
 
7
9
  require 'artemis/exceptions'
8
10
 
9
11
  module Artemis
10
12
  # TODO: Write documentation for +TestHelper+
11
13
  module TestHelper
12
- cattr_accessor :__graphql_fixture_path__
14
+ mattr_accessor :__graphql_fixture_path__
13
15
 
14
16
  # Creates an object that stubs a GraphQL request for the given +service+. No mock response is registered until the
15
17
  # +to_return+ method.
@@ -41,7 +43,7 @@ module Artemis
41
43
  # da_vinci.data.artist.name # => "Leonardo da Vinci"
42
44
  #
43
45
  def stub_graphql(service, query_name, arguments = :__unspecified__)
44
- StubbingDSL.new(service.to_s, graphql_fixtures(query_name), arguments)
46
+ StubbingDSL.new(service.to_s, query_name, graphql_fixture_files, arguments)
45
47
  end
46
48
 
47
49
  # Returns out-going GraphQL requests.
@@ -60,15 +62,11 @@ module Artemis
60
62
  __graphql_fixture_path__ || raise(Artemis::ConfigurationError, "GraphQL fixture path is unset")
61
63
  end
62
64
 
63
- def graphql_fixtures(query_name) #:nodoc:
64
- graphql_fixture_files.detect {|fixture| fixture.name == query_name.to_s } || \
65
- raise(Artemis::FixtureNotFound, "Fixture file `#{File.join(graphql_fixture_path, "#{query_name}.{yml,json}")}' not found")
66
- end
67
-
68
65
  def graphql_fixture_files #:nodoc:
69
66
  @graphql_fixture_sets ||= Dir["#{graphql_fixture_path}/{**,*}/*.{yml,json}"]
70
- .select {|file| ::File.file?(file) }
71
- .map {|file| GraphQLFixture.new(File.basename(file, File.extname(file)), file, read_erb_yaml(file)) }
67
+ .uniq
68
+ .select {|file| ::File.file?(file) }
69
+ .map {|file| GraphQLFixture.new(File.basename(file, File.extname(file)), file, read_erb_yaml(file)) }
72
70
  end
73
71
 
74
72
  def read_erb_yaml(path) #:nodoc:
@@ -76,15 +74,30 @@ module Artemis
76
74
  end
77
75
 
78
76
  class StubbingDSL #:nodoc:
79
- attr_reader :service_name, :fixture_set, :arguments
77
+ attr_reader :service_name, :query_name, :fixture_sets, :arguments
78
+
79
+ def initialize(service_name, query_name, fixture_sets, arguments) #:nodoc:
80
+ @service_name, @query_name, @fixture_sets, @arguments = service_name, query_name, fixture_sets, arguments
81
+ end
82
+
83
+ def get(fixture_key)
84
+ fixture_set = find_fixture_set
85
+ fixture = fixture_set.data[fixture_key.to_s]
86
+
87
+ if fixture.nil?
88
+ raise Artemis::FixtureNotFound, "Fixture `#{fixture_key}' not found in #{fixture_set.path}"
89
+ end
80
90
 
81
- def initialize(service_name, fixture_set, arguments) #:nodoc:
82
- @service_name, @fixture_set, @arguments = service_name, fixture_set, arguments
91
+ fixture
83
92
  end
84
93
 
85
94
  def to_return(fixture_key) #:nodoc:
86
- fixture = fixture_set.data[fixture_key.to_s] || \
87
- raise(Artemis::FixtureNotFound, "Fixture `#{fixture_key}' not found in #{fixture_set.path}")
95
+ fixture_set = find_fixture_set
96
+ fixture = fixture_set.data[fixture_key.to_s]
97
+
98
+ if fixture.nil?
99
+ raise Artemis::FixtureNotFound, "Fixture `#{fixture_key}' not found in #{fixture_set.path}"
100
+ end
88
101
 
89
102
  Artemis::Adapters::TestAdapter.responses <<
90
103
  TestResponse.new(
@@ -93,6 +106,20 @@ module Artemis
93
106
  fixture
94
107
  )
95
108
  end
109
+
110
+ private
111
+
112
+ def find_fixture_set
113
+ fixture_set = fixture_sets
114
+ .detect { |fixture| %r{#{service_name.underscore}/#{query_name}\.(yml|json)\z} =~ fixture.path }
115
+ fixture_set ||= fixture_sets.detect { |fixture| fixture.name == query_name.to_s }
116
+
117
+ if fixture_set.nil?
118
+ raise Artemis::FixtureNotFound, "Fixture file `#{query_name}.{yml,json}' not found"
119
+ end
120
+
121
+ fixture_set
122
+ end
96
123
  end
97
124
 
98
125
  TestResponse = Struct.new(:operation_name, :arguments, :data) #:nodoc: