evil-client 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1a0f6860291b235077bd6d9063eb9e1ec47cb9ef
4
- data.tar.gz: cbb9aca48a0283cf8e6ec84bec9444ea74bb440c
3
+ metadata.gz: 237e89873909ecf0ad225c9a714e68702c96b5c6
4
+ data.tar.gz: 5a030e4b7629338ebe24359311e7dac5b4d74fe8
5
5
  SHA512:
6
- metadata.gz: c1d2c2e407c90ef12db1d58c4aed5fef1a4c851934f90e25602ad5461c14278e30dcd3a1dbc7534dc17218797d46dedaa8f023389be46625f66bb046a06791a5
7
- data.tar.gz: 433099e8dcebb8e48e0f22f057db11c68eb78556a1660508bdd22a261a40ad2c59407971459716c027d7c5d4b7cf122b2c03fa63d3308b06762e5658f3e7d763
6
+ metadata.gz: 0ae5ee8377ef7ca78252356a4cad2726ef854a676b4b0715c4e6a881d7c8d381896307b48d7d8e6cbe7920362369f4886dc8f654a39bab10e2cf160cf8c6fed8
7
+ data.tar.gz: 54acb1b3f1d7e94d879240eaa6eba66576c719b5833707ef68393dcded7c971b7a1ed248e840f514d1a377a3daa6f33892caaf93a4acecffd485459e9c9f177f
data/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog], and this project adheres
5
5
  to [Semantic Versioning].
6
6
 
7
+ ## [1.1.0] [2017-08-10]
8
+
9
+ Some syntax sugar has been added to both the client and its RSpec helpers.
10
+
11
+ ### Added
12
+
13
+ - Assigned options are wrapped into simple delegator with rails-like methods
14
+ `#slice` and `#except`. This helps when you need to select part of assigned
15
+ options for some part of a request (nepalez)
16
+
17
+ Remember the options are collected from the very root of the client,
18
+ so at the endpoint operation there could be a lot of options
19
+ related to other endpoints, or to a different part of the request.
20
+
21
+ - Every container has reference to its `#client` along the standalone `#name`
22
+ of its schema. This allows to select operation containers by
23
+ `#client`, `#name`, `#options` to stub their methods `#call` (nepalez)
24
+
25
+ - RSpec stubs and expectations for operations (nepalez, palkan)
26
+
27
+ ### Removed
28
+
29
+ - RSpec matcher `perform_operation` has been dropped in favor of
30
+ `stub_client_operation` and `expect_client_operation` (nepalez)
31
+
32
+ - Unnecessary instance methods inherited from [Object] are removed
33
+ from various classes to avoid name conflicts with user-provided
34
+ scopes and operations (nepalez)
35
+
7
36
  ## [1.0.0] [2017-08-06]
8
37
 
9
38
  This is a total new reincarnation of the gem. I've changed its
@@ -326,6 +355,7 @@ formats will be added.
326
355
  response :not_found, 404, format: "json", raise: true
