twirp 0.2.0 → 0.3.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 +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
|