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,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