327
356
  ```
328
357
 
358
+ [1.1.0]: https://github.com/evilmartians/evil-client/compare/v1.0.0...v1.1.0
329
359
  [1.0.0]: https://github.com/evilmartians/evil-client/compare/v0.3.3...v1.0.0
330
360
  [0.3.3]: https://github.com/evilmartians/evil-client/compare/v0.3.2...v0.3.3
331
361
  [0.3.2]: https://github.com/evilmartians/evil-client/compare/v0.3.1...v0.3.2
data/README.md CHANGED
@@ -70,7 +70,7 @@ class CatsClient < Evil::Client
70
70
  path { "cats/#{id}" } # added to root path
71
71
  http_method :patch # you can use plain syntax instead of a block
72
72
  format "json"
73
- body { options.reject { |key, _val| key == :id } }
73
+ body { options.except(:id, :version) } # [#slice] is available too
74
74
 
75
75
  # Parses json response and wraps it into Cat instance with additional
76
76
  # parameter
data/docs/index.md CHANGED
@@ -68,7 +68,7 @@ class CatsClient < Evil::Client
68
68
  path { "cats/#{id}" } # added to root path
69
69
  http_method :patch # you can use plain syntax instead of a block
70
70
  format "json"
71
- body { options.reject { |key, _val| key == :id } }
71
+ body { options.except(:id, :version) } # [#slice] is available too
72
72
 
73
73
  # Parses json response and wraps it into Cat instance with additional
74
74
  # parameter
data/docs/rspec.md CHANGED
@@ -1,13 +1,16 @@
1
1
  When you provide a client to remote API, you would provide some means for its users to test their operations.
2
2
 
3
- Surely, they could use [webmock] to check the ultimate requests that are sent to the server. But doing this, they would inadvertedly specify not their own code, but your client's code too. What do they actually need is a means to check calls of your client's operations. This way they would back on correctness of your client, and take its interface as an endpoint.
3
+ Surely, they could use [webmock] to check the ultimate requests that are sent to the server. But doing this, they would inadvertedly specify not their own code, but your client's code too. What do they actually need is a means to stub and check invocations of your client's operations. This way they would back on correctness of your client, and take its interface as an endpoint.
4
4
 
5
- For this reason, we support a special RSpec matcher `perform_operation`. It checks, what operations are called via evil-client, and what options are used in there.
6
-
7
- The matcher isn't loaded by default, so you must require it first:
5
+ For this reason, we support a special RSpec stubs and expectations. sThey are not loaded by default, so you must require it first, and then include the module:
8
6
 
9
7
  ```ruby
10
8
  require "evil/client/rspec"
9
+
10
+ RSpec.describe CatsClient, "cats.fetch" do
11
+ include Evil::Client::RSpec
12
+ # ...
13
+ end
11
14
  ```
12
15
 
13
16
  Providing that you defined some client...
@@ -37,60 +40,87 @@ RSpec.describe CatsClient, "cats.fetch" do
37
40
  let(:scope) { client.cats(version: 1) }
38
41
 
39
42
  it "fetches a cat by id" do
40
- expect { scope.fetch(id: 8) }
41
- .to perform_operation("CatsClient.client.fetch")
43
+ stub_client_operation(CatsClient, "cats.fetch")
44
+ .with(token: "foo", version: 1, id: 8) # full hash of collected options
45
+ .to_return 8 # returned value by operation
46
+
47
+ expect(scope.fetch(id: 8)).to eq 8
48
+ expect_client_operation(CatsClient, "cats.fetch")
49
+ .to_have_been_performed
42
50
  end
43
51
  end
44
52
  ```
45
53
 
46
- You can add chaining using one of 3 additional methods: `with`, `with_exactly`, or `without`.
54
+ ## Selection
55
+
56
+ To select stubbed operations you can specify client class:
47
57
 
48
- ## with
58
+ ```ruby
59
+ stub_client_operation(CatsClient)
60
+ ```
49
61
 
50
- This method checks that the operation **includes some options**:
62
+ or its superclass
51
63
 
52
64
  ```ruby
53
- expect { scope.fetch(id: 8) }
54
- .to perform_operation("CatsClient.client.fetch")
55
- .with token: "foo"
65
+ stub_client_operation(Evil::Client)
56
66
  ```
57
67
 
58
- ## with_exactly
68
+ or leave it for default `Evil::Client`:
59
69
 
60
- This time you can check the full list of options given to operation:
70
+ ```ruby
71
+ stub_client_operation()
72
+ ```
73
+
74
+ or add a fully qualified name of the operation (for **exact** matching):
61
75
 
62
76
  ```ruby
63
- expect { scope.fetch(id: 8) }
64
- .to perform_operation("CatsClient.client.fetch")
65
- .with_exactly token: "foo", version: 1, id: 8
77
+ stub_client_operation(CatsClient, "cats.fetch")
66
78
  ```
