webmock-twirp 0.2.1 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b68a561786519f4bf91828a3a01e4ae803e36e03af7fd327af7780ba5da4d838
4
- data.tar.gz: 151994b76bc7d4494be3f302f20633a19fdb681a092d4f1d489e694f89d43189
3
+ metadata.gz: 292380b89f2b938b849edb2e4ce46cb84e197b6a39592d16425440446adce9a1
4
+ data.tar.gz: 64be8b4452a96b256adc5bfe9d08a479392d9cb916f4fe0ab1135cc763728f87
5
5
  SHA512:
6
- metadata.gz: b764858403de27f0b9cc28a6bddb7c827379308c1619be1b8bcbeff8d1824a3cb1c3f892644ce63c78c2bc4f1331e4fdb4a60eb88713541e0161d899a4ddaa1c
7
- data.tar.gz: 43a24c5182e1aab84cbc16897faae7ea54098be328ad07e5754b939c6b89783985c23f1d565db89fa751fb5e1edb719d72a0fef264b64ae1ceadbc877e8b92d0
6
+ metadata.gz: c4f52185995140dc0d2aa2646227bbd9195a87941a1ab40a20d6e1dfb1b29bf24c9b0a50796db26fecd453f802fecc5bd5003eee284f325ccea3a96bf4f152a6
7
+ data.tar.gz: fa2e3ed00ed69f960dae3c500979fc8a745cd6e78015c68d6b963ee85ffad842070855abcdd8d50e6f37263f2789ead2f231d50092698c496bd0e4e5b8251970
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ### v0.4.0 (2022-11-16)
2
+ - .to_return(Twirp::ClientResp)
3
+ - revert client class name filter
4
+ - unqualified client class filter
5
+ - uri matching
6
+
7
+ ### v0.3.0 (2022-10-28)
8
+ - twirpify all the things
9
+ - improve refinements
10
+ - make_twirp_request rspec matcher
11
+
1
12
  ### v0.2.1 (2022-10-19)
2
13
  - fix client matching
3
14
 
data/README.md CHANGED
@@ -97,6 +97,45 @@ stub_twirp_request.to_return do |request|
97
97
  end
