webmock-twirp 0.2.0 → 0.3.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
  SHA256:
3
- metadata.gz: 5a732c5a4926da1aa55cfb9348ea84c0cd7506fabc1e598a746dabd4fa57a2c9
4
- data.tar.gz: e4ec2abe817732781453442877ffbae84771919cebed81238360c3a002ef7820
3
+ metadata.gz: f1a34362dd929abe47a175e407335cc6f94a307c00fd902742d06285550fcd78
4
+ data.tar.gz: efefa4e116a259eeab30129f7afe2b803f682daf0e0c15785e48720c5efe5d70
5
5
  SHA512:
6
- metadata.gz: 28e13a626132b193b2d3aaa26c5d192949d296171d63d2b5bc2539fd88eab2a72c633d29bc0a4711d804ba7be144de2dc57a1be8b1d71a5d0b614fa3c927885b
7
- data.tar.gz: 7b42012c4c5ea5c49868091dddf44cdafcd4fcb7b76dadae2bd91135c8bb5b65307aca1797eef8153d9839fe72ddf08c0178156227f0c74b0fe561a03642d829
6
+ metadata.gz: 04c11a624eb11547a7d0df20fcddd5b40015e75b6af94311e764484d98bc1333ffd728b085e0b5a29b08b4b039945d209a7ae74a9312bd0b65d9bebdcf87e43c
7
+ data.tar.gz: 2179804f8df26c7c65d17781cd0a4df2c5c9806697e97c1f794f2bb008a5d5febdad89522df69b853ddfd6d2a869092b8dc572a5f5ac500fb5db3a5e30995215
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ### v0.3.0 (2022-10-28)
2
+ - twirpify all the things
3
+ - improve refinements
4
+ - make_twirp_request rspec matcher
5
+
6
+ ### v0.2.1 (2022-10-19)
7
+ - fix client matching
8
+
1
9
  ### v0.2.0 (2022-10-17)
2
10
  - improved matching
3
11
 
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  WebMock::Twirp
2
2
  ======
