proto_pharm 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rspec.yml +35 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +3 -0
  6. data/.travis.yml +7 -0
  7. data/CHANGELOG.md +12 -0
  8. data/Gemfile +8 -0
  9. data/Gemfile.lock +113 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +238 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +11 -0
  14. data/bin/regen_examples +7 -0
  15. data/bin/release +16 -0
  16. data/bin/setup +8 -0
  17. data/lib/proto_pharm.rb +47 -0
  18. data/lib/proto_pharm/action_stub.rb +57 -0
  19. data/lib/proto_pharm/adapter.rb +13 -0
  20. data/lib/proto_pharm/api.rb +31 -0
  21. data/lib/proto_pharm/configuration.rb +11 -0
  22. data/lib/proto_pharm/errors.rb +21 -0
  23. data/lib/proto_pharm/grpc_stub_adapter.rb +24 -0
  24. data/lib/proto_pharm/grpc_stub_adapter/mock_stub.rb +74 -0
  25. data/lib/proto_pharm/introspection.rb +18 -0
  26. data/lib/proto_pharm/introspection/rpc_inspector.rb +58 -0
  27. data/lib/proto_pharm/introspection/service_resolver.rb +24 -0
  28. data/lib/proto_pharm/matchers/hash_argument_matcher.rb +43 -0
  29. data/lib/proto_pharm/matchers/request_including_matcher.rb +39 -0
  30. data/lib/proto_pharm/operation_stub.rb +30 -0
  31. data/lib/proto_pharm/request_pattern.rb +29 -0
  32. data/lib/proto_pharm/request_stub.rb +67 -0
  33. data/lib/proto_pharm/response.rb +36 -0
  34. data/lib/proto_pharm/response_sequence.rb +40 -0
  35. data/lib/proto_pharm/rspec.rb +28 -0
  36. data/lib/proto_pharm/rspec/action_stub_builder.rb +37 -0
  37. data/lib/proto_pharm/rspec/action_stub_proxy.rb +58 -0
  38. data/lib/proto_pharm/rspec/dsl.rb +15 -0
  39. data/lib/proto_pharm/rspec/matchers/have_received_rpc.rb +72 -0
  40. data/lib/proto_pharm/stub_components/failure_response.rb +30 -0
  41. data/lib/proto_pharm/stub_registry.rb +36 -0
  42. data/lib/proto_pharm/version.rb +5 -0
  43. data/proto_pharm.gemspec +38 -0
  44. data/rakelib/regen_examples.rake +11 -0
  45. metadata +248 -0
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ module Matchers
5
+ # Base class for Hash matchers
6
+ # https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
7
+ class HashArgumentMatcher
8
+ def self.stringify_keys!(arg, options = {})
9
+ case arg
10
+ when Array
11
+ arg.map do |elem|
12
+ options[:deep] ? stringify_keys!(elem, options) : elem
13
+ end
14
+ when Hash
15
+ Hash[
16
+ *arg.map do |key, value|
17
+ k = key.is_a?(Symbol) ? key.to_s : key
18
+ v = (options[:deep] ? stringify_keys!(value, options) : value)
19
+ [k, v]
20
+ end.inject([]) { |r, x| r + x }]
21
+ else
22
+ arg
23
+ end
24
+ end
25
+
26
+ def initialize(expected)
27
+ @expected = Hash[
28
+ ProtoPharm::Matchers::HashArgumentMatcher.stringify_keys!(expected, deep: true).sort,
29
+ ]
30
+ end
31
+
32
+ def ==(_actual, &block)
33
+ @expected.all?(&block)
34
+ rescue NoMethodError
35
+ false
36
+ end
37
+
38
+ def self.from_rspec_matcher(matcher)
39
+ new(matcher.instance_variable_get(:@expected))
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "proto_pharm/matchers/hash_argument_matcher"
4
+
5
+ module ProtoPharm
6
+ module Matchers
7
+ class RequestIncludingMatcher < HashArgumentMatcher
8
+ def ==(actual)
9
+ if actual.respond_to?(:to_h)
10
+ actual = actual.to_h
11
+ end
12
+
13
+ actual = Hash[ProtoPharm::Matchers::HashArgumentMatcher.stringify_keys!(actual, deep: true)]
14
+ super { |key, value| inner_including(value, key, actual) }
15
+ rescue NoMethodError
16
+ false
17
+ end
18
+
19
+ private
20
+
21
+ def inner_including(expect, key, actual)
22
+ if actual.key?(key)
23
+ actual_value = actual[key]
24
+ if expect.is_a?(Hash)
25
+ RequestIncludingMatcher.new(expect) == actual_value
26
+ else
27
+ expect === actual_value
28
+ end
29
+ else
30
+ false
31
+ end
32
+ end
33
+
34
+ def inspect
35
+ "reqeust_including(#{@expected.inspect})"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ class OperationStub
5
+ attr_reader :response_proc, :metadata, :trailing_metadata, :deadline
6
+
7
+ # @param metadata [Hash] Any metadata passed into the GRPC request
8
+ # @param deadline [Time] The deadline set on the GRPC request
9
+ # @yieldreturn [*] The stubbed value or error expected to be returned from the request
10
+ def initialize(metadata: nil, deadline: nil, &response_proc)
11
+ @response_proc = response_proc
12
+ @metadata = metadata
13
+ @deadline = deadline
14
+
15
+ # TODO: support stubbing
16
+ @trailing_metadata = {}
17
+ end
18
+
19
+ # Calls the block given upon instantiation and returns the result
20
+ def response
21
+ response_proc.call
22
+ end
23
+ alias_method :execute, :response
24
+
25
+ # TODO: support stubbing
26
+ def cancelled?
27
+ false
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ class RequestPattern
5
+ attr_reader :path, :request, :block
6
+
7
+ # @param path [String]
8
+ def initialize(path)
9
+ @path = path
10
+ @block = nil
11
+ @request = nil
12
+ end
13
+
14
+ def with(request = nil, &block)
15
+ if request.nil? && !block_given?
16
+ raise ArgumentError, "#with method invoked with no arguments. Either options request or block must be specified."
17
+ end
18
+
19
+ @request = request
20
+ @block = block
21
+ end
22
+
23
+ def match?(match_path, match_request)
24
+ path == match_path &&
25
+ (request.nil? || request == match_request) &&
26
+ (block.nil? || block.call(match_path))
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "proto_pharm/request_pattern"
4
+ require "proto_pharm/response"
5
+ require "proto_pharm/response_sequence"
6
+ require "proto_pharm/errors"
7
+
8
+ module ProtoPharm
9
+ class RequestStub
10
+ attr_reader :received_requests, :request_pattern, :response_sequence
11
+
12
+ delegate :path, to: :request_pattern, allow_nil: true
13
+
14
+ # @param path [String] gRPC path like /${service_name}/${method_name}
15
+ def initialize(path)
16
+ @request_pattern = RequestPattern.new(path)
17
+ @response_sequence = []
18
+ @received_requests = []
19
+ end
20
+
21
+ def with(request = nil, &block)
22
+ @request_pattern.with(request, &block)
23
+ self
24
+ end
25
+
26
+ def to_return(*values)
27
+ responses = [*values].flatten.map { |v| Response::Value.new(v) }
28
+ @response_sequence << ProtoPharm::ResponsesSequence.new(responses)
29
+ self
30
+ end
31
+
32
+ def to_raise(*exceptions)
33
+ responses = [*exceptions].flatten.map { |e| Response::ExceptionValue.new(e) }
34
+ @response_sequence << ProtoPharm::ResponsesSequence.new(responses)
35
+ self
36
+ end
37
+
38
+ def response
39
+ if @response_sequence.empty?
40
+ raise ProtoPharm::NoResponseError, "Must be set some values by using ProtoPharm::RequestStub#to_run"
41
+ elsif @response_sequence.size == 1
42
+ @response_sequence.first.next
43
+ else
44
+ if @response_sequence.first.end?
45
+ @response_sequence.shift
46
+ end
47
+
48
+ @response_sequence.first.next
49
+ end
50
+ end
51
+
52
+ def received!(request)
53
+ @received_requests << request
54
+ end
55
+
56
+ def received_count
57
+ received_requests.size
58
+ end
59
+
60
+ # @param path [String]
61
+ # @param request [Object]
62
+ # @return [Bool]
63
+ def match?(path, request)
64
+ @request_pattern.match?(path, request)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ module Response
5
+ class ExceptionValue
6
+ attr_reader :exception
7
+
8
+ def initialize(exception)
9
+ @exception = case exception
10
+ when String
11
+ StandardError.new(exception)
12
+ when Class
13
+ exception.new("Exception from ProtoPharm")
14
+ when Exception
15
+ exception
16
+ else
17
+ raise ArgumentError.new(message: "Invalid exception class: #{exception.class}")
18
+ end
19
+ end
20
+
21
+ def evaluate
22
+ raise @exception.dup
23
+ end
24
+ end
25
+
26
+ class Value
27
+ def initialize(value)
28
+ @value = value
29
+ end
30
+
31
+ def evaluate
32
+ @value.dup
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ class ResponsesSequence
5
+ attr_accessor :repeat
6
+ attr_reader :responses
7
+
8
+ def initialize(responses)
9
+ @repeat = 1
10
+ @responses = responses
11
+ @current = 0
12
+ @last = @responses.length - 1
13
+ end
14
+
15
+ def end?
16
+ @repeat == 0
17
+ end
18
+
19
+ def next
20
+ if @repeat > 0
21
+ response = @responses[@current]
22
+ next_pos
23
+ response
24
+ else
25
+ @responses.last
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def next_pos
32
+ if @last == @current
33
+ @current = 0
34
+ @repeat -= 1
35
+ else
36
+ @current += 1
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec"
4
+
5
+ require_relative "../proto_pharm"
6
+
7
+ require_relative "rspec/action_stub_proxy"
8
+ require_relative "rspec/action_stub_builder"
9
+
10
+ require_relative "rspec/dsl"
11
+ require_relative "rspec/matchers/have_received_rpc"
12
+
13
+ RSpec.configure do |config|
14
+ config.before(:suite) do
15
+ ProtoPharm.enable!
16
+ end
17
+
18
+ config.after(:suite) do
19
+ ProtoPharm.disable!
20
+ end
21
+
22
+ config.after(:each) do
23
+ ProtoPharm.reset!
24
+ end
25
+
26
+ config.include ProtoPharm::RSpec::DSL
27
+ config.include ProtoPharm::RSpec::Matchers
28
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ module RSpec
5
+ class ActionStubBuilder
6
+ attr_accessor :grpc_service, :action_stub_proxy
7
+
8
+ delegate :rpc_action, :expectations, to: :action_stub_proxy
9
+
10
+ def initialize(grpc_service)
11
+ @grpc_service = grpc_service
12
+ end
13
+
14
+ def to(action_stub_proxy)
15
+ @action_stub_proxy = action_stub_proxy
16
+
17
+ ProtoPharm.stub_registry.register_request_stub(action_stub)
18
+ end
19
+
20
+ private
21
+
22
+ def action_stub
23
+ @action_stub ||= ActionStub.new(grpc_service, rpc_action).tap do |stub|
24
+ expectations.each do |ex|
25
+ if ex.kwargs.blank?
26
+ stub.public_send(ex.method, *ex.args)
27
+ elsif ex.args.blank?
28
+ stub.public_send(ex.method, **ex.kwargs)
29
+ else
30
+ stub.public_send(ex.method, *ex.args, **ex.kwargs)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ module RSpec
5
+ class ActionStubProxy
6
+ attr_reader :rpc_action, :expectations
7
+
8
+ def initialize(rpc_action)
9
+ @rpc_action = rpc_action
10
+ @expectations = []
11
+ end
12
+
13
+ # Proxies ActionStub#with
14
+ def with(*args, **kwargs)
15
+ expectations << Expectation.new(:with, args, kwargs)
16
+
17
+ self
18
+ end
19
+
20
+ # Proxies ActionStub#to_return
21
+ def and_return(*args, **kwargs)
22
+ expectations << Expectation.new(:to_return, args, kwargs)
23
+
24
+ self
25
+ end
26
+
27
+ # Proxies ActionStub#to_raise
28
+ def and_raise(*args, **kwargs)
29
+ expectations << Expectation.new(:to_raise, args, kwargs)
30
+
31
+ self
32
+ end
33
+
34
+ # Proxies ActionStub#to_fail_with
35
+ def and_fail_with(*args, **kwargs)
36
+ expectations << Expectation.new(:to_fail_with, args, kwargs)
37
+
38
+ self
39
+ end
40
+
41
+ def and_fail
42
+ expectations << Expectation.new(:to_fail, [], {})
43
+
44
+ self
45
+ end
46
+
47
+ class Expectation
48
+ attr_reader :method, :args, :kwargs
49
+
50
+ def initialize(method, args, kwargs)
51
+ @method = method
52
+ @args = args
53
+ @kwargs = kwargs
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ module RSpec
5
+ module DSL
6
+ def allow_grpc_service(service)
7
+ ActionStubBuilder.new(service)
8
+ end
9
+
10
+ def receive_rpc(rpc_action)
11
+ ActionStubProxy.new(rpc_action)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ module RSpec
5
+ module Matchers
6
+ extend ::RSpec::Matchers::DSL
7
+
8
+ matcher :have_received_rpc do |endpoint_reference| # rubocop:disable Metrics/BlockLength
9
+ include Introspection
10
+
11
+ attr_reader :service_reference, :endpoint_reference, :expected_proto, :expected_kwargs
12
+
13
+ description { "receive rpc #{endpoint_reference.inspect}#{with_parameters_description}" }
14
+
15
+ match do |service_reference|
16
+ @service_reference = service_reference
17
+ @endpoint_reference = endpoint_reference
18
+
19
+ raise RpcNotStubbedError, "RPC '#{grpc_path}' has not been stubbed. Stub it with stub_grpc_action before asserting." unless matching_request_stubs.any?
20
+
21
+ if expected_request_proto.blank?
22
+ expect(received_request_stubs.size).to be > 0
23
+ else
24
+ expect(received_request_stubs.flat_map(&:received_requests)).to include expected_request_proto
25
+ end
26
+ end
27
+
28
+ chain :with do |expected_proto = nil, **expected_kwargs|
29
+ raise ArgumentError, "assert only with proto or keyword arguments, not both" if expected_proto.present? && expected_kwargs.present?
30
+ raise ArgumentError, "cannot assert expected arguments multiple times in the same expectation" if (@expected_proto || @expected_kwargs).present?
31
+
32
+ @expected_proto = expected_proto
33
+ @expected_kwargs = expected_kwargs
34
+ end
35
+
36
+ private
37
+
38
+ delegate :grpc_path, :input_type, to: :endpoint
39
+
40
+ def endpoint
41
+ @endpoint ||= inspect_rpc(service_reference, endpoint_reference)
42
+ end
43
+
44
+ def received_request_stubs
45
+ matching_request_stubs.select { |stub| stub.received_count > 0 }
46
+ end
47
+
48
+ def matching_request_stubs
49
+ @matching_request_stubs ||= ProtoPharm.stub_registry.all_requests_matching(grpc_path, expected_request_proto)
50
+ end
51
+
52
+ def expected_request_proto
53
+ return if expected_proto.nil? && expected_kwargs.nil?
54
+
55
+ ensure_valid_proto_type!(expected_proto)
56
+
57
+ @expected_request_proto ||= expected_proto.presence || input_type.new(**expected_kwargs)
58
+ end
59
+
60
+ def ensure_valid_proto_type!(expected_proto)
61
+ raise InvalidProtoType, "Invalid proto type #{expected_proto.class} for #{grpc_path}, expected #{input_type}" if expected_proto.present? && expected_proto.class != input_type
62
+ end
63
+
64
+ def with_parameters_description
65
+ return if expected_request_proto.blank?
66
+
67
+ " with request #{expected_request_proto.inspect}"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end