twirp 0.1.0 → 0.2.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.
- 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
|