twirp 0.4.1 → 0.5.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: a30f71eb6c946fa8356d486b1decd82733bbb3af
4
- data.tar.gz: db3c570238ac24017af314eedb327fd8f5f07bb0
3
+ metadata.gz: 31b1acc1215a5040a22d6ef3715103a8aa6840c9
4
+ data.tar.gz: 9cf27b5d6c7232bd6ccbeaf4b54211d97dc1cf45
5
5
  SHA512:
6
- metadata.gz: a59ff7c4ba349bce47e06a3a47d68eccfd411f43b63df34948593ab0d7987a596a766df398b633a8b31890cbadd09297c89d7619bd7fb1990d4538ae65c9c297
7
- data.tar.gz: 39b024e1ba715058c2ad07c7b3e5942d0d3aae47960a02402b1f497c3bd6fdb1bdc6cd19c88005a8844238c2686e6d40251aeeeca13ed0cadb50f499f2c3c216
6
+ metadata.gz: af3f533423755f9d9ea5e27194a7f7efc857f1dbc4268a3d08548315651b14564f63ea8533b061c9b3c3efc261200c74c7b108696d99905a4ea8eed3c5f47b96
7
+ data.tar.gz: daf1b600b9df7240fbd99c1289be16f98f60ca0322ce23cfe9c3490611ecf99293d06a6b800ae8221f33c09cc1e0bbb65f536cfeab5b7d5beab678aaeb0590c5
data/README.md CHANGED
@@ -55,7 +55,7 @@ protoc --proto_path=. --ruby_out=. --twirp_ruby_out=. ./example/hello_world/serv
55
55
 
56
56
  ## Twirp Service Handler
57
57
 
58
- A handler is a simple class that implements each rpc method. For example a handler for `HelloWorld`:
58
+ A handler implements the rpc methods. For example a handler for `HelloWorldService`:
59
59
 
60
60
  ```ruby
61
61
  class HelloWorldHandler
@@ -71,14 +71,16 @@ class HelloWorldHandler
71
71
  end
72
72
  ```
73
73
 
74
- The `req` argument is the request message (input), and the returned value is expected to be the response message, or a `Twirp::Error`.
74
+ For each rpc method:
75
75
 
76
- The `env` argument contains metadata related to the request (e.g. `env[:output_class]`), and other fields that could have been set from before-hooks (e.g. `env[:user_id]` from authentication).
76
+ * The `req` argument is the input request message, already serialized.
77
+ * The `env` argument is the Twirp environment with data related to the request (e.g. `env[:output_class]`), and other fields that could have been set from before-hooks (e.g. `env[:user_id]` from authentication).
78
+ * The returned value is expected to be the response message (or its attributes), or a `Twirp::Error`.
77
79
 
78
80
 
79
- ### Start the Service
81
+ #### Start the Service
80
82
 