67
79
 
68
- ## without
80
+ or regexp for partial matching:
69
81
 
70
- You can also check that some keys are absent:
82
+ ```ruby
83
+ stub_client_operation(CatsClient, /fetch/)
84
+ ```
85
+
86
+ or use method `with` to check options exactly:
71
87
 
72
88
  ```ruby
73
- expect { scope.fetch(id: 8) }
74
- .to perform_operation("CatsClient.client.fetch")
75
- .without :name, :email
89
+ stub_client_operation(CatsClient, "cats.fetch").with(token: "foo", version: 1, id: 8)
76
90
  ```
77
91
 
78
- This can be useful to check a behaviour of the client with optional attributes.
92
+ or partially:
93
+
94
+ ```ruby
95
+ stub_client_operation(CatsClient, "cats.fetch").with(hash_including(id: 8))
96
+ ```
79
97
 
80
- All checks can be negated as well:
98
+ or via block:
81
99
 
82
100
  ```ruby
83
- expect { scope.fetch(id: 8) }
84
- .not_to perform_operation("CatsClient.client.fetch")
85
- .with token: "foo"
101
+ stub_client_operation(CatsClient, "cats.fetch").with { |opts| opts[:id] == 8 }
86
102
  ```
87
103
 
88
- **Notice**: Under the hood the matcher doesn't stub the request, so its better to stub all requests by hand:
104
+ ## Return value
105
+
106
+ You **must** define some value returned by a stub:
89
107
 
90
108
  ```ruby
91
- require "webmock/rspec"
109
+ stub_client_operation(CatsClient, "cats.fetch").to_return(8)
110
+ ```
111
+
112
+ or fall back to original implementation:
92
113
 
