twirp 0.3.0 → 0.4.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 +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
|
|