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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require_relative "../lib/proto_pharm"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "pry"
11
+ Pry.start
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+
3
+ grpc_tools_ruby_protoc \
4
+ -I ./spec/examples/hello \
5
+ --ruby_out=./spec/examples/hello \
6
+ --grpc_out=./spec/examples/hello \
7
+ spec/examples/hello/hello.proto
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/proto_pharm/version"
4
+
5
+ current_branch = `git rev-parse --abbrev-ref HEAD`.strip
6
+
7
+ unless current_branch == "master" || ProtoPharm::VERSION =~ %r{\.pre\d*\z}
8
+ puts "Can only release from the master branch 😿"
9
+ exit 1
10
+ end
11
+
12
+ system "git tag v#{ProtoPharm::VERSION}"
13
+
14
+ system "git push --tag"
15
+
16
+ system "git push fury #{current_branch}:master"
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module"
4
+ require "grpc"
5
+
6
+ require_relative "proto_pharm/version"
7
+ require_relative "proto_pharm/configuration"
8
+
9
+ require_relative "proto_pharm/introspection"
10
+ require_relative "proto_pharm/stub_components/failure_response"
11
+
12
+ require_relative "proto_pharm/adapter"
13
+ require_relative "proto_pharm/grpc_stub_adapter"
14
+ require_relative "proto_pharm/grpc_stub_adapter/mock_stub"
15
+
16
+ require_relative "proto_pharm/stub_registry"
17
+ require_relative "proto_pharm/api"
18
+
19
+ module ProtoPharm
20
+ extend ProtoPharm::Api
21
+
22
+ class << self
23
+ delegate :enable!, :disable!, :enabled?, to: :adapter
24
+
25
+ def reset!
26
+ ProtoPharm.stub_registry.reset!
27
+ end
28
+
29
+ def stub_registry
30
+ @stub_registry ||= ProtoPharm::StubRegistry.new
31
+ end
32
+
33
+ def adapter
34
+ @adapter ||= Adapter.new
35
+ end
36
+
37
+ def config
38
+ @config ||= Configuration.new
39
+ end
40
+ end
41
+
42
+ # Hook into GRPC::ClientStub
43
+ # https://github.com/grpc/grpc/blob/bec3b5ada2c5e5d782dff0b7b5018df646b65cb0/src/ruby/lib/grpc/generic/service.rb#L150-L186
44
+ GRPC::ClientStub.prepend GrpcStubAdapter::MockStub
45
+ end
46
+
47
+ GrpcMock = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("GrpcMock", "ProtoPharm")
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/blank"
4
+
5
+ module ProtoPharm
6
+ class ActionStub < RequestStub
7
+ include Introspection
8
+ include StubComponents::FailureResponse
9
+
10
+ attr_reader :service, :action
11
+
12
+ # @param service [GRPC::GenericService] gRPC service class representing the the service being stubbed
13
+ # @param action [String, Symbol] name of the endpoint being stubbed
14
+ def initialize(service, action)
15
+ @service = service
16
+ @action = action
17
+
18
+ super(grpc_path)
19
+ end
20
+
21
+ # @param proto [Object] request proto object
22
+ # @param request_kwargs [Hash] parameters for request
23
+ def with(proto = nil, **request_kwargs)
24
+ super(endpoint.normalize_request_proto(proto, **request_kwargs))
25
+ end
26
+
27
+ # @param proto [Object] response proto object
28
+ # @param request_kwargs [Hash] parameters to respond with
29
+ def to_return(proto = nil, **request_kwargs)
30
+ super(endpoint.normalize_response_proto(proto, **request_kwargs))
31
+ end
32
+
33
+ def received!(request)
34
+ @received_requests << request
35
+ end
36
+
37
+ def received_count
38
+ received_requests.size
39
+ end
40
+
41
+ def match?(match_path, match_request)
42
+ # If paths don't match, don't try to cast the request object
43
+ super unless grpc_path == match_path
44
+
45
+ # If paths match, cast the given request object to the expected proto
46
+ super(match_path, endpoint.normalize_request_proto(match_request))
47
+ end
48
+
49
+ private
50
+
51
+ delegate :grpc_path, :input_type, :output_type, to: :endpoint
52
+
53
+ def endpoint
54
+ @endpoint ||= inspect_rpc(service, action)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ class Adapter
5
+ delegate :enable!, :disable!, :enabled?, to: :adapter
6
+
7
+ private
8
+
9
+ def adapter
10
+ @adapter ||= GrpcStubAdapter.new
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "proto_pharm/request_stub"
4
+ require "proto_pharm/action_stub"
5
+ require "proto_pharm/matchers/request_including_matcher"
6
+
7
+ module ProtoPharm
8
+ module Api
9
+ # @param path [String]
10
+ def stub_request(path)
11
+ ProtoPharm.stub_registry.register_request_stub(ProtoPharm::RequestStub.new(path))
12
+ end
13
+
14
+ def stub_grpc_action(service, rpc_action)
15
+ ProtoPharm.stub_registry.register_request_stub(ProtoPharm::ActionStub.new(service, rpc_action))
16
+ end
17
+
18
+ # @param values [Hash]
19
+ def request_including(values)
20
+ ProtoPharm::Matchers::RequestIncludingMatcher.new(values)
21
+ end
22
+
23
+ def disable_net_connect!
24
+ ProtoPharm.config.allow_net_connect = false
25
+ end
26
+
27
+ def allow_net_connect!
28
+ ProtoPharm.config.allow_net_connect = true
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ class Configuration
5
+ attr_accessor :allow_net_connect
6
+
7
+ def initialize
8
+ @allow_net_connect = true
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ class Error < StandardError; end
5
+
6
+ class NetConnectNotAllowedError < Error
7
+ def initialize(sigunature)
8
+ super("Real gRPC connections are disabled. #{sigunature} is requested")
9
+ end
10
+ end
11
+
12
+ class NoResponseError < Error
13
+ def initialize(msg)
14
+ super("There is no response: #{msg}")
15
+ end
16
+ end
17
+
18
+ class InvalidProtoType < Error; end
19
+ class RpcNotFoundError < Error; end
20
+ class RpcNotStubbedError < Error; end
21
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "operation_stub"
5
+
6
+ module ProtoPharm
7
+ class GrpcStubAdapter
8
+ delegate :enable!, :disable!, :enabled?, to: :class
9
+
10
+ class << self
11
+ def disable!
12
+ @enabled = false
13
+ end
14
+
15
+ def enable!
16
+ @enabled = true
17
+ end
18
+
19
+ def enabled?
20
+ @enabled
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ class GrpcStubAdapter
5
+ module MockStub
6
+ def request_response(method, request, *args, return_op: false, **opts)
7
+ return super unless ProtoPharm.enabled?
8
+
9
+ request_stub = ProtoPharm.stub_registry.find_request_matching(method, request)
10
+
11
+ if request_stub
12
+ operation = OperationStub.new(metadata: opts[:metadata]) do
13
+ request_stub.received!(request)
14
+ request_stub.response.evaluate
15
+ end
16
+
17
+ return_op ? operation : operation.execute
18
+ elsif ProtoPharm.config.allow_net_connect
19
+ super
20
+ else
21
+ raise NetConnectNotAllowedError, method
22
+ end
23
+ end
24
+
25
+ # TODO
26
+ def client_streamer(method, requests, *args)
27
+ return super unless ProtoPharm.enabled?
28
+
29
+ r = requests.to_a # FIXME: this may not work
30
+ request_stub = ProtoPharm.stub_registry.find_request_matching(method, r)
31
+
32
+ if request_stub
33
+ request_stub.received!(requests)
34
+ request_stub.response.evaluate
35
+ elsif ProtoPharm.config.allow_net_connect
36
+ super
37
+ else
38
+ raise NetConnectNotAllowedError, method
39
+ end
40
+ end
41
+
42
+ def server_streamer(method, request, *args)
43
+ return super unless ProtoPharm.enabled?
44
+
45
+ request_stub = ProtoPharm.stub_registry.find_request_matching(method, request)
46
+
47
+ if request_stub
48
+ request_stub.received!(request)
49
+ request_stub.response.evaluate
50
+ elsif ProtoPharm.config.allow_net_connect
51
+ super
52
+ else
53
+ raise NetConnectNotAllowedError, method
54
+ end
55
+ end
56
+
57
+ def bidi_streamer(method, requests, *args)
58
+ return super unless ProtoPharm.enabled?
59
+
60
+ r = requests.to_a # FIXME: this may not work
61
+ request_stub = ProtoPharm.stub_registry.find_request_matching(method, r)
62
+
63
+ if request_stub
64
+ request_stub.received!(requests)
65
+ request_stub.response.evaluate
66
+ elsif ProtoPharm.config.allow_net_connect
67
+ super
68
+ else
69
+ raise NetConnectNotAllowedError, method
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "introspection/rpc_inspector"
4
+ require_relative "introspection/service_resolver"
5
+
6
+ module ProtoPharm
7
+ module Introspection
8
+ private
9
+
10
+ def resolve_service(service)
11
+ Introspection::ServiceResolver.resolve(service)
12
+ end
13
+
14
+ def inspect_rpc(service, endpoint)
15
+ Introspection::RpcInspector.new(service, endpoint)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ module Introspection
5
+ class RpcInspector
6
+ attr_reader :grpc_service, :endpoint_name
7
+
8
+ delegate :service_name, :rpc_descs, to: :grpc_service
9
+
10
+ def initialize(service, endpoint_name)
11
+ @grpc_service = ServiceResolver.resolve(service)
12
+
13
+ @endpoint_name = endpoint_name
14
+ end
15
+
16
+ def normalize_request_proto(proto = nil, **kwargs)
17
+ cast_proto(input_type, proto, **kwargs)
18
+ end
19
+
20
+ def normalize_response_proto(proto = nil, **kwargs)
21
+ cast_proto(output_type, proto, **kwargs)
22
+ end
23
+
24
+ def normalized_rpc_name
25
+ @normalized_rpc_name ||= endpoint_name.to_s.camelize.to_sym
26
+ end
27
+
28
+ def rpc_desc
29
+ @rpc_desc ||= rpc_descs[normalized_rpc_name].tap do |endpoint|
30
+ raise RpcNotFoundError, "Service #{service_name} does not implement '#{normalized_rpc_name}'" if endpoint.blank?
31
+ end
32
+ end
33
+
34
+ def grpc_path
35
+ @grpc_path ||= "/#{service_name}/#{normalized_rpc_name}"
36
+ end
37
+
38
+ def input_type
39
+ rpc_desc.input
40
+ end
41
+
42
+ def output_type
43
+ rpc_desc.output
44
+ end
45
+
46
+ private
47
+
48
+ def cast_proto(proto_class, proto = nil, **kwargs)
49
+ return proto_class.new(**kwargs) if proto.blank?
50
+ return proto_class.new(proto) if proto.respond_to?(:to_hash)
51
+
52
+ raise InvalidProtoType, "Invalid proto type #{proto.class} for #{grpc_path}, expected #{proto_class}" unless proto.class == proto_class
53
+
54
+ proto
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoPharm
4
+ module Introspection
5
+ module ServiceResolver
6
+ class InvalidGRPCServiceError < StandardError; end
7
+
8
+ class << self
9
+ def resolve(service)
10
+ raise InvalidGRPCServiceError, "Not a valid gRPC service module: #{service.inspect}" unless service.respond_to?(:const_defined?)
11
+
12
+ service.const_defined?(:Service) ? service::Service : service
13
+ end
14
+ end
15
+
16
+ # We'll need this later
17
+ # attr_reader :service
18
+
19
+ # def initialize(service)
20
+ # @service = service
21
+ # end
22
+ end
23
+ end
24
+ end