proto_pharm 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 (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