98
98
  ```
99
99
 
100
+ ## Improved WebMock Errors
101
+ Before
102
+ ```ruby
103
+ > client = EchoClient.new("http://example.com/twirp")
104
+ ...
105
+ > client.echo(msg: "Hi")
106
+ /lib/webmock/http_lib_adapters/net_http.rb:104:in `request': Real HTTP connections are disabled. Unregistered request:
107
+ POST http://example.com/twirp/Echo/Echo with body ' (WebMock::NetConnectNotAllowedError)
108
+ Hi' with headers {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type'=>'application/protobuf', 'User-Agent'=>'Faraday v1.10.2'}
109
+
110
+ You can stub this request with the following snippet:
111
+
112
+ stub_request(:post, "http://example.com/twirp/Echo/Echo").
113
+ with(
114
+ body: "\n\x02Hi",
115
+ headers: {
116
+ 'Accept'=>'*/*',
117
+ 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
118
+ 'Content-Type'=>'application/protobuf',
119
+ 'User-Agent'=>'Faraday v1.10.2'
120
+ }).
121
+ to_return(status: 200, body: "", headers: {})
122
+ ```
123
+
124
+ After
125
+ ```ruby
126
+ > require "webmock-twirp"
127
+ > client = EchoClient.new("http://example.com/twirp")
128
+ ...
129
+ > client.echo(msg: "Hi")
130
+ /lib/webmock/http_lib_adapters/net_http.rb:104:in `request': Real Twirp connections are disabled. Unregistered request:
131
+ EchoClient(http://example.com/twirp/Echo/Echo).echo(msg: "Hi") (WebMock::NetConnectNotAllowedError)
132
+
133
+ You can stub this request with the following snippet:
134
+
135
+ stub_twirp_request(:echo).with(
136
+ msg: "Hi",
137
+ ).to_return(...)
138
+ ```
100
139
 
101
140
  ----
102
141
  ## Contributing
@@ -0,0 +1,37 @@
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:",
13
+ @request_signature,
14
+ snippet.stubbing_instructions,
15
+ snippet.request_stubs,
16
+ "="*60
17
+ ].compact.join("\n\n")
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # hijack creation of Twirp errors
24
+ module WebMock
25
+ class NetConnectNotAllowedError
26
+ def self.new(request_signature)
27
+ allocate.tap do |instance|
28
+ if request_signature.is_a?(WebMock::Twirp::RequestSignature)
29
+ instance.extend(WebMock::Twirp::NetConnectNotAllowedError)
30
+ end
31
+
32
+ instance.send(:initialize, request_signature)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -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
@@ -8,16 +8,16 @@ module WebMock
8
8
  end
9
9
 
10
10
  refine Google::Protobuf::MessageExts do
11
- def normalized_hash(symbolize_keys: true)
11
+ def normalized_hash
12
12
  res = {}
13
13
 
14
14
  self.class.descriptor.each do |field|
15
- key = symbolize_keys ? field.name.to_sym : field.name
15
+ key = field.name.to_sym
16
16
  value = field.get(self)
17
17
 
18
18
  if value.is_a?(Google::Protobuf::MessageExts)
19
19
  # recursively serialize sub-message
20
- value = value.normalized_hash(symbolize_keys: symbolize_keys)
20
+ value = value.normalized_hash
21
21
  end
22
22
 
23
23
  res[key] = value unless field.default == value
@@ -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
@@ -1,11 +1,11 @@
1
+ require "uri"
2
+
1
3
  module WebMock
2
4
  module Twirp
3
5
  class RequestStub < WebMock::RequestStub
4
6
  using Refinements
5
7
 
6
8
  def initialize(*filters)
7
- rpc_name = filters.snag { |x| x.is_a?(Symbol) }
8
-
9
9
  client = filters.snag { |x| x.is_a?(::Twirp::Client) }
10
10
 
11
11
  klass = client&.class
@@ -13,11 +13,21 @@ module WebMock
13
13
  x.is_a?(Class) && (x < ::Twirp::Client || x < ::Twirp::Service)
14
14
  end
15
15
 
16
+ rpc_name = filters.snag { |x| x.is_a?(Symbol) }
17
+
18
+ uri = filters.snag do |x|
19
+ x.is_a?(String) && x.start_with?("http") && x =~ URI::regexp
20
+ end
21
+
22
+ if client && uri
23
+ raise ArgumentError, "specify uri or client instance, but not both"
24
+ end
25
+
16
26
  unless filters.empty?
17
27
  raise ArgumentError, "unexpected arguments: #{filters}"
18
28
  end
19
29
 
20
- uri = ""
30
+ uri ||= ""
21
31
 
22
32
  if client
23
33
  conn = client.instance_variable_get(:@conn)
@@ -25,15 +35,11 @@ module WebMock
25
35
  end
26
36
 
27
37
  if klass
28
- @rpcs = klass.rpcs
38
+ @twirp_client = klass
29
39
  uri += "/#{klass.service_full_name}"
30
- else
31
- uri += "/[^/]+"
32
- end
33
40
 
34
- if rpc_name
35
- if klass
36
- # kindly convert ruby method to rpc method name
41
+ if rpc_name
42
+ # type check and kindly convert ruby method to rpc method name
37
43
  rpc_info = klass.rpcs.values.find do |x|
38
44
  x[:rpc_method] == rpc_name || x[:ruby_method] == rpc_name
39
45
  end
@@ -41,19 +47,29 @@ module WebMock
41
47
  raise ArgumentError, "invalid rpc method: #{rpc_name}" unless rpc_info
42
48
 
43
49
  uri += "/#{rpc_info[:rpc_method]}"
44
- else
45
- uri += "/#{rpc_name}"
46
50
  end
47
- else
48
- uri += "/[^/]+"
49
51
  end
50
52
 
51
- super(:post, /#{uri}$/)
53
+ super(:post, /#{uri}/)
52
54
 
53
55
  # filter on Twirp header
54
56
  @request_pattern.with(
55
57
  headers: { "Content-Type" => ::Twirp::Encoding::PROTO },
56
58
  )
59
+
60
+ if rpc_name
61
+ # match rpc dynamically after client resolves
62
+ @rpc_name = rpc_name
63
+
64
+ @request_pattern.with do |request|
65
+ rpc_info = request.twirp_rpc
66
+
67
+ !!rpc_info && (
68
+ rpc_info[:rpc_method] == rpc_name ||
69
+ rpc_info[:ruby_method] == rpc_name
70
+ )
71
+ end
72
+ end
57
73
  end
58
74
 
59
75
  def with(request = nil, **attrs, &block)
@@ -69,16 +85,31 @@ module WebMock
69
85
  { body: request.to_proto }
70
86
  end
71
87
 
72
- decoder = ->(request) do
73
- input_class = rpc_from_request(request)[:input_class]
74
- decoded_request = input_class.decode(request.body)
88
+ # save for diffing
89
+ @with_attrs = if block_given?
90
+ block
91
+ elsif request
92
+ request
93
+ elsif attrs.any?
94
+ attrs
95
+ end
75
96
 
97
+ decoder = ->(request_signature) do
76
98
  matched = true
77
- matched &= decoded_request.include?(attrs) if attrs.any?
78
- matched &= block.call(decoded_request) if block
99
+
100
+ request = request_signature.twirp_request
101
+ matched &&= !!request
102
+
103
+ # match request attributes
104
+ if attrs.any?
105
+ matched &&= request.include?(attrs)
106
+ end
107
+
108
+ # match block
109
+ matched &&= block.call(request) if block_given?
79
110
 
80
111
  matched
81
- end if attrs.any? || block_given?
112
+ end
82
113
 
83
114
  super(request_matcher || {}, &decoder)
84
115
  end
@@ -93,17 +124,15 @@ module WebMock
93
124
 
94
125
  response_hashes = responses.map do |response|
95
126
  ->(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)
127
+ generate_http_response(request, response)
99
128
  end
100
129
  end
101
130
 
102
131
  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)
132
+ generate_http_response(
133
+ request,
134
+ block.call(request.twirp_request),
135
+ )
107
136
  end if block_given?
108
137
 
109
138
  super(*response_hashes, &decoder)
@@ -114,29 +143,18 @@ module WebMock
114
143
  raise NotImplementedError
115
144
  end
116
145
 
117
- private
118
-
119
- def rpc_from_request(request_signature)
120
- service_full_name, rpc_name = request_signature.uri.path.split("/").last(2)
146
+ # for internal, package use only
147
+ attr_reader :twirp_client, :rpc_name, :with_attrs
121
148
 
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
149
+ private
128
150
 
129
- unless client
130
- raise "could not determine Twirp::Client for call to: #{service_full_name}/#{rpc_name}"
131
- end
151
+ def generate_http_response(request, obj)
152
+ msg_class = request.twirp_rpc&.fetch(:output_class)
132
153
 
133
- client.class.rpcs
154
+ if msg_class.nil?
155
+ raise "could not determine Twirp::Client for request: #{request}"
134
156
  end
135
157
 
136
- rpcs[rpc_name]
137
- end
138
-
139
- def generate_http_response(msg_class, obj)
140
158
  res = case obj
141
159
  when nil
142
160
  msg_class.new
@@ -148,6 +166,8 @@ module WebMock
148
166
  end
149
167
 
150
168
  obj
169
+ when ::Twirp::ClientResp
170
+ obj.error || obj.data
151
171
  when ::Twirp::Error
152
172
  obj
153
173
  when Symbol
@@ -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.1"
3
+ VERSION = "0.4.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
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.1
4
+ version: 0.4.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-20 00:00:00.000000000 Z
11
+ date: 2022-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: webmock
@@ -133,8 +133,15 @@ files:
133
133
  - README.md
134
134
  - lib/webmock-twirp.rb
135
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
136
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
137
143
  - lib/webmock/twirp/request_stub.rb
144
+ - lib/webmock/twirp/stub_request_snippet.rb
138
145
  - lib/webmock/twirp/version.rb
139
146
  - webmock-twirp.gemspec
140
147
  homepage: https://github.com/dpep/webmock-twirp