93
- before { stub_request :any, // }
114
+ ```ruby
115
+ stub_client_operation(CatsClient, "cats.fetch").to_call_original
116
+ ```
117
+
118
+ or raise an exception:
119
+
120
+ ```ruby
121
+ stub_client_operation(CatsClient, "cats.fetch").to_raise StandardError, "Wrong id"
94
122
  ```
95
123
 
124
+ ## Some Hint
125
+
96
126
  [webmock]: https://github.com/bblimke/webmock
data/evil-client.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "evil-client"
3
- gem.version = "1.0.0"
3
+ gem.version = "1.1.0"
4
4
  gem.author = ["Andrew Kozin (nepalez)", "Ravil Bairamgalin (brainopia)"]
5
5
  gem.email = ["andrew.kozin@gmail.com", "nepalez@evilmartians.com"]
6
6
  gem.homepage = "https://github.com/evilmartians/evil-client"
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.add_runtime_dependency "dry-initializer", "~> 1.4"
17
17
  gem.add_runtime_dependency "i18n", "~> 0.8.6"
18
18
  gem.add_runtime_dependency "mime-types", "~> 3.1"
19
- gem.add_runtime_dependency "rack", "~> 1"
19
+ gem.add_runtime_dependency "rack", "~> 2"
20
20
 
21
21
  gem.add_development_dependency "rake", ">= 10"
22
22
  gem.add_development_dependency "rspec", "~> 3.0"
data/lib/evil/client.rb CHANGED
@@ -7,6 +7,7 @@ require "yaml"
7
7
  require "i18n"
8
8
  require "mime-types"
9
9
  require "securerandom"
10
+ require "delegate"
10
11
  require "dry-initializer"
11
12
  require "net/http"
12
13
  require "net/https"
@@ -18,6 +19,9 @@ module Evil
18
19
  # Absctract base class for clients to remote APIs
19
20
  #
20
21
  class Client
22
+ require_relative "client/names"
23
+ Names.clean(self) # Remove unnecessary methods from the instance
24
+
21
25
  require_relative "client/exceptions/definition_error"
22
26
  require_relative "client/exceptions/name_error"
23
27
  require_relative "client/exceptions/response_error"
@@ -25,6 +29,7 @@ module Evil
25
29
  require_relative "client/exceptions/validation_error"
26
30
 
27
31
  require_relative "client/chaining"
32
+ require_relative "client/options"
28
33
  require_relative "client/settings"
29
34
  require_relative "client/schema"
30
35
  require_relative "client/container"
@@ -9,6 +9,8 @@ class Evil::Client
9
9
  # for scope/operation instance whose options reload the [#parent]'s ones.
10
10
  #
11
11
  class Builder
12
+ Names.clean(self) # Remove unnecessary methods from the instance
13
+
12
14
  # Load concrete implementations for the abstact builder
13
15
  require_relative "builder/scope"
14
16
  require_relative "builder/operation"
@@ -3,6 +3,8 @@ class Evil::Client
3
3
  # Support chaining of calls for nested scopes/operations
4
4
  #
5
5
  module Chaining
6
+ Names.clean(self) # Remove unnecessary methods from the instance
7
+
6
8
  private
7
9
 
8
10
  def respond_to_missing?(name, *)
@@ -5,6 +5,8 @@ class Evil::Client
5
5
  # and methods to build sub-scope/operation or perform the current operation.
6
6
  #
7
7
  class Container
8
+ Names.clean(self) # Remove unnecessary methods from the instance
9
+
8
10
  # Loads concrete implementations of the abstract container
9
11
  require_relative "container/scope"
10
12
  require_relative "container/operation"
@@ -17,6 +19,18 @@ class Evil::Client
17
19
  # @return [Evil::Client::Settings]
18
20
  attr_reader :settings
19
21
 
22
+ # The client of the [#schema]
23
+ # @return [Class]
24
+ def client
25
+ schema.client
26
+ end
27
+
28
+ # The name of the current schema
29
+ # @return [String]
30
+ def name
31
+ schema.to_s
32
+ end
33
+
20
34
  # Options assigned to the [#settings]
21
35
  #
22
36
  # These are opts given to the [#initializer],
@@ -24,7 +38,7 @@ class Evil::Client
24
38
  #
25
39
  # @return [Hash<Symbol, Object>]
26
40
  def options
27
- @options ||= settings.options
41
+ settings.options
28
42
  end
29
43
 
30
44
  # The human-friendly representation of the scope instance
@@ -34,7 +48,7 @@ class Evil::Client
34
48
  #
35
49
  # @return [String]
36
50
  def to_s
37
- "#<#{schema} #{options.map { |key, val| "@#{key}=#{val}" }.join(', ')}>"
51
+ "#<#{name} #{options.map { |key, val| "@#{key}=#{val}" }.join(', ')}>"
38
52
  end
39
53
  alias_method :to_str, :to_s
40
54
  alias_method :inspect, :to_s
@@ -10,23 +10,21 @@ class Evil::Client
10
10
  # @return [Symbol] if name is valid
11
11
  # @raise [self] if name isn't valid
12
12
  #
13
- def self.check!(name, forbidden = [])
13
+ def self.check!(name)
14
14
  name = name.to_sym
15
- return name if name[FORMAT] && !forbidden.include?(name)
16
- raise new(name, forbidden)
15
+ return name if name[Names::FORMAT] && !Names::FORBIDDEN.include?(name)
16
+ raise new(name)
17
17
  end
18
18
 
19
19
  private
20
20
 
21
- def initialize(name, forbidden)
21
+ def initialize(name)
22
22
  super "Invalid name :#{name}." \
23
23
  " It should contain latin letters in the lower case, digits," \
24
24
  " and underscores only; have minimum 2 chars;" \
25
25
  " start from a letter; end with either letter or digit." \
26
- " The following names: '#{forbidden.join("', '")}'" \
26
+ " The following names: '#{Names::FORBIDDEN.join("', '")}'" \
27
27
  " are already used by Evil::Client."
28
28
  end
29
-
30
- FORMAT = /^[a-z]([a-z\d_])*[a-z\d]$/
31
29
  end
32
30
  end
@@ -0,0 +1,54 @@
1
+ class Evil::Client
2
+ #
3
+ # Utility to remove unnecessary methods from instances
4
+ # to clear a namespace
5
+ # @private
6
+ #
7
+ module Names
8
+ extend self
9
+
10
+ # Removes unused instance methods inherited from [Object] from given class
11
+ #
12
+ # @param [Class] klass
13
+ # @return [nil]
14
+ #
15
+ def clean(klass)
16
+ (klass.instance_methods - BasicObject.instance_methods - FORBIDDEN)
17
+ .each { |m| klass.send(:undef_method, m) if m[FORMAT] } && nil
18
+ end
19
+
20
+ # List of preserved methods.
21
+ # They also couldn't be used as names of operations/scopes/options
22
+ # to avoid name conflicts.
23
+ FORBIDDEN = %i[
24
+ basic_auth
25
+ class
26
+ datetime
27
+ hash
28
+ inspect
29
+ instance_exec
30
+ instance_variable_get
31
+ instance_variable_set
32
+ key_auth
33
+ logger
34
+ logger
35
+ object_id
36
+ operation
37
+ operations
38
+ options
39
+ schema
40
+ scope
41
+ scopes
42
+ self
43
+ send
44
+ settings
45
+ singleton_class
46
+ to_s
47
+ to_str
48
+ token_auth
49
+ ].freeze
50
+
51
+ # Matches whether a name can be used in operations/scopes/options
52
+ FORMAT = /^[a-z]([a-z\d_])*[a-z\d]$/
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ class Evil::Client
2
+ # Wraps hash of options with railsy methods [#slice] and [#except]
3
+ #
4
+ # Both methods works on the root level only.
5
+ # Nevertheless, this is sufficient to select/reject a part of the whole
6
+ # options collected from the very root of the client.
7
+ #
8
+ class Options < SimpleDelegator
9
+ # Returns a new hash which include only selected keys
10
+ #
11
+ # @param [Object, Array<Object>] keys
12
+ # @return [Hash]
13
+ #
14
+ def slice(*keys)
15
+ select { |key| keys.flatten.include? key }
16
+ end
17
+
18
+ # Returns a new hash where some keys are excluded from
19
+ #
20
+ # @param [Object, Array<Object>] keys
21
+ # @return [Hash]
22
+ #
23
+ def except(*keys)
24
+ reject { |key| keys.flatten.include? key }
25
+ end
26
+ end
27
+ end
@@ -7,6 +7,8 @@ class Evil::Client
7
7
  # (request, middleware or response).
8
8
  #
9
9
  class Resolver
10
+ Names.clean(self) # Remove unnecessary methods from the instance
11
+
10
12
  # Loads concrete implementation of the abstract resolver
11
13
  require_relative "resolver/request"
12
14
  require_relative "resolver/middleware"
@@ -1,127 +1,25 @@
1
- #
2
- # Checks that an operation has been performed with given options
3
- #
4
- # @example
5
- # subject do
6
- # MyClient.new(token: "foo").users(version: 3).fetch(token: "bar", id: 42)
7
- # end
8
- #
9
- # # call only matcher
10
- # expect { subject }.to perform_operation("MyClient.users.fetch")
11
- #
12
- # # exact matcher
13
- # expect { subject }
14
- # .to perform_operation("MyClient.users.fetch")
15
- # .with_exactly(token: "bar", version: 3, id: 42)
16
- #
17
- # # partial matcher
18
- # expect { subject }
19
- # .to perform_operation("MyClient.users.fetch")
20
- # .with(token: "bar", version: 3)
21
- #
22
- # # absence matcher
23
- # expect { subject }
24
- # .to perform_operation("MyClient.users.fetch")
25
- # .without(:user, :password)
26
- #
27
- # # block syntax
28
- # expect { subject }
29
- # .to perform_operation("MyClient.users.fetch")
30
- # .with { |token:, **| expect(token).to eq "bar" }
31
- #
32
- # @param [String] name The full name of the operation
33
- #
34
- RSpec::Matchers.define :perform_operation do |name|
35
- supports_block_expectations
36
-
37
- description { "perform operation #{name} " }
38
-
39
- chain :with do |**options|
40
- @some_options = options
41
- end
42
-
43
- chain :with_exactly do |**options|
44
- @exact_options = options
45
- end
46
-
47
- chain :without do |*options|
48
- @no_options = options.flatten.map(&:to_sym)
49
- end
50
-
51
- def full_signature(name)
52
- name.dup.tap do |text|
53
- text << " with options #{@exact_options}" if @exact_options
54
- text << " with options including #{@some_options}" if @some_options
55
- text << " without options :#{@no_options.join(', :')}" if @no_options
1
+ class Evil::Client
2
+ #
3
+ # Collection of RSpec-related definitions
4
+ #
5
+ module RSpec
6
+ require_relative "rspec/evil_client_schema_matching"
7
+ require_relative "rspec/base_stub"
8
+ require_relative "rspec/allow_stub"
9
+ require_relative "rspec/expect_stub"
10
+
11
+ def stub_client_operation(klass = Evil::Client, name = nil)
12
+ AllowStub.new(klass, name)
56
13
  end
57
- end
58
-
59
- # rubocop: disable Metrics/CyclomaticComplexity
60
- # rubocop: disable Style/InverseMethods
61
- def expected_options?(options, check)
62
- return if @exact_options && options != @exact_options
63
- return if @some_options && !(options >= @some_options)
64
- return if (options.keys & @no_options.to_a).any?
65
- check.nil? || check.call(options)
66
- end
67
- # rubocop: enable Metrics/CyclomaticComplexity
68
- # rubocop: enable Style/InverseMethods
69
-
70
- def stub_resolver
71
- resolver = Evil::Client::Resolver::Request
72
14
 
73
- allow(resolver).to receive(:call) do |schema, settings|
74
- register(schema, settings)
75
- resolver.new(schema, settings).send(:__call__)
15
+ def expect_client_operation(klass, name = nil)
16
+ ExpectStub.new(klass, name)
76
17
  end
77
- end
78
-
79
- def actual_operations
80
- @actual_operations ||= []
81
- end
82
-
83
- def register(schema, settings)
84
- actual_operations << [schema.to_s, settings.options]
85
- end
86
-
87
- def performed(name, check)
88
- @performed ||= actual_operations.find do |(key, options)|
89
- (key == name) && expected_options?(options, check)
90
- end
91
- end
92
-
93
- match do |block|
94
- stub_resolver
95
- block.call
96
- !performed(name, block_arg).nil?
97
- end
98
-
99
- match_when_negated do |block|
100
- stub_resolver
101
- block.call
102
- performed(name, block_arg).nil?
103
- end
104
-
105
- def describe_expectations(name, perform)
106
- "It was expected the operation #{full_signature(name)}" \
107
- " #{'NOT ' unless perform}to be performed.\n" \
108
- "The following operations has been actually performed:"
109
- end
110
-
111
- failure_message do
112
- text = describe_expectations(name, true)
113
- actual_operations.each.with_index(1) do |(key, opts), index|
114
- text << format("\n %02d) #{key} with #{opts}", index)
115
- end
116
- text
117
- end
118
18
 
119
- failure_message_when_negated do
120
- text = describe_expectations(name, false)
121
- actual_operations.each.with_index(1) do |(key, opts), index|
122
- marker = performed(name, block_arg) == [key, opts] ? "->" : " "
123
- text << format("\n#{marker} % 2d) #{key} with #{opts}", index)
19
+ def unstub_all
20
+ allow(Evil::Client::Container::Operation)
21
+ .to receive(:new)
22
+ .and_call_original
124
23
  end
125
- text
126
24
  end
127
25
  end