twirp 0.3.0 → 0.4.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 +22 -15
- data/lib/twirp/client.rb +27 -5
- data/lib/twirp/encoding.rb +31 -17
- data/lib/twirp/service.rb +4 -6
- data/lib/twirp/service_dsl.rb +3 -3
- data/lib/twirp/version.rb +1 -1
- data/test/client_test.rb +95 -44
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8be53839103af850b802185600fb7496556d3334
|
4
|
+
data.tar.gz: '08333f8e2be8996b92d9d25368eaa03d0470ef51'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d4a61196709d7b86207d20618046c87eed889cdb1d5f15693019aad4e25f7df2aecee842b17d2e546cd84212718301e5938fddd4fd274d7e982c6d5354ac6ce1
|
7
|
+
data.tar.gz: b51466076296ddc2a4ee33edfd4557c4236408786902556c77a49e478c0e0a71d4dd603e9a788a141e98739fe15ab252e62f00a96d3bae024386909e27f68aa7
|
data/README.md
CHANGED
@@ -136,14 +136,6 @@ else
|
|
136
136
|
end
|
137
137
|
```
|
138
138
|
|
139
|
-
### Protobuf or JSON
|
140
|
-
|
141
|
-
Clients use Protobuf by default. To use JSON, set the `content_type` option:
|
142
|
-
|
143
|
-
```ruby
|
144
|
-
c = Example::HelloWorldClient.new("http://localhost:3000", content_type: "application/json")
|
145
|
-
```
|
146
|
-
|
147
139
|
### Configure Clients with Faraday
|
148
140
|
|
149
141
|
While Twirp takes care of routing, serialization and error handling, other advanced HTTP options can be configured with [Faraday](https://github.com/lostisland/faraday) middleware. For example:
|
@@ -159,12 +151,29 @@ end
|
|
159
151
|
c = Example::HelloWorldClient.new(conn)
|
160
152
|
```
|
161
153
|
|
154
|
+
### Protobuf or JSON
|
155
|
+
|
156
|
+
Clients use Protobuf by default. To use JSON, set the `content_type` option as 2nd argument:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
c = Example::HelloWorldClient.new("http://localhost:3000", content_type: "application/json")
|
160
|
+
resp = c.hello(name: "World") # serialized as JSON
|
161
|
+
```
|
162
|
+
|
163
|
+
### Add-hoc JSON requests
|
164
|
+
|
165
|
+
If you just want to make a few quick requests from the console, you can instantiate a plain client and make `.json` calls:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
c = Twirp::Client.new("http://localhost:3000", package: "example", service: "HelloWorld")
|
169
|
+
resp = c.json(:Hello, name: "World")
|
170
|
+
puts resp.data["message"]
|
171
|
+
```
|
172
|
+
|
173
|
+
|
162
174
|
## Server Hooks
|
163
175
|
|
164
|
-
In the lifecycle of a request,
|
165
|
-
RPC method. If routing fails, the `on_error` hook is called with a bad_route error.
|
166
|
-
If routing succeeds, the `before` hook is called before calling the RPC method handler,
|
167
|
-
and then either `on_success` or `on_error` depending if the response is a Twirp error or not.
|
176
|
+
In the lifecycle of a server request, Twirp starts by routing the request to a valid RPC method. If routing fails, the `on_error` hook is called with a bad_route error. If routing succeeds, the `before` hook is called before calling the RPC method handler, and then either `on_success` or `on_error` depending if the response is a Twirp error or not.
|
168
177
|
|
169
178
|
```
|
170
179
|
routing -> before -> handler -> on_success
|
@@ -174,9 +183,7 @@ routing -> before -> handler -> on_success
|
|
174
183
|
On every request, one and only one of `on_success` or `on_error` is called.
|
175
184
|
|
176
185
|
|
177
|
-
If exceptions are raised, the `exception_raised` hook is called. The exceptioni is wrapped with
|
178
|
-
an internal Twirp error, and if the `on_error` hook was not called yet, then it is called with
|
179
|
-
the wrapped exception.
|
186
|
+
If exceptions are raised, the `exception_raised` hook is called. The exceptioni is wrapped with an internal Twirp error, and if the `on_error` hook was not called yet, then it is called with the wrapped exception.
|
180
187
|
|
181
188
|
|
182
189
|
```
|
data/lib/twirp/client.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'faraday'
|
2
|
-
require 'json'
|
3
2
|
|
4
3
|
require_relative 'encoding'
|
5
4
|
require_relative 'error'
|
@@ -41,14 +40,16 @@ module Twirp
|
|
41
40
|
if !Encoding.valid_content_type?(@content_type)
|
42
41
|
raise ArgumentError.new("Invalid content_type #{@content_type.inspect}. Expected one of #{Encoding.valid_content_types.inspect}")
|
43
42
|
end
|
44
|
-
end
|
45
43
|
|
46
|
-
|
47
|
-
|
44
|
+
@service_full_name = if opts[:package] || opts[:service]
|
45
|
+
opts[:package].to_s.empty? ? opts[:service].to_s : "#{opts[:package]}.#{opts[:service]}"
|
46
|
+
else
|
47
|
+
self.class.service_full_name # defined through DSL
|
48
|
+
end
|
48
49
|
end
|
49
50
|
|
50
51
|
def rpc_path(rpc_method)
|
51
|
-
"/#{service_full_name}/#{rpc_method}"
|
52
|
+
"/#{@service_full_name}/#{rpc_method}"
|
52
53
|
end
|
53
54
|
|
54
55
|
# Make a remote procedure call to a defined rpc_method. The input can be a Proto message instance,
|
@@ -82,6 +83,27 @@ module Twirp
|
|
82
83
|
return ClientResp.new(data, nil)
|
83
84
|
end
|
84
85
|
|
86
|
+
# Convenience method to call any rpc method with dynamic json attributes.
|
87
|
+
# It is like .rpc but does not use the defined Protobuf messages to serialize/deserialize data;
|
88
|
+
# the request attrs can be anything and the response data is always a plain Hash of attributes.
|
89
|
+
# This is useful to test a service before doing any code-generation.
|
90
|
+
def json(rpc_method, attrs={})
|
91
|
+
body = Encoding.encode_json(attrs)
|
92
|
+
|
93
|
+
resp = @conn.post do |r|
|
94
|
+
r.url rpc_path(rpc_method)
|
95
|
+
r.headers['Content-Type'] = Encoding::JSON
|
96
|
+
r.body = body
|
97
|
+
end
|
98
|
+
|
99
|
+
if resp.status != 200
|
100
|
+
return ClientResp.new(nil, error_from_response(resp))
|
101
|
+
end
|
102
|
+
|
103
|
+
data = Encoding.decode_json(resp.body)
|
104
|
+
return ClientResp.new(data, nil)
|
105
|
+
end
|
106
|
+
|
85
107
|
def error_from_response(resp)
|
86
108
|
status = resp.status
|
87
109
|
|
data/lib/twirp/encoding.rb
CHANGED
@@ -1,31 +1,45 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
1
3
|
module Twirp
|
2
4
|
|
3
5
|
module Encoding
|
4
6
|
JSON = "application/json"
|
5
7
|
PROTO = "application/protobuf"
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
class << self
|
10
|
+
|
11
|
+
def decode(bytes, msg_class, content_type)
|
12
|
+
case content_type
|
13
|
+
when JSON then msg_class.decode_json(bytes)
|
14
|
+
when PROTO then msg_class.decode(bytes)
|
15
|
+
else raise ArgumentError.new("Invalid content_type")
|
16
|
+
end
|
12
17
|
end
|
13
|
-
end
|
14
18
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
19
|
+
def encode(msg_obj, msg_class, content_type)
|
20
|
+
case content_type
|
21
|
+
when JSON then msg_class.encode_json(msg_obj)
|
22
|
+
when PROTO then msg_class.encode(msg_obj)
|
23
|
+
else raise ArgumentError.new("Invalid content_type")
|
24
|
+
end
|
20
25
|
end
|
21
|
-
end
|
22
26
|
|
23
|
-
|
24
|
-
|
25
|
-
|
27
|
+
def encode_json(attrs)
|
28
|
+
::JSON.generate(attrs)
|
29
|
+
end
|
30
|
+
|
31
|
+
def decode_json(bytes)
|
32
|
+
::JSON.parse(bytes)
|
33
|
+
end
|
34
|
+
|
35
|
+
def valid_content_type?(content_type)
|
36
|
+
content_type == JSON || content_type == PROTO
|
37
|
+
end
|
38
|
+
|
39
|
+
def valid_content_types
|
40
|
+
[JSON, PROTO]
|
41
|
+
end
|
26
42
|
|
27
|
-
def self.valid_content_types
|
28
|
-
[Encoding::JSON, Encoding::PROTO]
|
29
43
|
end
|
30
44
|
end
|
31
45
|
|
data/lib/twirp/service.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'json'
|
2
|
-
|
3
1
|
require_relative 'encoding'
|
4
2
|
require_relative 'error'
|
5
3
|
require_relative 'service_dsl'
|
@@ -34,8 +32,8 @@ module Twirp
|
|
34
32
|
def exception_raised(&block) @exception_raised << block; end
|
35
33
|
|
36
34
|
# Service full_name is needed to route http requests to this service.
|
37
|
-
def full_name; self.class.service_full_name; end
|
38
|
-
def name; self.class.service_name; end
|
35
|
+
def full_name; @full_name ||= self.class.service_full_name; end
|
36
|
+
def name; @name ||= self.class.service_name; end
|
39
37
|
|
40
38
|
# Rack app handler.
|
41
39
|
def call(rack_env)
|
@@ -168,7 +166,7 @@ module Twirp
|
|
168
166
|
@on_error.each{|hook| hook.call(twerr, env) }
|
169
167
|
|
170
168
|
status = Twirp::ERROR_CODES_TO_HTTP_STATUS[twerr.code]
|
171
|
-
resp_body =
|
169
|
+
resp_body = Encoding.encode_json(twerr.to_h)
|
172
170
|
[status, error_response_headers, [resp_body]]
|
173
171
|
|
174
172
|
rescue => e
|
@@ -185,7 +183,7 @@ module Twirp
|
|
185
183
|
end
|
186
184
|
|
187
185
|
twerr = Twirp::Error.internal_with(e)
|
188
|
-
resp_body =
|
186
|
+
resp_body = Encoding.encode_json(twerr.to_h)
|
189
187
|
[500, error_response_headers, [resp_body]]
|
190
188
|
end
|
191
189
|
|
data/lib/twirp/service_dsl.rb
CHANGED
@@ -35,12 +35,12 @@ module Twirp
|
|
35
35
|
# Get configured package name as String.
|
36
36
|
# An empty value means that there's no package.
|
37
37
|
def package_name
|
38
|
-
@
|
38
|
+
@package.to_s
|
39
39
|
end
|
40
40
|
|
41
41
|
# Service name as String. Defaults to the class name.
|
42
42
|
def service_name
|
43
|
-
|
43
|
+
(@service || self.name.split("::").last).to_s
|
44
44
|
end
|
45
45
|
|
46
46
|
# Service name with package prefix, which should uniquelly identifiy the service,
|
@@ -48,7 +48,7 @@ module Twirp
|
|
48
48
|
# This can be used as a path prefix to route requests to the service, because a Twirp URL is:
|
49
49
|
# "#{base_url}/#{service_full_name}/#{method]"
|
50
50
|
def service_full_name
|
51
|
-
|
51
|
+
package_name.empty? ? service_name : "#{package_name}.#{service_name}"
|
52
52
|
end
|
53
53
|
|
54
54
|
# Get raw definitions for rpc methods.
|
data/lib/twirp/version.rb
CHANGED
data/test/client_test.rb
CHANGED
@@ -12,7 +12,7 @@ class ClientTest < Minitest::Test
|
|
12
12
|
c = EmptyClient.new("http://localhost:3000")
|
13
13
|
refute_nil c
|
14
14
|
refute_nil c.instance_variable_get(:@conn) # make sure that connection was assigned
|
15
|
-
assert_equal "EmptyClient", c.service_full_name
|
15
|
+
assert_equal "EmptyClient", c.instance_variable_get(:@service_full_name)
|
16
16
|
end
|
17
17
|
|
18
18
|
def test_new_with_invalid_url
|
@@ -27,45 +27,11 @@ class ClientTest < Minitest::Test
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
def test_rpc_success
|
31
|
-
c = FooClient.new(conn_stub("/Foo/Foo") {|req|
|
32
|
-
[200, protoheader, proto(Foo, foo: "out")]
|
33
|
-
})
|
34
|
-
resp = c.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_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.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_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
|
-
c.rpc :Foo, foo: "in"
|
57
|
-
end
|
58
|
-
end
|
59
30
|
|
60
|
-
|
61
|
-
|
62
|
-
resp = c.rpc :OtherStuff, foo: "noo"
|
63
|
-
assert_nil resp.data
|
64
|
-
refute_nil resp.error
|
65
|
-
assert_equal :bad_route, resp.error.code
|
66
|
-
end
|
31
|
+
# Call .rpc on Protobuf client
|
32
|
+
# ----------------------------
|
67
33
|
|
68
|
-
def
|
34
|
+
def test_proto_success
|
69
35
|
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
70
36
|
[200, protoheader, proto(Example::Hat, inches: 99, color: "red")]
|
71
37
|
})
|
@@ -75,7 +41,7 @@ class ClientTest < Minitest::Test
|
|
75
41
|
assert_equal "red", resp.data.color
|
76
42
|
end
|
77
43
|
|
78
|
-
def
|
44
|
+
def test_proto_serialized_request_body_attrs
|
79
45
|
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
80
46
|
size = Example::Size.decode(req.body) # body is valid protobuf
|
81
47
|
assert_equal 666, size.inches
|
@@ -87,7 +53,7 @@ class ClientTest < Minitest::Test
|
|
87
53
|
refute_nil resp.data
|
88
54
|
end
|
89
55
|
|
90
|
-
def
|
56
|
+
def test_proto_serialized_request_body
|
91
57
|
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
92
58
|
assert_equal "application/protobuf", req.request_headers['Content-Type']
|
93
59
|
|
@@ -101,7 +67,7 @@ class ClientTest < Minitest::Test
|
|
101
67
|
refute_nil resp.data
|
102
68
|
end
|
103
69
|
|
104
|
-
def
|
70
|
+
def test_proto_twirp_error
|
105
71
|
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
106
72
|
[500, {}, json(code: "internal", msg: "something went wrong")]
|
107
73
|
})
|
@@ -112,7 +78,7 @@ class ClientTest < Minitest::Test
|
|
112
78
|
assert_equal "something went wrong", resp.error.msg
|
113
79
|
end
|
114
80
|
|
115
|
-
def
|
81
|
+
def test_proto_intermediary_plain_error
|
116
82
|
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
117
83
|
[503, {}, 'plain text error from proxy']
|
118
84
|
})
|
@@ -126,7 +92,7 @@ class ClientTest < Minitest::Test
|
|
126
92
|
assert_equal "plain text error from proxy", resp.error.meta[:body]
|
127
93
|
end
|
128
94
|
|
129
|
-
def
|
95
|
+
def test_proto_redirect_error
|
130
96
|
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
131
97
|
[300, {'location' => "http://rainbow.com"}, '']
|
132
98
|
})
|
@@ -139,7 +105,7 @@ class ClientTest < Minitest::Test
|
|
139
105
|
assert_equal "Redirects not allowed on Twirp requests", resp.error.meta[:not_a_twirp_error_because]
|
140
106
|
end
|
141
107
|
|
142
|
-
def
|
108
|
+
def test_proto_missing_response_header
|
143
109
|
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
144
110
|
[200, {}, proto(Example::Hat, inches: 99, color: "red")]
|
145
111
|
})
|
@@ -174,6 +140,9 @@ class ClientTest < Minitest::Test
|
|
174
140
|
assert_equal '{"msg":"I have no code of honor"}', resp.error.meta[:body]
|
175
141
|
end
|
176
142
|
|
143
|
+
# Call .rpc on JSON client
|
144
|
+
# ------------------------
|
145
|
+
|
177
146
|
def test_json_success
|
178
147
|
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
179
148
|
[200, jsonheader, '{"inches": 99, "color": "red"}']
|
@@ -233,6 +202,88 @@ class ClientTest < Minitest::Test
|
|
233
202
|
end
|
234
203
|
|
235
204
|
|
205
|
+
# Directly call .rpc
|
206
|
+
# ------------------
|
207
|
+
|
208
|
+
def test_rpc_success
|
209
|
+
c = FooClient.new(conn_stub("/Foo/Foo") {|req|
|
210
|
+
[200, protoheader, proto(Foo, foo: "out")]
|
211
|
+
})
|
212
|
+
resp = c.rpc :Foo, foo: "in"
|
213
|
+
assert_nil resp.error
|
214
|
+
refute_nil resp.data
|
215
|
+
assert_equal "out", resp.data.foo
|
216
|
+
end
|
217
|
+
|
218
|
+
def test_rpc_error
|
219
|
+
c = FooClient.new(conn_stub("/Foo/Foo") {|req|
|
220
|
+
[400, {}, json(code: "invalid_argument", msg: "dont like empty")]
|
221
|
+
})
|
222
|
+
resp = c.rpc :Foo, foo: ""
|
223
|
+
assert_nil resp.data
|
224
|
+
refute_nil resp.error
|
225
|
+
assert_equal :invalid_argument, resp.error.code
|
226
|
+
assert_equal "dont like empty", resp.error.msg
|
227
|
+
end
|
228
|
+
|
229
|
+
def test_rpc_serialization_exception
|
230
|
+
c = FooClient.new(conn_stub("/Foo/Foo") {|req|
|
231
|
+
[200, protoheader, "badstuff"]
|
232
|
+
})
|
233
|
+
assert_raises Google::Protobuf::ParseError do
|
234
|
+
c.rpc :Foo, foo: "in"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def test_rpc_invalid_method
|
239
|
+
c = FooClient.new("http://localhost")
|
240
|
+
resp = c.rpc :OtherStuff, foo: "noo"
|
241
|
+
assert_nil resp.data
|
242
|
+
refute_nil resp.error
|
243
|
+
assert_equal :bad_route, resp.error.code
|
244
|
+
end
|
245
|
+
|
246
|
+
# Call .json
|
247
|
+
# ----------
|
248
|
+
|
249
|
+
def test_direct_json_success
|
250
|
+
c = Twirp::Client.new(conn_stub("/my.pkg.Talking/Blah") {|req|
|
251
|
+
assert_equal "application/json", req.request_headers['Content-Type']
|
252
|
+
assert_equal '{"blah1":1,"blah2":2}', req.body # body is json
|
253
|
+
|
254
|
+
[200, {}, json(blah_resp: 3)]
|
255
|
+
}, package: "my.pkg", service: "Talking")
|
256
|
+
|
257
|
+
resp = c.json :Blah, blah1: 1, blah2: 2
|
258
|
+
assert_nil resp.error
|
259
|
+
refute_nil resp.data
|
260
|
+
assert_equal 3, resp.data["blah_resp"]
|
261
|
+
end
|
262
|
+
|
263
|
+
def test_direct_json_error
|
264
|
+
c = Twirp::Client.new(conn_stub("/Foo/Foomo") {|req|
|
265
|
+
[400, {}, json(code: "invalid_argument", msg: "dont like empty")]
|
266
|
+
}, service: "Foo")
|
267
|
+
|
268
|
+
resp = c.json :Foomo, foo: ""
|
269
|
+
assert_nil resp.data
|
270
|
+
refute_nil resp.error
|
271
|
+
assert_equal :invalid_argument, resp.error.code
|
272
|
+
assert_equal "dont like empty", resp.error.msg
|
273
|
+
end
|
274
|
+
|
275
|
+
def test_direct_json_bad_route
|
276
|
+
c = Twirp::Client.new(conn_stub("/Foo/OtherMethod") {|req|
|
277
|
+
[404, {}, 'not here buddy']
|
278
|
+
}, service: "Foo")
|
279
|
+
|
280
|
+
resp = c.json :OtherMethod, foo: ""
|
281
|
+
assert_nil resp.data
|
282
|
+
refute_nil resp.error
|
283
|
+
assert_equal :bad_route, resp.error.code
|
284
|
+
end
|
285
|
+
|
286
|
+
|
236
287
|
# Test Helpers
|
237
288
|
# ------------
|
238
289
|
|