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 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