twirp 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +141 -62
- data/lib/twirp.rb +1 -0
- data/lib/twirp/client.rb +147 -0
- data/lib/twirp/error.rb +12 -8
- data/lib/twirp/service.rb +42 -76
- data/lib/twirp/service_dsl.rb +61 -0
- data/lib/twirp/version.rb +1 -1
- data/test/client_test.rb +203 -0
- data/test/fake_services.rb +21 -1
- data/test/service_test.rb +27 -28
- data/twirp.gemspec +6 -6
- metadata +30 -15
- data/.gitignore +0 -1
- data/Gemfile.lock +0 -20
- data/example/Gemfile +0 -7
- data/example/Gemfile.lock +0 -17
- data/example/gen/haberdasher_pb.rb +0 -18
- data/example/gen/haberdasher_twirp.rb +0 -10
- data/example/haberdasher.proto +0 -14
- data/example/main.rb +0 -14
- data/protoc-gen-twirp_ruby/main.go +0 -259
data/lib/twirp/error.rb
CHANGED
@@ -26,15 +26,15 @@ module Twirp
|
|
26
26
|
# List of all valid error codes in Twirp
|
27
27
|
ERROR_CODES = ERROR_CODES_TO_HTTP_STATUS.keys
|
28
28
|
|
29
|
-
def valid_error_code?(code)
|
30
|
-
ERROR_CODES_TO_HTTP_STATUS.key? code # one of the valid symbols
|
31
|
-
end
|
32
|
-
|
33
29
|
|
34
30
|
# Twirp::Error represents an error response from a Twirp service.
|
35
31
|
# Twirp::Error is not an Exception to be raised, but a value to be returned
|
36
32
|
# by service handlers and received by clients.
|
37
|
-
|
33
|
+
class Error
|
34
|
+
|
35
|
+
def self.valid_code?(code)
|
36
|
+
ERROR_CODES_TO_HTTP_STATUS.key? code # one of the valid symbols
|
37
|
+
end
|
38
38
|
|
39
39
|
# Use this constructors to ensure the errors have valid error codes. Example:
|
40
40
|
# Twirp::Error.internal("boom")
|
@@ -54,7 +54,7 @@ module Twirp
|
|
54
54
|
end
|
55
55
|
|
56
56
|
attr_reader :code, :msg, :meta
|
57
|
-
|
57
|
+
|
58
58
|
attr_accessor :cause # used when wrapping another error, but this is not serialized
|
59
59
|
|
60
60
|
# Initialize a Twirp::Error
|
@@ -77,7 +77,11 @@ module Twirp
|
|
77
77
|
end
|
78
78
|
|
79
79
|
def to_s
|
80
|
-
"<Twirp::Error code:#{code
|
80
|
+
"<Twirp::Error code:#{code} msg:#{msg.inspect} meta:#{meta.inspect}>"
|
81
|
+
end
|
82
|
+
|
83
|
+
def inspect
|
84
|
+
to_s
|
81
85
|
end
|
82
86
|
|
83
87
|
|
@@ -89,7 +93,7 @@ module Twirp
|
|
89
93
|
if !meta.is_a? Hash
|
90
94
|
raise ArgumentError.new("Twirp::Error meta must be a Hash, but it is a #{meta.class.to_s}")
|
91
95
|
end
|
92
|
-
meta.each do |key, value|
|
96
|
+
meta.each do |key, value|
|
93
97
|
if !value.is_a?(String)
|
94
98
|
raise ArgumentError.new("Twirp::Error meta values must be Strings, but key #{key.inspect} has the value <#{value.class.to_s}> #{value.inspect}")
|
95
99
|
end
|
data/lib/twirp/service.rb
CHANGED
@@ -1,68 +1,21 @@
|
|
1
1
|
require "json"
|
2
2
|
|
3
|
+
require_relative "error"
|
4
|
+
require_relative "service_dsl"
|
5
|
+
|
3
6
|
module Twirp
|
4
7
|
|
5
8
|
class Service
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
# Configure service package name.
|
10
|
-
def package(name)
|
11
|
-
@package_name = name.to_s
|
12
|
-
end
|
13
|
-
|
14
|
-
# Configure service name.
|
15
|
-
def service(name)
|
16
|
-
@service_name = name.to_s
|
17
|
-
end
|
18
|
-
|
19
|
-
# Configure service routing to handle rpc calls.
|
20
|
-
def rpc(rpc_method, input_class, output_class, opts)
|
21
|
-
raise ArgumentError.new("input_class must be a Protobuf Message class") unless input_class.is_a?(Class)
|
22
|
-
raise ArgumentError.new("output_class must be a Protobuf Message class") unless output_class.is_a?(Class)
|
23
|
-
raise ArgumentError.new("opts[:handler_method] is mandatory") unless opts && opts[:handler_method]
|
24
|
-
|
25
|
-
@base_envs ||= {}
|
26
|
-
@base_envs[rpc_method.to_s] = {
|
27
|
-
rpc_method: rpc_method.to_sym,
|
28
|
-
input_class: input_class,
|
29
|
-
output_class: output_class,
|
30
|
-
handler_method: opts[:handler_method].to_sym,
|
31
|
-
}
|
32
|
-
end
|
33
|
-
|
34
|
-
# Get configured package name as String.
|
35
|
-
# And empty value means that there's no package.
|
36
|
-
def package_name
|
37
|
-
@package_name.to_s
|
38
|
-
end
|
39
|
-
|
40
|
-
# Service name as String.
|
41
|
-
# Defaults to the current class name.
|
42
|
-
def service_name
|
43
|
-
(@service_name || self.name).to_s
|
44
|
-
end
|
45
|
-
|
46
|
-
# Base Twirp environments for each rpc method.
|
47
|
-
def base_envs
|
48
|
-
@base_envs || {}
|
49
|
-
end
|
50
|
-
|
51
|
-
# Package and servicce name, as a unique identifier for the service,
|
52
|
-
# for example "example.v3.Haberdasher" (package "example.v3", service "Haberdasher").
|
53
|
-
# This can be used as a path prefix to route requests to the service, because a Twirp URL is:
|
54
|
-
# "#{BaseURL}/#{ServiceFullName}/#{Method]"
|
55
|
-
def service_full_name
|
56
|
-
package_name.empty? ? service_name : "#{package_name}.#{service_name}"
|
57
|
-
end
|
10
|
+
# DSL to define a service with package, service and rpcs.
|
11
|
+
extend ServiceDSL
|
58
12
|
|
13
|
+
class << self
|
59
14
|
# Raise exceptions instead of handling them with exception_raised hooks.
|
60
15
|
# Useful during tests to easily debug and catch unexpected exceptions.
|
61
16
|
# Default false.
|
62
17
|
attr_accessor :raise_exceptions
|
63
|
-
|
64
|
-
end # class << self
|
65
|
-
|
18
|
+
end
|
66
19
|
|
67
20
|
def initialize(handler)
|
68
21
|
@handler = handler
|
@@ -73,20 +26,16 @@ module Twirp
|
|
73
26
|
@exception_raised = []
|
74
27
|
end
|
75
28
|
|
76
|
-
|
77
|
-
self.class.service_name
|
78
|
-
end
|
79
|
-
|
80
|
-
def full_name
|
81
|
-
self.class.service_full_name # use to route requests to this servie
|
82
|
-
end
|
83
|
-
|
84
|
-
# Setup hook blocks
|
29
|
+
# Setup hook blocks.
|
85
30
|
def before(&block) @before << block; end
|
86
31
|
def on_success(&block) @on_success << block; end
|
87
32
|
def on_error(&block) @on_error << block; end
|
88
33
|
def exception_raised(&block) @exception_raised << block; end
|
89
34
|
|
35
|
+
# Service full_name is needed to route http requests to this service.
|
36
|
+
def full_name; self.class.service_full_name; end
|
37
|
+
def name; self.class.service_name; end
|
38
|
+
|
90
39
|
# Rack app handler.
|
91
40
|
def call(rack_env)
|
92
41
|
begin
|
@@ -116,6 +65,23 @@ module Twirp
|
|
116
65
|
end
|
117
66
|
end
|
118
67
|
|
68
|
+
# Call the handler method with input attributes or protobuf object.
|
69
|
+
# Returns a proto object (response) or a Twirp::Error.
|
70
|
+
# Hooks are not called and exceptions are raised instead of being wrapped.
|
71
|
+
# This is useful for unit testing the handler. The env should include
|
72
|
+
# fake data that is used by the handler, replicating middleware and before hooks.
|
73
|
+
def call_rpc(rpc_method, input={}, env={})
|
74
|
+
base_env = self.class.rpcs[rpc_method.to_s]
|
75
|
+
return Twirp::Error.bad_route("Invalid rpc method #{rpc_method.to_s.inspect}") unless base_env
|
76
|
+
|
77
|
+
env = env.merge(base_env)
|
78
|
+
input = env[:input_class].new(input) if input.is_a? Hash
|
79
|
+
env[:input] = input
|
80
|
+
env[:content_type] ||= "application/protobuf"
|
81
|
+
env[:http_response_headers] = {}
|
82
|
+
call_handler(env)
|
83
|
+
end
|
84
|
+
|
119
85
|
|
120
86
|
private
|
121
87
|
|
@@ -140,11 +106,11 @@ module Twirp
|
|
140
106
|
end
|
141
107
|
method_name = path_parts[-1]
|
142
108
|
|
143
|
-
base_env = self.class.
|
109
|
+
base_env = self.class.rpcs[method_name]
|
144
110
|
if !base_env
|
145
111
|
return bad_route_error("Invalid rpc method #{method_name.inspect}", rack_request)
|
146
112
|
end
|
147
|
-
env.merge!(base_env) # :rpc_method, :input_class, :output_class
|
113
|
+
env.merge!(base_env) # :rpc_method, :input_class, :output_class
|
148
114
|
|
149
115
|
input = nil
|
150
116
|
begin
|
@@ -162,35 +128,35 @@ module Twirp
|
|
162
128
|
Twirp::Error.bad_route msg, twirp_invalid_route: "#{req.request_method} #{req.fullpath}"
|
163
129
|
end
|
164
130
|
|
165
|
-
def decode_input(
|
131
|
+
def decode_input(bytes, input_class, content_type)
|
166
132
|
case content_type
|
167
|
-
when "application/protobuf" then input_class.decode(
|
168
|
-
when "application/json" then input_class.decode_json(
|
133
|
+
when "application/protobuf" then input_class.decode(bytes)
|
134
|
+
when "application/json" then input_class.decode_json(bytes)
|
169
135
|
end
|
170
136
|
end
|
171
137
|
|
172
|
-
def encode_output(
|
138
|
+
def encode_output(obj, output_class, content_type)
|
173
139
|
case content_type
|
174
|
-
when "application/protobuf" then output_class.encode(
|
175
|
-
when "application/json" then output_class.encode_json(
|
140
|
+
when "application/protobuf" then output_class.encode(obj)
|
141
|
+
when "application/json" then output_class.encode_json(obj)
|
176
142
|
end
|
177
143
|
end
|
178
144
|
|
179
145
|
# Call handler method and return a Protobuf Message or a Twirp::Error.
|
180
146
|
def call_handler(env)
|
181
|
-
|
182
|
-
if !@handler.respond_to?(
|
183
|
-
return Twirp::Error.unimplemented("Handler method #{
|
147
|
+
m = env[:ruby_method]
|
148
|
+
if !@handler.respond_to?(m)
|
149
|
+
return Twirp::Error.unimplemented("Handler method #{m} is not implemented.")
|
184
150
|
end
|
185
151
|
|
186
|
-
out = @handler.send(
|
152
|
+
out = @handler.send(m, env[:input], env)
|
187
153
|
case out
|
188
154
|
when env[:output_class], Twirp::Error
|
189
155
|
out
|
190
156
|
when Hash
|
191
157
|
env[:output_class].new(out)
|
192
158
|
else
|
193
|
-
Twirp::Error.internal("Handler method #{
|
159
|
+
Twirp::Error.internal("Handler method #{m} expected to return one of #{env[:output_class].name}, Hash or Twirp::Error, but returned #{out.class.name}.")
|
194
160
|
end
|
195
161
|
end
|
196
162
|
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Twirp
|
2
|
+
|
3
|
+
module ServiceDSL
|
4
|
+
|
5
|
+
# Configure service package name.
|
6
|
+
def package(name)
|
7
|
+
@package = name
|
8
|
+
end
|
9
|
+
|
10
|
+
# Configure service name.
|
11
|
+
def service(name)
|
12
|
+
@service = name
|
13
|
+
end
|
14
|
+
|
15
|
+
# Configure service rpc methods.
|
16
|
+
def rpc(rpc_method, input_class, output_class, opts)
|
17
|
+
raise ArgumentError.new("rpc_method can not be empty") if rpc_method.to_s.empty?
|
18
|
+
raise ArgumentError.new("input_class must be a Protobuf Message class") unless input_class.is_a?(Class)
|
19
|
+
raise ArgumentError.new("output_class must be a Protobuf Message class") unless output_class.is_a?(Class)
|
20
|
+
raise ArgumentError.new("opts[:ruby_method] is mandatory") unless opts && opts[:ruby_method]
|
21
|
+
|
22
|
+
rpcdef = {
|
23
|
+
rpc_method: rpc_method.to_sym, # as defined in the Proto file.
|
24
|
+
input_class: input_class, # google/protobuf Message class to serialize the input (proto request).
|
25
|
+
output_class: output_class, # google/protobuf Message class to serialize the output (proto response).
|
26
|
+
ruby_method: opts[:ruby_method].to_sym, # method on the handler or client to handle this rpc requests.
|
27
|
+
}
|
28
|
+
|
29
|
+
@rpcs ||= {}
|
30
|
+
@rpcs[rpc_method.to_s] = rpcdef
|
31
|
+
|
32
|
+
rpc_define_method(rpcdef) if respond_to? :rpc_define_method # hook for the client to implement the methods on the class
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get configured package name as String.
|
36
|
+
# An empty value means that there's no package.
|
37
|
+
def package_name
|
38
|
+
@package_name ||= @package.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
# Service name as String. Defaults to the class name.
|
42
|
+
def service_name
|
43
|
+
@service_name ||= (@service || self.name.split("::").last).to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
# Service name with package prefix, which should uniquelly identifiy the service,
|
47
|
+
# for example "example.v3.Haberdasher" for package "example.v3" and service "Haberdasher".
|
48
|
+
# This can be used as a path prefix to route requests to the service, because a Twirp URL is:
|
49
|
+
# "#{base_url}/#{service_full_name}/#{method]"
|
50
|
+
def service_full_name
|
51
|
+
@service_full_name ||= package_name.empty? ? service_name : "#{package_name}.#{service_name}"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get raw definitions for rpc methods.
|
55
|
+
# This values are used as base env for handler methods.
|
56
|
+
def rpcs
|
57
|
+
@rpcs || {}
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
data/lib/twirp/version.rb
CHANGED
data/test/client_test.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'rack/mock'
|
3
|
+
require 'google/protobuf'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require_relative '../lib/twirp'
|
7
|
+
require_relative './fake_services'
|
8
|
+
|
9
|
+
class ClientTest < Minitest::Test
|
10
|
+
|
11
|
+
def test_new_empty_client
|
12
|
+
c = EmptyClient.new("http://localhost:3000")
|
13
|
+
refute_nil c
|
14
|
+
refute_nil c.instance_variable_get(:@conn) # make sure that connection was assigned
|
15
|
+
assert_equal "EmptyClient", c.service_full_name
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_new_with_invalid_url
|
19
|
+
assert_raises URI::InvalidURIError do
|
20
|
+
EmptyClient.new("lulz")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_new_with_invalid_faraday_connection
|
25
|
+
assert_raises ArgumentError do
|
26
|
+
EmptyClient.new(something: "else")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_call_rpc_success
|
31
|
+
c = FooClient.new(conn_stub("/Foo/Foo") {|req|
|
32
|
+
[200, protoheader, proto(Foo, foo: "out")]
|
33
|
+
})
|
34
|
+
resp = c.call_rpc(:Foo, foo: "in")
|
35
|
+
assert_nil resp.error
|
36
|
+
refute_nil resp.data
|
37
|
+
assert_equal "out", resp.data.foo
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_call_rpc_error
|
41
|
+
c = FooClient.new(conn_stub("/Foo/Foo") {|req|
|
42
|
+
[400, {}, json(code: "invalid_argument", msg: "dont like empty")]
|
43
|
+
})
|
44
|
+
resp = c.call_rpc(:Foo, foo: "")
|
45
|
+
assert_nil resp.data
|
46
|
+
refute_nil resp.error
|
47
|
+
assert_equal :invalid_argument, resp.error.code
|
48
|
+
assert_equal "dont like empty", resp.error.msg
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_call_rpc_serialization_exception
|
52
|
+
c = FooClient.new(conn_stub("/Foo/Foo") {|req|
|
53
|
+
[200, protoheader, "badstuff"]
|
54
|
+
})
|
55
|
+
assert_raises Google::Protobuf::ParseError do
|
56
|
+
resp = c.call_rpc(:Foo, foo: "in")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_call_rpc_invalid_method
|
61
|
+
c = FooClient.new("http://localhost")
|
62
|
+
resp = c.call_rpc(:OtherStuff, foo: "noo")
|
63
|
+
assert_nil resp.data
|
64
|
+
refute_nil resp.error
|
65
|
+
assert_equal :bad_route, resp.error.code
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_success
|
69
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
70
|
+
[200, protoheader, proto(Example::Hat, inches: 99, color: "red")]
|
71
|
+
})
|
72
|
+
resp = c.make_hat({})
|
73
|
+
assert_nil resp.error
|
74
|
+
assert_equal 99, resp.data.inches
|
75
|
+
assert_equal "red", resp.data.color
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_serialized_request_body_attrs
|
79
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
80
|
+
size = Example::Size.decode(req.body) # body is valid protobuf
|
81
|
+
assert_equal 666, size.inches
|
82
|
+
|
83
|
+
[200, protoheader, proto(Example::Hat)]
|
84
|
+
})
|
85
|
+
resp = c.make_hat(inches: 666)
|
86
|
+
assert_nil resp.error
|
87
|
+
refute_nil resp.data
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_serialized_request_body_proto
|
91
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
92
|
+
assert_equal "application/protobuf", req.request_headers['Content-Type']
|
93
|
+
|
94
|
+
size = Example::Size.decode(req.body) # body is valid protobuf
|
95
|
+
assert_equal 666, size.inches
|
96
|
+
|
97
|
+
[200, protoheader, proto(Example::Hat)]
|
98
|
+
})
|
99
|
+
resp = c.make_hat(Example::Size.new(inches: 666))
|
100
|
+
assert_nil resp.error
|
101
|
+
refute_nil resp.data
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_twirp_error
|
105
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
106
|
+
[500, {}, json(code: "internal", msg: "something went wrong")]
|
107
|
+
})
|
108
|
+
resp = c.make_hat(inches: 1)
|
109
|
+
assert_nil resp.data
|
110
|
+
refute_nil resp.error
|
111
|
+
assert_equal :internal, resp.error.code
|
112
|
+
assert_equal "something went wrong", resp.error.msg
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_intermediary_plain_error
|
116
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
117
|
+
[503, {}, 'plain text error from proxy']
|
118
|
+
})
|
119
|
+
resp = c.make_hat(inches: 1)
|
120
|
+
assert_nil resp.data
|
121
|
+
refute_nil resp.error
|
122
|
+
assert_equal :unavailable, resp.error.code # 503 maps to :unavailable
|
123
|
+
assert_equal "unavailable", resp.error.msg
|
124
|
+
assert_equal "true", resp.error.meta[:http_error_from_intermediary]
|
125
|
+
assert_equal "Response is not JSON", resp.error.meta[:not_a_twirp_error_because]
|
126
|
+
assert_equal "plain text error from proxy", resp.error.meta[:body]
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_redirect_error
|
130
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
131
|
+
[300, {'location' => "http://rainbow.com"}, '']
|
132
|
+
})
|
133
|
+
resp = c.make_hat(inches: 1)
|
134
|
+
assert_nil resp.data
|
135
|
+
refute_nil resp.error
|
136
|
+
assert_equal :internal, resp.error.code
|
137
|
+
assert_equal "Unexpected HTTP Redirect from location=http://rainbow.com", resp.error.msg
|
138
|
+
assert_equal "true", resp.error.meta[:http_error_from_intermediary]
|
139
|
+
assert_equal "Redirects not allowed on Twirp requests", resp.error.meta[:not_a_twirp_error_because]
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_missing_proto_response_header
|
143
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
144
|
+
[200, {}, proto(Example::Hat, inches: 99, color: "red")]
|
145
|
+
})
|
146
|
+
resp = c.make_hat({})
|
147
|
+
refute_nil resp.error
|
148
|
+
assert_equal :internal, resp.error.code
|
149
|
+
assert_equal 'Expected response Content-Type "application/protobuf" but found nil', resp.error.msg
|
150
|
+
end
|
151
|
+
|
152
|
+
def test_error_with_invalid_code
|
153
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
154
|
+
[500, {}, json(code: "unicorn", msg: "the unicorn is here")]
|
155
|
+
})
|
156
|
+
resp = c.make_hat({})
|
157
|
+
assert_nil resp.data
|
158
|
+
refute_nil resp.error
|
159
|
+
assert_equal :internal, resp.error.code
|
160
|
+
assert_equal "Invalid Twirp error code: unicorn", resp.error.msg
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_error_with_no_code
|
164
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
165
|
+
[500, {}, json(msg: "I have no code of honor")]
|
166
|
+
})
|
167
|
+
resp = c.make_hat({})
|
168
|
+
assert_nil resp.data
|
169
|
+
refute_nil resp.error
|
170
|
+
assert_equal :unknown, resp.error.code # 500 maps to :unknown
|
171
|
+
assert_equal "unknown", resp.error.msg
|
172
|
+
assert_equal "true", resp.error.meta[:http_error_from_intermediary]
|
173
|
+
assert_equal 'Response is JSON but it has no "code" attribute', resp.error.meta[:not_a_twirp_error_because]
|
174
|
+
assert_equal '{"msg":"I have no code of honor"}', resp.error.meta[:body]
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
# Test Helpers
|
179
|
+
# ------------
|
180
|
+
|
181
|
+
def protoheader
|
182
|
+
{'Content-Type' => 'application/protobuf'}
|
183
|
+
end
|
184
|
+
|
185
|
+
def proto(clss, attrs={})
|
186
|
+
clss.encode(clss.new(attrs))
|
187
|
+
end
|
188
|
+
|
189
|
+
def json(attrs)
|
190
|
+
JSON.generate(attrs)
|
191
|
+
end
|
192
|
+
|
193
|
+
def conn_stub(path)
|
194
|
+
Faraday.new do |conn|
|
195
|
+
conn.adapter :test do |stub|
|
196
|
+
stub.post(path) do |env|
|
197
|
+
yield(env)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|