proto_pharm 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/rspec.yml +35 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +113 -0
- data/LICENSE.txt +21 -0
- data/README.md +238 -0
- data/Rakefile +8 -0
- data/bin/console +11 -0
- data/bin/regen_examples +7 -0
- data/bin/release +16 -0
- data/bin/setup +8 -0
- data/lib/proto_pharm.rb +47 -0
- data/lib/proto_pharm/action_stub.rb +57 -0
- data/lib/proto_pharm/adapter.rb +13 -0
- data/lib/proto_pharm/api.rb +31 -0
- data/lib/proto_pharm/configuration.rb +11 -0
- data/lib/proto_pharm/errors.rb +21 -0
- data/lib/proto_pharm/grpc_stub_adapter.rb +24 -0
- data/lib/proto_pharm/grpc_stub_adapter/mock_stub.rb +74 -0
- data/lib/proto_pharm/introspection.rb +18 -0
- data/lib/proto_pharm/introspection/rpc_inspector.rb +58 -0
- data/lib/proto_pharm/introspection/service_resolver.rb +24 -0
- data/lib/proto_pharm/matchers/hash_argument_matcher.rb +43 -0
- data/lib/proto_pharm/matchers/request_including_matcher.rb +39 -0
- data/lib/proto_pharm/operation_stub.rb +30 -0
- data/lib/proto_pharm/request_pattern.rb +29 -0
- data/lib/proto_pharm/request_stub.rb +67 -0
- data/lib/proto_pharm/response.rb +36 -0
- data/lib/proto_pharm/response_sequence.rb +40 -0
- data/lib/proto_pharm/rspec.rb +28 -0
- data/lib/proto_pharm/rspec/action_stub_builder.rb +37 -0
- data/lib/proto_pharm/rspec/action_stub_proxy.rb +58 -0
- data/lib/proto_pharm/rspec/dsl.rb +15 -0
- data/lib/proto_pharm/rspec/matchers/have_received_rpc.rb +72 -0
- data/lib/proto_pharm/stub_components/failure_response.rb +30 -0
- data/lib/proto_pharm/stub_registry.rb +36 -0
- data/lib/proto_pharm/version.rb +5 -0
- data/proto_pharm.gemspec +38 -0
- data/rakelib/regen_examples.rake +11 -0
- metadata +248 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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
|
data/bin/regen_examples
ADDED
data/bin/release
ADDED
@@ -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"
|
data/bin/setup
ADDED
data/lib/proto_pharm.rb
ADDED
@@ -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,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,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
|