3
+ ![Gem](https://img.shields.io/gem/dt/webmock-twirp?style=plastic)
4
+ [![codecov](https://codecov.io/gh/dpep/webmock-twirp/branch/main/graph/badge.svg)](https://codecov.io/gh/dpep/webmock-twirp)
5
+
3
6
  Twirp support for [WebMock](https://github.com/bblimke/webmock). All our favorite http request stubbing for Twirp RPCs - message and error serialization done automatically.
4
7
 
5
8
  ### Install
@@ -106,8 +109,3 @@ Yes please :)
106
109
  1. Commit your changes (`git commit -am 'awesome new feature'`)
107
110
  1. Push your branch (`git push origin my-feature`)
108
111
  1. Create a Pull Request
109
-
110
-
111
- ----
112
- ![Gem](https://img.shields.io/gem/dt/webmock-twirp?style=plastic)
113
- [![codecov](https://codecov.io/gh/dpep/webmock-twirp/branch/main/graph/badge.svg)](https://codecov.io/gh/dpep/webmock-twirp)
@@ -0,0 +1,36 @@
1
+ module WebMock
2
+ module Twirp
3
+ module NetConnectNotAllowedError
4
+ def initialize(request_signature)
5
+ @request_signature = request_signature
6
+ end
7
+
8
+ def message
9
+ snippet = WebMock::RequestSignatureSnippet.new(@request_signature)
10
+
11
+ text = [
12
+ "Real Twirp connections are disabled. Unregistered request: #{@request_signature}",
13
+ snippet.stubbing_instructions,
14
+ snippet.request_stubs,
15
+ "="*60
16
+ ].compact.join("\n\n")
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ # hijack creation of Twirp errors
23
+ module WebMock
24
+ class NetConnectNotAllowedError
25
+ def self.new(request_signature)
26
+ allocate.tap do |instance|
27
+ if request_signature.is_a?(WebMock::Twirp::RequestSignature)
28
+ instance.extend(WebMock::Twirp::NetConnectNotAllowedError)
29
+ end
30
+
31
+ instance.send(:initialize, request_signature)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,46 @@
1
+ module WebMock
2
+ module Twirp
3
+ module Matchers
4
+ class MakeTwirpRequest
5
+ def initialize(*matchers)
6
+ @stub = WebMock::Twirp::RequestStub.new(*matchers)
7
+ end
8
+
9
+ def method_missing(name, *args, **kwargs, &block)
10
+ super unless respond_to?(name)
11
+ @stub.send(name, *args, **kwargs, &block)
12
+
13
+ self
14
+ end
15
+
16
+ def respond_to?(method_name, include_private = false)
17
+ super || @stub.respond_to?(method_name)
18
+ end
19
+
20
+ def matches?(block)
21
+ unless block.is_a?(Proc)
22
+ raise ArgumentError, "expected block, found: #{block}"
23
+ end
24
+
25
+ WebMock::StubRegistry.instance.register_request_stub(@stub)
26
+ block.call
27
+ WebMock::StubRegistry.instance.remove_request_stub(@stub)
28
+
29
+ RequestRegistry.instance.times_executed(@stub) > 0
30
+ end
31
+
32
+ def failure_message
33
+ "expected a Twirp request but received none"
34
+ end
35
+
36
+ def failure_message_when_negated
37
+ "did not expect a Twirp request, but received one"
38
+ end
39
+
40
+ def supports_block_expectations?
41
+ true
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ require "webmock/twirp/matchers/make_twirp_request"
2
+
3
+ module WebMock
4
+ module Twirp
5
+ module Matchers
6
+ def make_twirp_request(...)
7
+ MakeTwirpRequest.new(...)
8
+ end
9
+
10
+ alias_method :make_a_twirp_request, :make_twirp_request
11
+ end
12
+ end
13
+ end
@@ -71,6 +71,12 @@ module WebMock
71
71
  end
72
72
  end
73
73
  end
74
+
75
+ refine ::WebMock::RequestSignature do
76
+ def proto_headers?
77
+ !!(headers&.fetch("Content-Type", nil)&.start_with?(::Twirp::Encoding::PROTO))
78
+ end
79
+ end
74
80
  end
75
81
  end
76
82
  end
@@ -0,0 +1,39 @@
1
+ using WebMock::Twirp::Refinements
2
+
3
+ module WebMock
4
+ module Twirp
5
+ module RequestBodyDiff
6
+
7
+ private
8
+
9
+ def request_signature_diffable?
10
+ !!request_signature.twirp_request
11
+ end
12
+
13
+ def request_stub_diffable?
14
+ !!request_stub.with_attrs && !request_stub.with_attrs.is_a?(Proc)
15
+ end
16
+
17
+ def request_signature_body_hash
18
+ request_signature.twirp_request.normalized_hash
19
+ end
20
+
21
+ def request_stub_body_hash
22
+ request_stub.with_attrs
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # hijack creation of Twirp snippets
29
+ module WebMock
30
+ class RequestBodyDiff
31
+ def self.new(request_signature, request_stub)
32
+ super.tap do |instance|
33
+ if request_signature.proto_headers? && request_stub.is_a?(WebMock::Twirp::RequestStub)
34
+ instance.extend(WebMock::Twirp::RequestBodyDiff)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,55 @@
1
+ using WebMock::Twirp::Refinements
2
+
3
+ module WebMock
4
+ module Twirp
5
+ module RequestSignature
6
+ def twirp_client
7
+ @twirp_client ||= begin
8
+ service_full_name, rpc_name = uri.path.split("/").last(2)
9
+
10
+ # find matching client
11
+ client = ObjectSpace.each_object(::Twirp::Client.singleton_class).find do |obj|
12
+ next unless obj < ::Twirp::Client && obj.name
13
+ obj.service_full_name == service_full_name && obj.rpcs.key?(rpc_name)
14
+ end
15
+ end
16
+ end
17
+
18
+ def twirp_rpc
19
+ @twirp_rpc ||= begin
20
+ rpc_name = uri.path.split("/").last
21
+ client = twirp_client.rpcs[rpc_name] if twirp_client
22
+ end
23
+ end
24
+
25
+ def twirp_request
26
+ twirp_rpc[:input_class].decode(body) if twirp_rpc
27
+ end
28
+
29
+ def to_s
30
+ return super unless twirp_rpc
31
+
32
+ uri = WebMock::Util::URI.strip_default_port_from_uri_string(self.uri.to_s)
33
+ params = twirp_request.normalized_hash.map do |k, v|
34
+ "#{k}: #{v.inspect}"
35
+ end.join(", ")
36
+
37
+ string = "#{twirp_client}(#{uri})"
38
+ string << ".#{twirp_rpc[:ruby_method]}"
39
+ string << "(#{params.empty? ? "{}" : params})"
40
+
41
+ string
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ module WebMock
48
+ class RequestSignature
49
+ def self.new(...)
50
+ super(...).tap do |instance|
51
+ instance.extend(WebMock::Twirp::RequestSignature) if instance.proto_headers?
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,40 @@
1
+ using WebMock::Twirp::Refinements
2
+
3
+ module WebMock
4
+ module Twirp
5
+ module RequestSignatureSnippet
6
+ def stubbing_instructions
7
+ return unless WebMock.show_stubbing_instructions?
8
+
9
+ client = @request_signature.twirp_client
10
+ rpc = @request_signature.twirp_rpc
11
+
12
+ return super unless client
13
+
14
+ string = "You can stub this request with the following snippet:\n\n"
15
+ string << "stub_twirp_request(#{rpc[:ruby_method].inspect})"
16
+
17
+ if request = @request_signature.twirp_request
18
+ params = request.normalized_hash.map do |k, v|
19
+ " #{k}: #{v.inspect},"
20
+ end.join("\n")
21
+
22
+ string << ".with(\n#{params}\n)" unless params.empty?
23
+ end
24
+
25
+ string << ".to_return(...)"
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ # hijack creation of Twirp snippets
32
+ module WebMock
33
+ class RequestSignatureSnippet
34
+ def self.new(request_signature)
35
+ super.tap do |instance|
36
+ instance.extend(WebMock::Twirp::RequestSignatureSnippet) if request_signature.proto_headers?
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,6 +3,8 @@ module WebMock
3
3
  class RequestStub < WebMock::RequestStub
4
4
  using Refinements
5
5
 
6
+ attr_reader :twirp_client, :rpc_name, :with_attrs
7
+
6
8
  def initialize(*filters)
7
9
  rpc_name = filters.snag { |x| x.is_a?(Symbol) }
8
10
 
@@ -21,19 +23,15 @@ module WebMock
21
23
 
22
24
  if client
23
25
  conn = client.instance_variable_get(:@conn)
24
- uri += conn.url_prefix.to_s if conn
26
+ uri += conn.url_prefix.to_s.chomp("/") if conn
25
27
  end
26
28
 
27
29
  if klass
28
- @rpcs = klass.rpcs
30
+ @twirp_client = klass
29
31
  uri += "/#{klass.service_full_name}"
30
- else
31
- uri += "/[^/]+"
32
- end
33
32
 
34
- if rpc_name
35
- if klass
36
- # kindly convert ruby method to rpc method name
33
+ if rpc_name
34
+ # type check and kindly convert ruby method to rpc method name
37
35
  rpc_info = klass.rpcs.values.find do |x|
38
36
  x[:rpc_method] == rpc_name || x[:ruby_method] == rpc_name
39
37
  end
@@ -41,19 +39,29 @@ module WebMock
41
39
  raise ArgumentError, "invalid rpc method: #{rpc_name}" unless rpc_info
42
40
 
43
41
  uri += "/#{rpc_info[:rpc_method]}"
44
- else
45
- uri += "/#{rpc_name}"
46
42
  end
47
- else
48
- uri += "/[^/]+"
49
43
  end
50
44
 
51
- super(:post, /#{uri}$/)
45
+ super(:post, /#{uri}/)
52
46
 
53
47
  # filter on Twirp header
54
48
  @request_pattern.with(
55
49
  headers: { "Content-Type" => ::Twirp::Encoding::PROTO },
56
50
  )
51
+
52
+ if rpc_name
53
+ # match rpc dynamically after client resolves
54
+ @rpc_name = rpc_name
55
+
56
+ @request_pattern.with do |request|
57
+ rpc_info = request.twirp_rpc
58
+
59
+ !!rpc_info && (
60
+ rpc_info[:rpc_method] == rpc_name ||
61
+ rpc_info[:ruby_method] == rpc_name
62
+ )
63
+ end
64
+ end
57
65
  end
58
66
 
59
67
  def with(request = nil, **attrs, &block)
@@ -69,16 +77,31 @@ module WebMock
69
77
  { body: request.to_proto }
70
78
  end
71
79
 
72
- decoder = ->(request) do
73
- input_class = rpc_from_request(request)[:input_class]
74
- decoded_request = input_class.decode(request.body)
80
+ # save for diffing
81
+ @with_attrs = if block_given?
82
+ block
83
+ elsif request
84
+ request
85
+ elsif attrs.any?
86
+ attrs
87
+ end
75
88
 
89
+ decoder = ->(request_signature) do
76
90
  matched = true
77
- matched &= decoded_request.include?(attrs) if attrs.any?
78
- matched &= block.call(decoded_request) if block
91
+
92
+ request = request_signature.twirp_request
93
+ matched &&= !!request
94
+
95
+ # match request attributes
96
+ if attrs.any?
97
+ matched &&= request.include?(attrs)
98
+ end
99
+
100
+ # match block
101
+ matched &&= block.call(request) if block_given?
79
102
 
80
103
  matched
81
- end if attrs.any? || block_given?
104
+ end
82
105
 
83
106
  super(request_matcher || {}, &decoder)
84
107
  end
@@ -93,17 +116,15 @@ module WebMock
93
116
 
94
117
  response_hashes = responses.map do |response|
95
118
  ->(request) do
96
- # determine msg type and package response
97
- output_class = rpc_from_request(request)[:output_class]
98
- generate_http_response(output_class, response)
119
+ generate_http_response(request, response)
99
120
  end
100
121
  end
101
122
 
102
123
  decoder = ->(request) do
103
- # determine msg type and call provided block
104
- rpc = rpc_from_request(request)
105
- res = block.call(rpc[:input_class].decode(request.body))
106
- generate_http_response(rpc[:output_class], res)
124
+ generate_http_response(
125
+ request,
126
+ block.call(request.twirp_request),
127
+ )
107
128
  end if block_given?
108
129
 
109
130
  super(*response_hashes, &decoder)
@@ -116,27 +137,13 @@ module WebMock
116
137
 
117
138
  private
118
139
 
119
- def rpc_from_request(request_signature)
120
- service_full_name, rpc_name = request_signature.uri.path.split("/").last(2)
140
+ def generate_http_response(request, obj)
141
+ msg_class = request.twirp_rpc&.fetch(:output_class)
121
142
 
122
- rpcs = @rpcs || begin
123
- # find matching client instance
124
- client = ObjectSpace.each_object(::Twirp::Client).find do |client|
125
- service_full_name == client.class.service_full_name && \
126
- client.class.rpcs.key?(rpc_name)
127
- end
128
-
129
- unless client
130
- raise "could not determine Twirp::Client for call to: #{service_full_name}/#{rpc_name}"
131
- end
132
-
133
- client.class.rpcs
143
+ if msg_class.nil?
144
+ raise "could not determine Twirp::Client for request: #{request}"
134
145
  end
135
146
 
136
- rpcs[rpc_name]
137
- end
138
-
139
- def generate_http_response(msg_class, obj)
140
147
  res = case obj
141
148
  when nil
142
149
  msg_class.new
@@ -0,0 +1,46 @@
1
+ module WebMock
2
+ module Twirp
3
+ module StubRequestSnippet
4
+ def to_s(with_response = true)
5
+ string = "stub_twirp_request"
6
+
7
+ filters = [
8
+ @request_stub.twirp_client,
9
+ @request_stub.rpc_name&.inspect,
10
+ ].compact.join(", ")
11
+ string << "(#{filters})" unless filters.empty?
12
+
13
+ if attrs = @request_stub.with_attrs
14
+ string << ".with(\n"
15
+
16
+ if attrs.is_a?(Hash)
17
+ string << attrs.map do |k, v|
18
+ " #{k}: #{v.inspect},"
19
+ end.join("\n")
20
+ elsif attrs.is_a?(Proc)
21
+ string << " { ... }"
22
+ else
23
+ string << " #{attrs}"
24
+ end
25
+
26
+ string << "\n)"
27
+ end
28
+
29
+ string
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ # hijack creation of Twirp snippets
36
+ module WebMock
37
+ class StubRequestSnippet
38
+ def self.new(request_stub)
39
+ super.tap do |instance|
40
+ if request_stub.is_a?(WebMock::Twirp::RequestStub)
41
+ instance.extend(WebMock::Twirp::StubRequestSnippet)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,5 +1,5 @@
1
1
  module WebMock
2
2
  module Twirp
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
data/lib/webmock/twirp.rb CHANGED
@@ -1,11 +1,16 @@
1
1
  require "google/protobuf"
2
2
  require "twirp"
3
3
  require "webmock"
4
+ require "webmock/twirp/error"
5
+ require "webmock/twirp/matchers"
4
6
  require "webmock/twirp/refinements"
7
+ require "webmock/twirp/request_body_diff"
8
+ require "webmock/twirp/request_signature"
9
+ require "webmock/twirp/request_signature_snippet"
5
10
  require "webmock/twirp/request_stub"
11
+ require "webmock/twirp/stub_request_snippet"
6
12
  require "webmock/twirp/version"
7
13
 
8
-
9
14
  module WebMock
10
15
  module Twirp
11
16
  extend self
@@ -26,15 +31,22 @@ module WebMock
26
31
  end
27
32
  end
28
33
 
34
+ # patch WebMock to export Twirp helpers
35
+ module WebMock
36
+ module API
37
+ include WebMock::Twirp::API
38
+ end
29
39
 
30
- if $LOADED_FEATURES.find { |x| x =~ %r{/webmock/rspec.rb} }
31
- # require "webmock/rspec"
32
- RSpec.configure { |c| c.include WebMock::Twirp::API }
33
- else
34
- # patch WebMock to also export stub_twirp_request
35
- module WebMock
36
- module API
37
- include WebMock::Twirp::API
38
- end
40
+ module Matchers
41
+ include WebMock::Twirp::Matchers
42
+ end
43
+ end
44
+
45
+ if $LOADED_FEATURES.find { |x| x =~ %r{/webmock/rspec.rb$} }
46
+ # require "webmock/rspec" was already called, so load helpers
47
+
48
+ RSpec.configure do |conf|
49
+ conf.include WebMock::Twirp::API
50
+ conf.include WebMock::Twirp::Matchers
39
51
  end
40
52
  end
@@ -26,6 +26,8 @@ Gem::Specification.new do |s|
26
26
 
27
27
  s.add_development_dependency "byebug"
28
28
  s.add_development_dependency "codecov"
29
+ s.add_development_dependency "rack"
29
30
  s.add_development_dependency "rspec"
30
31
  s.add_development_dependency "simplecov"
32
+ s.add_development_dependency "webrick"
31
33
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webmock-twirp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-18 00:00:00.000000000 Z
11
+ date: 2022-10-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: webmock
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rack
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rspec
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +108,20 @@ dependencies:
94
108
  - - ">="
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webrick
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
97
125
  description: Twirp support for WebMock
98
126
  email:
99
127
  executables: []
@@ -105,8 +133,15 @@ files:
105
133
  - README.md
106
134
  - lib/webmock-twirp.rb
107
135
  - lib/webmock/twirp.rb
136
+ - lib/webmock/twirp/error.rb
137
+ - lib/webmock/twirp/matchers.rb
138
+ - lib/webmock/twirp/matchers/make_twirp_request.rb
108
139
  - lib/webmock/twirp/refinements.rb
140
+ - lib/webmock/twirp/request_body_diff.rb
141
+ - lib/webmock/twirp/request_signature.rb
142
+ - lib/webmock/twirp/request_signature_snippet.rb
109
143
  - lib/webmock/twirp/request_stub.rb
144
+ - lib/webmock/twirp/stub_request_snippet.rb
110
145
  - lib/webmock/twirp/version.rb
111
146
  - webmock-twirp.gemspec
112
147
  homepage: https://github.com/dpep/webmock-twirp