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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1e62a59d0a5485723751b5d1f15452c79d50a338
4
- data.tar.gz: 54444353c6556ad93ac5d3eee42a844f0d9ee106
3
+ metadata.gz: 8be53839103af850b802185600fb7496556d3334
4
+ data.tar.gz: '08333f8e2be8996b92d9d25368eaa03d0470ef51'
5
5
  SHA512:
6
- metadata.gz: 0c6a92c2d93ef86c8ecffc4ce1a0581eb9b12bca892e018113ab5e4ac0d0c5bce949b1fe19fe8d14b53887f9273196832a4a3d99ed71d0bde07ccfbab5ca5c5d
7
- data.tar.gz: 1ae6b8cb525b577e4b2eeb92114100f0d7b0444d3e5e2226dea24467934d464db92e304b6ed9899cbeff6aace09b1cf4b795448c0d10dac3066567cbfffa0e8f
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, the Twirp service starts by routing the request to a valid
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
  ```
@@ -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
- def service_full_name
47
- self.class.service_full_name
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
 
@@ -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
- def self.decode(bytes, msg_class, content_type)
8
- case content_type
9
- when JSON then msg_class.decode_json(bytes)
10
- when PROTO then msg_class.decode(bytes)
11
- else raise ArgumentError.new("Invalid content_type")
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
- def self.encode(msg_obj, msg_class, content_type)
16
- case content_type
17
- when JSON then msg_class.encode_json(msg_obj)
18
- when PROTO then msg_class.encode(msg_obj)
19
- else raise ArgumentError.new("Invalid content_type")
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
- def self.valid_content_type?(content_type)
24
- content_type == Encoding::JSON || content_type == Encoding::PROTO
25
- end
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
 
@@ -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 = JSON.generate(twerr.to_h)
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 = JSON.generate(twerr.to_h)
186
+ resp_body = Encoding.encode_json(twerr.to_h)
189
187
  [500, error_response_headers, [resp_body]]
190
188
  end
191
189
 
@@ -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
- @package_name ||= @package.to_s
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
- @service_name ||= (@service || self.name.split("::").last).to_s
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
- @service_full_name ||= package_name.empty? ? service_name : "#{package_name}.#{service_name}"
51
+ package_name.empty? ? service_name : "#{package_name}.#{service_name}"
52
52
  end
53
53
 
54
54
  # Get raw definitions for rpc methods.
@@ -1,3 +1,3 @@
1
1
  module Twirp
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -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
- def test_rpc_invalid_method
61
- c = FooClient.new("http://localhost")
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 test_success
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 test_serialized_request_body_attrs
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 test_serialized_request_body_proto
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 test_twirp_error
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 test_intermediary_plain_error
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 test_redirect_error
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 test_missing_proto_response_header
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
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twirp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyrus A. Forbes