artemis 0.4.0 → 0.6.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 (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: