twirp 0.2.0 → 0.3.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 +85 -108
- data/lib/twirp/client.rb +30 -17
- data/lib/twirp/encoding.rb +32 -0
- data/lib/twirp/service.rb +16 -26
- data/lib/twirp/service_dsl.rb +2 -2
- data/lib/twirp/version.rb +1 -1
- data/test/client_test.rb +70 -8
- data/test/service_test.rb +4 -4
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e62a59d0a5485723751b5d1f15452c79d50a338
|
4
|
+
data.tar.gz: 54444353c6556ad93ac5d3eee42a844f0d9ee106
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c6a92c2d93ef86c8ecffc4ce1a0581eb9b12bca892e018113ab5e4ac0d0c5bce949b1fe19fe8d14b53887f9273196832a4a3d99ed71d0bde07ccfbab5ca5c5d
|
7
|
+
data.tar.gz: 1ae6b8cb525b577e4b2eeb92114100f0d7b0444d3e5e2226dea24467934d464db92e304b6ed9899cbeff6aace09b1cf4b795448c0d10dac3066567cbfffa0e8f
|
data/README.md
CHANGED
@@ -1,150 +1,133 @@
|
|
1
1
|
# Twirp
|
2
2
|
|
3
|
-
|
3
|
+
Twirp allows to easily define RPC services and clients that communicate using Protobuf or JSON over HTTP.
|
4
4
|
|
5
|
-
|
6
|
-
* Twirp::Error
|
7
|
-
* Twirp::Service
|
8
|
-
* Twirp::Client
|
9
|
-
* `protoc-gen-twirp_ruby` protoc plugin for code generation.
|
5
|
+
The [Twirp protocol](https://twitchtv.github.io/twirp/docs/spec_v5.html) is implemented in multiple languages. This means that you can write your service in one language and automatically generate clients in other languages. Refer to the [Golang implementation](https://github.com/twitchtv/twirp) for more details on the project.
|
10
6
|
|
7
|
+
## Install
|
11
8
|
|
12
|
-
|
9
|
+
Add `gem "twirp"` to your Gemfile, or install with `gem install twirp`.
|
13
10
|
|
14
|
-
|
11
|
+
For code generation, you also need [protoc](https://github.com/golang/protobuf) (version 3+).
|
15
12
|
|
16
|
-
|
17
|
-
➜ gem install twirp
|
18
|
-
```
|
19
|
-
|
20
|
-
For code generation, Make sure that you have the [protobuf compiler](https://github.com/golang/protobuf) (install version 3+).
|
21
|
-
And then use `go get` to install the ruby_twirp protoc plugin:
|
13
|
+
## Service DSL
|
22
14
|
|
23
|
-
|
24
|
-
➜ go get -u github.com/cyrusaf/ruby-twirp/protoc-gen-twirp_ruby
|
25
|
-
```
|
15
|
+
A Twirp service defines RPC methods to send and receive Protobuf messages. For example, a `HelloWorld` service:
|
26
16
|
|
17
|
+
```ruby
|
18
|
+
require 'twirp'
|
27
19
|
|
28
|
-
|
20
|
+
module Example
|
21
|
+
class HelloWorldService < Twirp::Service
|
22
|
+
package "example"
|
23
|
+
service "HelloWorld"
|
24
|
+
rpc :Hello, HelloRequest, HelloResponse, :ruby_method => :hello
|
25
|
+
end
|
29
26
|
|
30
|
-
|
27
|
+
class HelloWorldClient < Twirp::Client
|
28
|
+
client_for HelloWorldService
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
31
32
|
|
32
|
-
|
33
|
+
The `HelloRequest` and `HelloResponse` messages are expected to be [google-protobuf](https://github.com/google/protobuf/tree/master/ruby) messages, which can also be defined from their DSL or auto-generated.
|
33
34
|
|
34
|
-
Starting with a [Protobuf](https://developers.google.com/protocol-buffers/docs/proto3) file:
|
35
35
|
|
36
|
-
|
37
|
-
// hello_world/service.proto
|
36
|
+
## Code Generation
|
38
37
|
|
39
|
-
|
40
|
-
package example;
|
38
|
+
RPC messages and the service definition can be auto-generated form a `.proto` file.
|
41
39
|
|
42
|
-
|
43
|
-
|
44
|
-
}
|
40
|
+
Code generation works with [protoc](https://github.com/golang/protobuf) (the protobuf compiler)
|
41
|
+
using the `--ruby_out` option to generate messages and `--twirp_ruby_out` to generate services and clients.
|
45
42
|
|
46
|
-
|
47
|
-
string name = 1;
|
48
|
-
}
|
43
|
+
Make sure to install `protoc` (version 3+). Then use `go get` (Golang) to install the ruby_twirp protoc plugin:
|
49
44
|
|
50
|
-
|
51
|
-
|
52
|
-
}
|
45
|
+
```sh
|
46
|
+
go get -u github.com/cyrusaf/ruby-twirp/protoc-gen-twirp_ruby
|
53
47
|
```
|
54
48
|
|
55
|
-
|
49
|
+
Given a [Protobuf](https://developers.google.com/protocol-buffers/docs/proto3) file like [example/hello_world/service.proto](example/hello_world/service.proto), you can auto-generate proto and twirp files with the command:
|
56
50
|
|
57
51
|
```sh
|
58
|
-
|
52
|
+
protoc --proto_path=. --ruby_out=. --twirp_ruby_out=. ./example/hello_world/service.proto
|
59
53
|
```
|
60
54
|
|
61
|
-
|
55
|
+
|
56
|
+
## Twirp Service
|
57
|
+
|
58
|
+
A Twirp service delegates into a service handler to implement each rpc method. For example a handler for `HelloWorld`:
|
62
59
|
|
63
60
|
```ruby
|
64
|
-
|
65
|
-
require 'twirp'
|
61
|
+
class HelloWorldHandler
|
66
62
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
63
|
+
def hello(req, env)
|
64
|
+
if req.name.empty?
|
65
|
+
Twirp::Error.invalid_argument("name is mandatory")
|
66
|
+
else
|
67
|
+
{message: "Hello #{req.name}"}
|
68
|
+
end
|
72
69
|
end
|
73
70
|
|
74
|
-
class HelloWorldClient < Twirp::Client
|
75
|
-
client_for HelloWorldService
|
76
|
-
end
|
77
71
|
end
|
78
72
|
```
|
79
73
|
|
80
|
-
|
74
|
+
The `req` argument is the request message (input), and the returned value is expected to be the response message, or a `Twirp::Error`.
|
81
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).
|
82
77
|
|
83
|
-
|
78
|
+
### Unit Tests
|
84
79
|
|
85
|
-
|
86
|
-
For each method, the `intput` is an instance of the protobuf request message. The Twirp `env`
|
87
|
-
contains metadata related to the request, and other fields that could have been set from before
|
88
|
-
hooks (e.g. `env[:user_id]` from authentication).
|
80
|
+
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:
|
89
81
|
|
90
82
|
```ruby
|
91
|
-
|
92
|
-
|
93
|
-
|
83
|
+
require 'minitest/autorun'
|
84
|
+
|
85
|
+
class HelloWorldHandlerTest < Minitest::Test
|
86
|
+
|
87
|
+
def test_hello_responds_with_name
|
88
|
+
resp = service.call_rpc :Hello, name: "World"
|
89
|
+
assert_equal "Hello World", resp.message
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_hello_name_is_mandatory
|
93
|
+
twerr = service.call_rpc :Hello, name: ""
|
94
|
+
assert_equal :invalid_argument, twerr.code
|
95
|
+
end
|
96
|
+
|
97
|
+
def service
|
98
|
+
handler = HelloWorldHandler.new()
|
99
|
+
Example::HelloWorldService.new(handler)
|
94
100
|
end
|
95
101
|
end
|
96
102
|
```
|
97
103
|
|
98
|
-
### Mount the service to receive HTTP requests
|
99
104
|
|
100
|
-
|
105
|
+
### Start the Service
|
101
106
|
|
102
|
-
|
103
|
-
require 'rack'
|
107
|
+
The service is a [Rack app](https://rack.github.io/) instantiated with your handler impementation. For example:
|
104
108
|
|
105
|
-
|
106
|
-
|
109
|
+
```ruby
|
110
|
+
handler = HelloWorldHandler.new()
|
111
|
+
service = Example::HelloWorldService.new(handler)
|
107
112
|
|
113
|
+
require 'rack'
|
108
114
|
Rack::Handler::WEBrick.run service
|
109
115
|
```
|
110
116
|
|
111
|
-
|
117
|
+
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.
|
112
118
|
|
113
|
-
Now you can start the server and `curl` with JSON to test if everything works:
|
114
119
|
|
115
|
-
|
116
|
-
➜ curl --request POST \
|
117
|
-
--url http://localhost:8080/example.HelloWorld/Hello \
|
118
|
-
--header 'Content-Type: application/json' \
|
119
|
-
--data '{"name": "World"}'
|
120
|
-
```
|
120
|
+
## Twirp Clients
|
121
121
|
|
122
|
-
|
123
|
-
|
124
|
-
Twirp already takes care of HTTP routing and serialization, you don't really need to build fake HTTP requests in your tests.
|
125
|
-
Instead, you should focus on testing your Service Handler. For convenience, the Twirp Service has the method
|
126
|
-
`.call_rpc(rpc_method, attrs={}, env={})` to call the handler with a fake Twirp env and making sure that the handler output is valid.
|
122
|
+
Instantiate the client with the base_url:
|
127
123
|
|
128
124
|
```ruby
|
129
|
-
|
130
|
-
|
131
|
-
class HelloWorldHandlerTest < Minitest::Test
|
132
|
-
def test_hello_responds_with_name
|
133
|
-
service = Example::HelloWorld.new(HelloWorldHandler.new())
|
134
|
-
out = service.call_rpc :Hello, name: "World"
|
135
|
-
assert_equal "Hello World", out.message
|
136
|
-
end
|
137
|
-
end
|
125
|
+
c = Example::HelloWorldClient.new("http://localhost:3000")
|
138
126
|
```
|
139
127
|
|
140
|
-
|
141
|
-
## Clients
|
142
|
-
|
143
|
-
Generated clients implement the methods defined in the proto file. The response object contains `data` with an instance of the response class if successfull,
|
144
|
-
or an `error` with an instance of `Twirp::Error` if there was a problem. For example, with the HelloWorld generated client:
|
128
|
+
Clients implement the same methods as in the service and return a response object with `data` or `error`:
|
145
129
|
|
146
130
|
```ruby
|
147
|
-
c = Example::HelloWorldClient.new("http://localhost:3000")
|
148
131
|
resp = c.hello(name: "World")
|
149
132
|
if resp.error
|
150
133
|
puts resp.error #=> <Twirp::Error code:... msg:"..." meta:{...}>
|
@@ -153,33 +136,27 @@ else
|
|
153
136
|
end
|
154
137
|
```
|
155
138
|
|
156
|
-
|
139
|
+
### Protobuf or JSON
|
157
140
|
|
158
|
-
|
159
|
-
class MyClient < Twirp::Client
|
160
|
-
package "example"
|
161
|
-
service "MyService"
|
162
|
-
rpc :MyMethod, ReqClass, RespClass, :ruby_method => :my_method
|
163
|
-
end
|
141
|
+
Clients use Protobuf by default. To use JSON, set the `content_type` option:
|
164
142
|
|
165
|
-
|
166
|
-
|
143
|
+
```ruby
|
144
|
+
c = Example::HelloWorldClient.new("http://localhost:3000", content_type: "application/json")
|
167
145
|
```
|
168
146
|
|
147
|
+
### Configure Clients with Faraday
|
169
148
|
|
170
|
-
|
171
|
-
|
172
|
-
A Twirp client takes care of routing, serialization and error handling.
|
173
|
-
|
174
|
-
Other advanced HTTP options can be configured with [Faraday](https://github.com/lostisland/faraday) middleware. For example:
|
149
|
+
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:
|
175
150
|
|
176
151
|
```ruby
|
177
|
-
|
178
|
-
c.use Faraday::Request::Retry
|
152
|
+
conn = Faraday.new(:url => 'http://localhost:3000') do |c|
|
153
|
+
c.use Faraday::Request::Retry
|
179
154
|
c.use Faraday::Request::BasicAuthentication, 'login', 'pass'
|
180
155
|
c.use Faraday::Response::Logger # log to STDOUT
|
181
|
-
c.use Faraday::Adapter::NetHttp #
|
182
|
-
end
|
156
|
+
c.use Faraday::Adapter::NetHttp # can use different HTTP libraries
|
157
|
+
end
|
158
|
+
|
159
|
+
c = Example::HelloWorldClient.new(conn)
|
183
160
|
```
|
184
161
|
|
185
162
|
## Server Hooks
|
data/lib/twirp/client.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
require 'faraday'
|
2
2
|
require 'json'
|
3
3
|
|
4
|
-
require_relative
|
5
|
-
require_relative
|
4
|
+
require_relative 'encoding'
|
5
|
+
require_relative 'error'
|
6
|
+
require_relative 'service_dsl'
|
6
7
|
|
7
8
|
module Twirp
|
8
9
|
|
@@ -23,37 +24,49 @@ module Twirp
|
|
23
24
|
# Hook for ServiceDSL#rpc to define a new method client.<ruby_method>(input, opts).
|
24
25
|
def self.rpc_define_method(rpcdef)
|
25
26
|
define_method rpcdef[:ruby_method] do |input|
|
26
|
-
|
27
|
+
rpc(rpcdef[:rpc_method], input)
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
30
|
-
# Init with a Faraday connection.
|
31
|
-
|
31
|
+
# Init with a Faraday connection, or a base_url that is used in a default connection.
|
32
|
+
# Clients use Content-Type="application/protobuf" by default. For JSON clinets use :content_type => "application/json".
|
33
|
+
def initialize(conn, opts={})
|
32
34
|
@conn = case conn
|
33
|
-
|
34
|
-
|
35
|
-
|
35
|
+
when String then Faraday.new(url: conn) # init with hostname
|
36
|
+
when Faraday::Connection then conn # init with connection
|
37
|
+
else raise ArgumentError.new("Invalid conn #{conn.inspect}. Expected String hostname or Faraday::Connection")
|
38
|
+
end
|
39
|
+
|
40
|
+
@content_type = (opts[:content_type] || Encoding::PROTO)
|
41
|
+
if !Encoding.valid_content_type?(@content_type)
|
42
|
+
raise ArgumentError.new("Invalid content_type #{@content_type.inspect}. Expected one of #{Encoding.valid_content_types.inspect}")
|
36
43
|
end
|
37
44
|
end
|
38
45
|
|
39
|
-
def service_full_name
|
46
|
+
def service_full_name
|
47
|
+
self.class.service_full_name
|
48
|
+
end
|
40
49
|
|
41
50
|
def rpc_path(rpc_method)
|
42
51
|
"/#{service_full_name}/#{rpc_method}"
|
43
52
|
end
|
44
53
|
|
45
|
-
|
54
|
+
# Make a remote procedure call to a defined rpc_method. The input can be a Proto message instance,
|
55
|
+
# or the attributes (Hash) to instantiate it. Returns a ClientResp instance with an instance of
|
56
|
+
# output_class, or a Twirp::Error. The input and output classes are the ones configued with the rpc DSL.
|
57
|
+
# If rpc_method was not defined with the rpc DSL then a response with a bad_route error is returned instead.
|
58
|
+
def rpc(rpc_method, input)
|
46
59
|
rpcdef = self.class.rpcs[rpc_method.to_s]
|
47
60
|
if !rpcdef
|
48
61
|
return ClientResp.new(nil, Twirp::Error.bad_route("rpc not defined on this client"))
|
49
62
|
end
|
50
63
|
|
51
64
|
input = rpcdef[:input_class].new(input) if input.is_a? Hash
|
52
|
-
body = rpcdef[:input_class]
|
65
|
+
body = Encoding.encode(input, rpcdef[:input_class], @content_type)
|
53
66
|
|
54
67
|
resp = @conn.post do |r|
|
55
68
|
r.url rpc_path(rpc_method)
|
56
|
-
r.headers['Content-Type'] =
|
69
|
+
r.headers['Content-Type'] = @content_type
|
57
70
|
r.body = body
|
58
71
|
end
|
59
72
|
|
@@ -61,11 +74,11 @@ module Twirp
|
|
61
74
|
return ClientResp.new(nil, error_from_response(resp))
|
62
75
|
end
|
63
76
|
|
64
|
-
if resp.headers['Content-Type'] !=
|
65
|
-
return ClientResp.new(nil, Twirp::Error.internal("Expected response Content-Type
|
77
|
+
if resp.headers['Content-Type'] != @content_type
|
78
|
+
return ClientResp.new(nil, Twirp::Error.internal("Expected response Content-Type #{@content_type.inspect} but found #{resp.headers['Content-Type'].inspect}"))
|
66
79
|
end
|
67
80
|
|
68
|
-
data =
|
81
|
+
data = Encoding.decode(resp.body, rpcdef[:output_class], @content_type)
|
69
82
|
return ClientResp.new(data, nil)
|
70
83
|
end
|
71
84
|
|
@@ -79,7 +92,7 @@ module Twirp
|
|
79
92
|
err_attrs = nil
|
80
93
|
begin
|
81
94
|
err_attrs = JSON.parse(resp.body)
|
82
|
-
rescue JSON::ParserError
|
95
|
+
rescue JSON::ParserError
|
83
96
|
return twirp_error_from_intermediary(status, "Response is not JSON", resp.body)
|
84
97
|
end
|
85
98
|
|
@@ -109,7 +122,7 @@ module Twirp
|
|
109
122
|
else :unknown
|
110
123
|
end
|
111
124
|
|
112
|
-
|
125
|
+
Twirp::Error.new(code, code.to_s, {
|
113
126
|
http_error_from_intermediary: "true",
|
114
127
|
not_a_twirp_error_because: reason,
|
115
128
|
status_code: status.to_s,
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Twirp
|
2
|
+
|
3
|
+
module Encoding
|
4
|
+
JSON = "application/json"
|
5
|
+
PROTO = "application/protobuf"
|
6
|
+
|
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")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
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")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.valid_content_type?(content_type)
|
24
|
+
content_type == Encoding::JSON || content_type == Encoding::PROTO
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.valid_content_types
|
28
|
+
[Encoding::JSON, Encoding::PROTO]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
data/lib/twirp/service.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
require
|
1
|
+
require 'json'
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
3
|
+
require_relative 'encoding'
|
4
|
+
require_relative 'error'
|
5
|
+
require_relative 'service_dsl'
|
5
6
|
|
6
7
|
module Twirp
|
7
8
|
|
@@ -42,12 +43,12 @@ module Twirp
|
|
42
43
|
env = {}
|
43
44
|
bad_route = route_request(rack_env, env)
|
44
45
|
return error_response(bad_route, env) if bad_route
|
45
|
-
|
46
|
+
|
46
47
|
@before.each do |hook|
|
47
48
|
result = hook.call(rack_env, env)
|
48
49
|
return error_response(result, env) if result.is_a? Twirp::Error
|
49
50
|
end
|
50
|
-
|
51
|
+
|
51
52
|
output = call_handler(env)
|
52
53
|
return error_response(output, env) if output.is_a? Twirp::Error
|
53
54
|
return success_response(output, env)
|
@@ -59,7 +60,7 @@ module Twirp
|
|
59
60
|
rescue => hook_e
|
60
61
|
e = hook_e
|
61
62
|
end
|
62
|
-
|
63
|
+
|
63
64
|
twerr = Twirp::Error.internal_with(e)
|
64
65
|
return error_response(twerr, env)
|
65
66
|
end
|
@@ -77,7 +78,7 @@ module Twirp
|
|
77
78
|
env = env.merge(base_env)
|
78
79
|
input = env[:input_class].new(input) if input.is_a? Hash
|
79
80
|
env[:input] = input
|
80
|
-
env[:content_type] ||=
|
81
|
+
env[:content_type] ||= Encoding::PROTO
|
81
82
|
env[:http_response_headers] = {}
|
82
83
|
call_handler(env)
|
83
84
|
end
|
@@ -95,11 +96,11 @@ module Twirp
|
|
95
96
|
end
|
96
97
|
|
97
98
|
content_type = rack_request.get_header("CONTENT_TYPE")
|
98
|
-
if content_type
|
99
|
-
return bad_route_error("
|
99
|
+
if !Encoding.valid_content_type?(content_type)
|
100
|
+
return bad_route_error("Unexpected Content-Type: #{content_type.inspect}. Content-Type header must be one of #{Encoding.valid_content_types.inspect}", rack_request)
|
100
101
|
end
|
101
102
|
env[:content_type] = content_type
|
102
|
-
|
103
|
+
|
103
104
|
path_parts = rack_request.fullpath.split("/")
|
104
105
|
if path_parts.size < 3 || path_parts[-2] != self.full_name
|
105
106
|
return bad_route_error("Invalid route. Expected format: POST {BaseURL}/#{self.full_name}/{Method}", rack_request)
|
@@ -114,8 +115,8 @@ module Twirp
|
|
114
115
|
|
115
116
|
input = nil
|
116
117
|
begin
|
117
|
-
input =
|
118
|
-
rescue
|
118
|
+
input = Encoding.decode(rack_request.body.read, env[:input_class], content_type)
|
119
|
+
rescue
|
119
120
|
return bad_route_error("Invalid request body for rpc method #{method_name.inspect} with Content-Type=#{content_type}", rack_request)
|
120
121
|
end
|
121
122
|
|
@@ -128,19 +129,7 @@ module Twirp
|
|
128
129
|
Twirp::Error.bad_route msg, twirp_invalid_route: "#{req.request_method} #{req.fullpath}"
|
129
130
|
end
|
130
131
|
|
131
|
-
def decode_input(bytes, input_class, content_type)
|
132
|
-
case content_type
|
133
|
-
when "application/protobuf" then input_class.decode(bytes)
|
134
|
-
when "application/json" then input_class.decode_json(bytes)
|
135
|
-
end
|
136
|
-
end
|
137
132
|
|
138
|
-
def encode_output(obj, output_class, content_type)
|
139
|
-
case content_type
|
140
|
-
when "application/protobuf" then output_class.encode(obj)
|
141
|
-
when "application/json" then output_class.encode_json(obj)
|
142
|
-
end
|
143
|
-
end
|
144
133
|
|
145
134
|
# Call handler method and return a Protobuf Message or a Twirp::Error.
|
146
135
|
def call_handler(env)
|
@@ -166,7 +155,7 @@ module Twirp
|
|
166
155
|
@on_success.each{|hook| hook.call(env) }
|
167
156
|
|
168
157
|
headers = env[:http_response_headers].merge('Content-Type' => env[:content_type])
|
169
|
-
resp_body =
|
158
|
+
resp_body = Encoding.encode(output, env[:output_class], env[:content_type])
|
170
159
|
[200, headers, [resp_body]]
|
171
160
|
|
172
161
|
rescue => e
|
@@ -201,7 +190,8 @@ module Twirp
|
|
201
190
|
end
|
202
191
|
|
203
192
|
def error_response_headers
|
204
|
-
|
193
|
+
# Twirp errors are always JSON, even if the request was protobuf
|
194
|
+
{'Content-Type' => Encoding::JSON}
|
205
195
|
end
|
206
196
|
|
207
197
|
end
|
data/lib/twirp/service_dsl.rb
CHANGED
@@ -4,12 +4,12 @@ module Twirp
|
|
4
4
|
|
5
5
|
# Configure service package name.
|
6
6
|
def package(name)
|
7
|
-
@package = name
|
7
|
+
@package = name.to_s
|
8
8
|
end
|
9
9
|
|
10
10
|
# Configure service name.
|
11
11
|
def service(name)
|
12
|
-
@service = name
|
12
|
+
@service = name.to_s
|
13
13
|
end
|
14
14
|
|
15
15
|
# Configure service rpc methods.
|
data/lib/twirp/version.rb
CHANGED
data/test/client_test.rb
CHANGED
@@ -27,39 +27,39 @@ class ClientTest < Minitest::Test
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
def
|
30
|
+
def test_rpc_success
|
31
31
|
c = FooClient.new(conn_stub("/Foo/Foo") {|req|
|
32
32
|
[200, protoheader, proto(Foo, foo: "out")]
|
33
33
|
})
|
34
|
-
resp = c.
|
34
|
+
resp = c.rpc :Foo, foo: "in"
|
35
35
|
assert_nil resp.error
|
36
36
|
refute_nil resp.data
|
37
37
|
assert_equal "out", resp.data.foo
|
38
38
|
end
|
39
39
|
|
40
|
-
def
|
40
|
+
def test_rpc_error
|
41
41
|
c = FooClient.new(conn_stub("/Foo/Foo") {|req|
|
42
42
|
[400, {}, json(code: "invalid_argument", msg: "dont like empty")]
|
43
43
|
})
|
44
|
-
resp = c.
|
44
|
+
resp = c.rpc :Foo, foo: ""
|
45
45
|
assert_nil resp.data
|
46
46
|
refute_nil resp.error
|
47
47
|
assert_equal :invalid_argument, resp.error.code
|
48
48
|
assert_equal "dont like empty", resp.error.msg
|
49
49
|
end
|
50
50
|
|
51
|
-
def
|
51
|
+
def test_rpc_serialization_exception
|
52
52
|
c = FooClient.new(conn_stub("/Foo/Foo") {|req|
|
53
53
|
[200, protoheader, "badstuff"]
|
54
54
|
})
|
55
55
|
assert_raises Google::Protobuf::ParseError do
|
56
|
-
|
56
|
+
c.rpc :Foo, foo: "in"
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
-
def
|
60
|
+
def test_rpc_invalid_method
|
61
61
|
c = FooClient.new("http://localhost")
|
62
|
-
resp = c.
|
62
|
+
resp = c.rpc :OtherStuff, foo: "noo"
|
63
63
|
assert_nil resp.data
|
64
64
|
refute_nil resp.error
|
65
65
|
assert_equal :bad_route, resp.error.code
|
@@ -174,6 +174,64 @@ class ClientTest < Minitest::Test
|
|
174
174
|
assert_equal '{"msg":"I have no code of honor"}', resp.error.meta[:body]
|
175
175
|
end
|
176
176
|
|
177
|
+
def test_json_success
|
178
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
179
|
+
[200, jsonheader, '{"inches": 99, "color": "red"}']
|
180
|
+
}, content_type: "application/json")
|
181
|
+
|
182
|
+
resp = c.make_hat({})
|
183
|
+
assert_nil resp.error
|
184
|
+
assert_equal 99, resp.data.inches
|
185
|
+
assert_equal "red", resp.data.color
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_json_serialized_request_body_attrs
|
189
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
190
|
+
assert_equal "application/json", req.request_headers['Content-Type']
|
191
|
+
assert_equal '{"inches":666}', req.body # body is valid json
|
192
|
+
[200, jsonheader, '{}']
|
193
|
+
}, content_type: "application/json")
|
194
|
+
|
195
|
+
resp = c.make_hat(inches: 666)
|
196
|
+
assert_nil resp.error
|
197
|
+
refute_nil resp.data
|
198
|
+
end
|
199
|
+
|
200
|
+
def test_json_serialized_request_body_object
|
201
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
202
|
+
assert_equal "application/json", req.request_headers['Content-Type']
|
203
|
+
assert_equal '{"inches":666}', req.body # body is valid json
|
204
|
+
[200, jsonheader, '{}']
|
205
|
+
}, content_type: "application/json")
|
206
|
+
|
207
|
+
resp = c.make_hat(Example::Size.new(inches: 666))
|
208
|
+
assert_nil resp.error
|
209
|
+
refute_nil resp.data
|
210
|
+
end
|
211
|
+
|
212
|
+
def test_json_error
|
213
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
214
|
+
[500, {}, json(code: "internal", msg: "something went wrong")]
|
215
|
+
}, content_type: "application/json")
|
216
|
+
|
217
|
+
resp = c.make_hat(inches: 1)
|
218
|
+
assert_nil resp.data
|
219
|
+
refute_nil resp.error
|
220
|
+
assert_equal :internal, resp.error.code
|
221
|
+
assert_equal "something went wrong", resp.error.msg
|
222
|
+
end
|
223
|
+
|
224
|
+
def test_json_missing_response_header
|
225
|
+
c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
|
226
|
+
[200, {}, json(inches: 99, color: "red")]
|
227
|
+
}, content_type: "application/json")
|
228
|
+
|
229
|
+
resp = c.make_hat({})
|
230
|
+
refute_nil resp.error
|
231
|
+
assert_equal :internal, resp.error.code
|
232
|
+
assert_equal 'Expected response Content-Type "application/json" but found nil', resp.error.msg
|
233
|
+
end
|
234
|
+
|
177
235
|
|
178
236
|
# Test Helpers
|
179
237
|
# ------------
|
@@ -182,6 +240,10 @@ class ClientTest < Minitest::Test
|
|
182
240
|
{'Content-Type' => 'application/protobuf'}
|
183
241
|
end
|
184
242
|
|
243
|
+
def jsonheader
|
244
|
+
{'Content-Type' => 'application/json'}
|
245
|
+
end
|
246
|
+
|
185
247
|
def proto(clss, attrs={})
|
186
248
|
clss.encode(clss.new(attrs))
|
187
249
|
end
|
data/test/service_test.rb
CHANGED
@@ -100,7 +100,7 @@ class ServiceTest < Minitest::Test
|
|
100
100
|
assert_equal 'application/json', headers['Content-Type']
|
101
101
|
assert_equal({
|
102
102
|
"code" => 'bad_route',
|
103
|
-
"msg" => '
|
103
|
+
"msg" => 'Unexpected Content-Type: "text/plain". Content-Type header must be one of ["application/json", "application/protobuf"]',
|
104
104
|
"meta" => {"twirp_invalid_route" => "POST /example.Haberdasher/MakeHat"},
|
105
105
|
}, JSON.parse(body[0]))
|
106
106
|
end
|
@@ -178,7 +178,7 @@ class ServiceTest < Minitest::Test
|
|
178
178
|
end
|
179
179
|
|
180
180
|
rack_env = json_req "/example.Haberdasher/BadRouteMethod", inches: 10
|
181
|
-
status,
|
181
|
+
status, _, body = svc.call(rack_env)
|
182
182
|
|
183
183
|
assert_equal 404, status
|
184
184
|
refute before_called
|
@@ -188,7 +188,7 @@ class ServiceTest < Minitest::Test
|
|
188
188
|
|
189
189
|
def test_long_base_url
|
190
190
|
rack_env = json_req "long-ass/base/url/twirp/example.Haberdasher/MakeHat", {inches: 10}
|
191
|
-
status,
|
191
|
+
status, _, body = haberdasher_service.call(rack_env)
|
192
192
|
|
193
193
|
assert_equal 200, status
|
194
194
|
end
|
@@ -200,7 +200,7 @@ class ServiceTest < Minitest::Test
|
|
200
200
|
end)
|
201
201
|
|
202
202
|
rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new
|
203
|
-
status,
|
203
|
+
status, _, body = svc.call(rack_env)
|
204
204
|
|
205
205
|
assert_equal 200, status
|
206
206
|
assert_equal Example::Hat.new(inches: 11, color: ""), Example::Hat.decode(body[0])
|
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
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyrus A. Forbes
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2018-04-
|
12
|
+
date: 2018-04-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: google-protobuf
|
@@ -73,6 +73,7 @@ files:
|
|
73
73
|
- README.md
|
74
74
|
- lib/twirp.rb
|
75
75
|
- lib/twirp/client.rb
|
76
|
+
- lib/twirp/encoding.rb
|
76
77
|
- lib/twirp/error.rb
|
77
78
|
- lib/twirp/service.rb
|
78
79
|
- lib/twirp/service_dsl.rb
|