81
- The service is a [Rack app](https://rack.github.io/) instantiated with your handler impementation. For example:
83
+ Instantiate the service with your handler impementation. The service is a [Rack app](https://rack.github.io/). For example:
82
84
 
83
85
  ```ruby
84
86
  handler = HelloWorldHandler.new()
@@ -90,7 +92,7 @@ Rack::Handler::WEBrick.run service
90
92
 
91
93
  Rack apps can also be mounted as Rails routes (e.g. `mount service, at: service.full_name`) and are compatible with many other HTTP frameworks.
92
94
 
93
- ### Unit Tests
95
+ #### Unit Tests
94
96
 
95
97
  Twirp already takes care of HTTP routing and serialization, you don't really need to test that part, insteadof that, focus on testing the handler using the method `.call_rpc(rpc_method, attrs={}, env={})` on the service:
96
98
 
@@ -119,26 +121,37 @@ end
119
121
 
120
122
  ## Twirp Clients
121
123
 
122
- Clients implement the same methods as the service. For Example:
124
+ Instantiate a client with the service base url:
123
125
 
124
126
  ```ruby
125
- c = Example::HelloWorldClient.new("http://localhost:3000")
126
- resp = c.hello(name: "World") # serialized as Protobuf
127
+ client = Example::HelloWorldClient.new("http://localhost:3000")
127
128
  ```
128
129
 
129
- The response object can have `data` or an `error`.
130
+ Clients implement the same methods as the service handler. For example the client for `HelloWorldService` implements the `hello` method:
131
+
132
+ ```ruby
133
+ resp = client.hello(name: "World")
134
+ ```
135
+
136
+ As an alternative, in case that a service method collides with a Ruby method, you can always use the more general `.rpc` method:
137
+
138
+ ```ruby
139
+ resp = client.rpc(:Hello, name: "World") # alternative
140
+ ```
141
+
142
+ If the request fails, the response has an `error` with a `Twirp::Error`. If the request succeeds, the response has `data` with an instance of the response message class.
130
143
 
131
144
  ```ruby
132
145
  if resp.error
133
- puts resp.error #=> <Twirp::Error code:... msg:"..." meta:{...}>
146
+ puts resp.error # <Twirp::Error code:... msg:"..." meta:{...}>
134
147
  else
135
- puts resp.data #=> <Example::HelloResponse: message:"Hello World">
148
+ puts resp.data # <Example::HelloResponse: message:"Hello World">
136
149
  end
137
150
  ```
138
151
 
139
152
  #### Configure Clients with Faraday
140
153
 
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. Clients can be initialized with a Faraday connection:
154
+ 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. Clients can be initialized with a Faraday connection. For example:
142
155
 
143
156
  ```ruby
144
157
  conn = Faraday.new(:url => 'http://localhost:3000') do |c|
@@ -148,25 +161,27 @@ conn = Faraday.new(:url => 'http://localhost:3000') do |c|
148
161
  c.use Faraday::Adapter::NetHttp # can use different HTTP libraries
149
162
  end
150
163
 
151
- c = Example::HelloWorldClient.new(conn)
164
+ client = Example::HelloWorldClient.new(conn)
152
165
  ```
153
166
 
154
167
  #### Protobuf or JSON
155
168
 
156
- Clients use Protobuf by default. To use JSON, set the `content_type` option as 2nd argument:
169
+ Protobuf is used by default. To serialize with JSON, set the `content_type` option as 2nd argument:
157
170
 
158
171
  ```ruby
159
- c = Example::HelloWorldClient.new(conn, content_type: "application/json")
160
- resp = c.hello(name: "World") # serialized as JSON
172
+ client = Example::HelloWorldClient.new(conn, content_type: "application/json")
173
+ resp = client.hello(name: "World") # serialized with JSON
161
174
  ```
162
175
 
163
176
  #### Add-hoc JSON requests
164
177
 
165
- If you just want to make a few quick requests from the console, you can instantiate a plain client and make `.json` calls:
178
+ If you just want to make a few quick requests from the console, you can make a `ClientJSON` instance. This doesn't require a service definition at all, but in the other hand, request and response values are not validated. Responses are just a Hash with attributes.
166
179
 
167
180
  ```ruby
168
- c = Twirp::Client.new(conn, package: "example", service: "HelloWorld")
169
- resp = c.json(:Hello, name: "World") # serialized as JSON, resp.data is a Hash
181
+ require 'twirp'
182
+ client = Twirp::ClientJSON.new(conn, package: "example", service: "HelloWorld")
183
+ resp = client.rpc(:Hello, name: "World") # serialized with JSON
184
+ puts resp # resp.data is a plain Hash
170
185
  ```
171
186
 
172
187
 
@@ -190,10 +205,10 @@ routing -> before -> handler
190
205
  ! exception_raised -> on_error
191
206
  ```
192
207
 
193
- Hooks are setup in the service instance:
208
+ Hooks are setup in the service instance. For example:
194
209
 
195
210
  ```ruby
196
- svc = Example::HelloWorld.new(handler)
211
+ svc = Example::HelloWorldService.new(handler)
197
212
 
198
213
  svc.before do |rack_env, env|
199
214
  # Runs if properly routed to an rpc method, but before calling the method handler.
@@ -2,3 +2,4 @@ require_relative 'twirp/version'
2
2
  require_relative 'twirp/error'
3
3
  require_relative 'twirp/service'
4
4
  require_relative 'twirp/client'
5
+ require_relative 'twirp/client_json'
@@ -1,5 +1,6 @@
1
1
  require 'faraday'
2
2
 
3
+ require_relative 'client_resp'
3
4
  require_relative 'encoding'
4
5
  require_relative 'error'
5
6
  require_relative 'service_dsl'
@@ -117,11 +118,7 @@ module Twirp
117
118
  raise ArgumentError.new("Invalid content_type #{@content_type.inspect}. Expected one of #{Encoding.valid_content_types.inspect}")
118
119
  end
119
120
 
120
- @service_full_name = if opts[:package] || opts[:service]
121
- opts[:package].to_s.empty? ? opts[:service].to_s : "#{opts[:package]}.#{opts[:service]}"
122
- else
123
- self.class.service_full_name # defined through DSL
124
- end
121
+ @service_full_name = self.class.service_full_name # defined through DSL
125
122
  end
126
123
 
127
124
  # Make a remote procedure call to a defined rpc_method. The input can be a Proto message instance,
@@ -140,6 +137,7 @@ module Twirp
140
137
  resp = @conn.post do |r|
141
138
  r.url "/#{@service_full_name}/#{rpc_method}"
142
139
  r.headers['Content-Type'] = @content_type
140
+ r.headers['Accept'] = @content_type
143
141
  r.body = body
144
142
  end
145
143
 
@@ -155,35 +153,5 @@ module Twirp
155
153
  return ClientResp.new(data, nil)
156
154
  end
157
155
 
158
- # Convenience method to call any rpc method with dynamic json attributes.
159
- # It is like .rpc but does not use the defined Protobuf messages to serialize/deserialize data;
160
- # the request attrs can be anything and the response data is always a plain Hash of attributes.
161
- # This is useful to test a service before doing any code-generation.
162
- def json(rpc_method, attrs={})
163
- body = Encoding.encode_json(attrs)
164
-
165
- resp = @conn.post do |r|
166
- r.url "/#{@service_full_name}/#{rpc_method}"
167
- r.headers['Content-Type'] = Encoding::JSON
168
- r.body = body
169
- end
170
-
171
- if resp.status != 200
172
- return ClientResp.new(nil, self.class.error_from_response(resp))
173
- end
174
-
175
- data = Encoding.decode_json(resp.body)
176
- return ClientResp.new(data, nil)
177
- end
178
- end
179
-
180
- class ClientResp
181
- attr_accessor :data
182
- attr_accessor :error
183
-
184
- def initialize(data, error)
185
- @data = data
186
- @error = error
187
- end
188
156
  end
189
157
  end
@@ -0,0 +1,39 @@
1
+ require_relative 'client'
2
+
3
+ module Twirp
4
+
5
+ # Convenience class to call any rpc method with dynamic json attributes, without a service definition.
6
+ # This is useful to test a service before doing any code-generation.
7
+ class ClientJSON < Twirp::Client
8
+
9
+ def initialize(conn, opts={})
10
+ super(conn, opts)
11
+
12
+ package = opts[:package].to_s
13
+ service = opts[:service].to_s
14
+ raise ArgumentError.new("Missing option :service") if service.empty?
15
+ @service_full_name = package.empty? ? service : "#{package}.#{service}"
16
+ end
17
+
18
+ # This implementation does not use the defined Protobuf messages to serialize/deserialize data;
19
+ # the request attrs can be anything and the response data is always a plain Hash of attributes.
20
+ def rpc(rpc_method, attrs={})
21
+ body = Encoding.encode_json(attrs)
22
+
23
+ resp = @conn.post do |r|
24
+ r.url "/#{@service_full_name}/#{rpc_method}"
25
+ r.headers['Content-Type'] = Encoding::JSON
26
+ r.headers['Accept'] = Encoding::JSON
27
+ r.body = body
28
+ end
29
+
30
+ if resp.status != 200
31
+ return ClientResp.new(nil, self.class.error_from_response(resp))
32
+ end
33
+
34
+ data = Encoding.decode_json(resp.body)
35
+ return ClientResp.new(data, nil)
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,11 @@
1
+ module Twirp
2
+ class ClientResp
3
+ attr_accessor :data
4
+ attr_accessor :error
5
+
6
+ def initialize(data, error)
7
+ @data = data
8
+ @error = error
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module Twirp
2
- VERSION = "0.4.1"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -0,0 +1,67 @@
1
+ require 'minitest/autorun'
2
+ require 'rack/mock'
3
+ require 'google/protobuf'
4
+ require 'json'
5
+
6
+ require_relative '../lib/twirp/client_json'
7
+ require_relative './fake_services'
8
+
9
+ class ClientJSONTest < Minitest::Test
10
+
11
+ def test_client_json_requires_service
12
+ assert_raises ArgumentError do
13
+ Twirp::ClientJSON.new("http://localhost:8080") # missing service
14
+ end
15
+ Twirp::ClientJSON.new("http://localhost:8080", service: "FooBar") # ok
16
+ end
17
+
18
+ def test_client_json_success
19
+ c = Twirp::ClientJSON.new(conn_stub("/my.pkg.Talking/Blah") {|req|
20
+ assert_equal "application/json", req.request_headers['Content-Type']
21
+ assert_equal "application/json", req.request_headers['Accept']
22
+ assert_equal '{"blah1":1,"blah2":2}', req.body # body is json
23
+
24
+ [200, {}, '{"blah_resp": 3}']
25
+ }, package: "my.pkg", service: "Talking")
26
+
27
+ resp = c.rpc :Blah, blah1: 1, blah2: 2
28
+ assert_nil resp.error
29
+ refute_nil resp.data
30
+ assert_equal 3, resp.data["blah_resp"]
31
+ end
32
+
33
+ def test_client_json_error
34
+ c = Twirp::ClientJSON.new(conn_stub("/Foo/Foomo") {|req|
35
+ [400, {}, '{"code": "invalid_argument", "msg": "dont like empty"}']
36
+ }, service: "Foo")
37
+
38
+ resp = c.rpc :Foomo, foo: ""
39
+ assert_nil resp.data
40
+ refute_nil resp.error
41
+ assert_equal :invalid_argument, resp.error.code
42
+ assert_equal "dont like empty", resp.error.msg
43
+ end
44
+
45
+ def test_client_bad_json_route
46
+ c = Twirp::ClientJSON.new(conn_stub("/Foo/OtherMethod") {|req|
47
+ [404, {}, 'not here buddy']
48
+ }, service: "Foo")
49
+
50
+ resp = c.rpc :OtherMethod, foo: ""
51
+ assert_nil resp.data
52
+ refute_nil resp.error
53
+ assert_equal :bad_route, resp.error.code
54
+ end
55
+
56
+
57
+ def conn_stub(path)
58
+ Faraday.new do |conn|
59
+ conn.adapter :test do |stub|
60
+ stub.post(path) do |env|
61
+ yield(env)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ end
@@ -9,7 +9,7 @@ require_relative './fake_services'
9
9
  class ClientTest < Minitest::Test
10
10
 
11
11
  def test_new_empty_client
12
- c = EmptyClient.new("http://localhost:3000")
12
+ c = EmptyClient.new("http://localhost:8080")
13
13
  refute_nil c
14
14
  refute_nil c.instance_variable_get(:@conn) # make sure that connection was assigned
15
15
  assert_equal "EmptyClient", c.instance_variable_get(:@service_full_name)
@@ -27,22 +27,31 @@ class ClientTest < Minitest::Test
27
27
  end
28
28
  end
29
29
 
30
- def test_dsl_method_definition_collision
31
- # To avoid collisions, the Twirp::Client class should have very few methods
32
- mthds = Twirp::Client.instance_methods(false)
33
- assert_equal [:json, :rpc], mthds
30
+ def test_dsl_rpc_method_definition_collisions
31
+ # To avoid collisions, the Twirp::Client class should only have the rpc method.
32
+ assert_equal [:rpc], Twirp::Client.instance_methods(false)
34
33
 
35
- # If one of the methods is being implemented through the DSL, the colision should be avoided
34
+ # If one of the methods is being implemented through the DSL, the colision should be avoided, keeping the previous method.
36
35
  num_mthds = EmptyClient.instance_methods.size
37
- EmptyClient.rpc :Json, Example::Empty, Example::Empty, :ruby_method => :json
38
- assert_equal num_mthds, EmptyClient.instance_methods.size # no new method was added (collision)
36
+ EmptyClient.rpc :Rpc, Example::Empty, Example::Empty, :ruby_method => :rpc
37
+ assert_equal num_mthds, EmptyClient.instance_methods.size # no new method was added (is a collision)
39
38
 
40
- # Make sure that the previous .json method was not modified
41
- c = EmptyClient.new(conn_stub("/EmptyClient/Json") {|req|
42
- [200, {}, json(foo: "bar")]
39
+ # Make sure that the previous .rpc method was not modified
40
+ c = EmptyClient.new(conn_stub("/EmptyClient/Rpc") {|req|
41
+ [200, protoheader, proto(Example::Empty, {})]
43
42
  })
44
- resp = c.json(:Json, foo: "bar")
45
- assert_equal "bar", resp.data["foo"]
43
+ resp = c.rpc(:Rpc, {})
44
+ assert_nil resp.error
45
+ refute_nil resp.data
46
+
47
+ # Adding a method that would override super-class methods like .to_s should also be avoided.
48
+ EmptyClient.rpc :ToString, Example::Empty, Example::Empty, :ruby_method => :to_s
49
+ assert_equal num_mthds, EmptyClient.instance_methods.size # no new method was added (is a collision)
50
+
51
+ # Make sure that the previous .to_s method was not modified
52
+ c = EmptyClient.new("http://localhost:8080")
53
+ resp = c.to_s
54
+ assert_equal String, resp.class
46
55
 
47
56
  # Adding any other rpc would work as expected
48
57
  EmptyClient.rpc :Other, Example::Empty, Example::Empty, :ruby_method => :other
@@ -78,6 +87,7 @@ class ClientTest < Minitest::Test
78
87
  def test_proto_serialized_request_body
79
88
  c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
80
89
  assert_equal "application/protobuf", req.request_headers['Content-Type']
90
+ assert_equal "application/protobuf", req.request_headers['Accept']
81
91
 
82
92
  size = Example::Size.decode(req.body) # body is valid protobuf
83
93
  assert_equal 666, size.inches
@@ -179,6 +189,7 @@ class ClientTest < Minitest::Test
179
189
  def test_json_serialized_request_body_attrs
180
190
  c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
181
191
  assert_equal "application/json", req.request_headers['Content-Type']
192
+ assert_equal "application/json", req.request_headers['Accept']
182
193
  assert_equal '{"inches":666}', req.body # body is valid json
183
194
  [200, jsonheader, '{}']
184
195
  }, content_type: "application/json")
@@ -191,6 +202,7 @@ class ClientTest < Minitest::Test
191
202
  def test_json_serialized_request_body_object
192
203
  c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
193
204
  assert_equal "application/json", req.request_headers['Content-Type']
205
+ assert_equal "application/json", req.request_headers['Accept']
194
206
  assert_equal '{"inches":666}', req.body # body is valid json
195
207
  [200, jsonheader, '{}']
196
208
  }, content_type: "application/json")
@@ -265,46 +277,6 @@ class ClientTest < Minitest::Test
265
277
  assert_equal :bad_route, resp.error.code
266
278
  end
267
279
 
268
- # Call .json
269
- # ----------
270
-
271
- def test_direct_json_success
272
- c = Twirp::Client.new(conn_stub("/my.pkg.Talking/Blah") {|req|
273
- assert_equal "application/json", req.request_headers['Content-Type']
274
- assert_equal '{"blah1":1,"blah2":2}', req.body # body is json
275
-
276
- [200, {}, json(blah_resp: 3)]
277
- }, package: "my.pkg", service: "Talking")
278
-
279
- resp = c.json :Blah, blah1: 1, blah2: 2
280
- assert_nil resp.error
281
- refute_nil resp.data
282
- assert_equal 3, resp.data["blah_resp"]
283
- end
284
-
285
- def test_direct_json_error
286
- c = Twirp::Client.new(conn_stub("/Foo/Foomo") {|req|
287
- [400, {}, json(code: "invalid_argument", msg: "dont like empty")]
288
- }, service: "Foo")
289
-
290
- resp = c.json :Foomo, foo: ""
291
- assert_nil resp.data
292
- refute_nil resp.error
293
- assert_equal :invalid_argument, resp.error.code
294
- assert_equal "dont like empty", resp.error.msg
295
- end
296
-
297
- def test_direct_json_bad_route
298
- c = Twirp::Client.new(conn_stub("/Foo/OtherMethod") {|req|
299
- [404, {}, 'not here buddy']
300
- }, service: "Foo")
301
-
302
- resp = c.json :OtherMethod, foo: ""
303
- assert_nil resp.data
304
- refute_nil resp.error
305
- assert_equal :bad_route, resp.error.code
306
- end
307
-
308
280
 
309
281
  # Test Helpers
310
282
  # ------------
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.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyrus A. Forbes
@@ -73,11 +73,14 @@ files:
73
73
  - README.md
74
74
  - lib/twirp.rb
75
75
  - lib/twirp/client.rb
76
+ - lib/twirp/client_json.rb
77
+ - lib/twirp/client_resp.rb
76
78
  - lib/twirp/encoding.rb
77
79
  - lib/twirp/error.rb
78
80
  - lib/twirp/service.rb
79
81
  - lib/twirp/service_dsl.rb
80
82
  - lib/twirp/version.rb
83
+ - test/client_json_test.rb
81
84
  - test/client_test.rb
82
85
  - test/error_test.rb
83
86
  - test/fake_services.rb
@@ -108,6 +111,7 @@ signing_key:
108
111
  specification_version: 4
109
112
  summary: Twirp services in Ruby.
110
113
  test_files:
114
+ - test/client_json_test.rb
111
115
  - test/client_test.rb
112
116
  - test/error_test.rb
113
117
  - test/fake_services.rb