twirp 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +30 -31
- data/lib/twirp/client.rb +92 -85
- data/lib/twirp/version.rb +1 -1
- data/test/client_test.rb +22 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a30f71eb6c946fa8356d486b1decd82733bbb3af
|
4
|
+
data.tar.gz: db3c570238ac24017af314eedb327fd8f5f07bb0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a59ff7c4ba349bce47e06a3a47d68eccfd411f43b63df34948593ab0d7987a596a766df398b633a8b31890cbadd09297c89d7619bd7fb1990d4538ae65c9c297
|
7
|
+
data.tar.gz: 39b024e1ba715058c2ad07c7b3e5942d0d3aae47960a02402b1f497c3bd6fdb1bdc6cd19c88005a8844238c2686e6d40251aeeeca13ed0cadb50f499f2c3c216
|
data/README.md
CHANGED
@@ -53,19 +53,19 @@ protoc --proto_path=. --ruby_out=. --twirp_ruby_out=. ./example/hello_world/serv
|
|
53
53
|
```
|
54
54
|
|
55
55
|
|
56
|
-
## Twirp Service
|
56
|
+
## Twirp Service Handler
|
57
57
|
|
58
|
-
A
|
58
|
+
A handler is a simple class that implements each rpc method. For example a handler for `HelloWorld`:
|
59
59
|
|
60
60
|
```ruby
|
61
61
|
class HelloWorldHandler
|
62
62
|
|
63
63
|
def hello(req, env)
|
64
64
|
if req.name.empty?
|
65
|
-
Twirp::Error.invalid_argument("
|
66
|
-
else
|
67
|
-
{message: "Hello #{req.name}"}
|
65
|
+
return Twirp::Error.invalid_argument("is mandatory", argument: "name")
|
68
66
|
end
|
67
|
+
|
68
|
+
{message: "Hello #{req.name}"}
|
69
69
|
end
|
70
70
|
|
71
71
|
end
|
@@ -75,6 +75,21 @@ The `req` argument is the request message (input), and the returned value is exp
|
|
75
75
|
|
76
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).
|
77
77
|
|
78
|
+
|
79
|
+
### Start the Service
|
80
|
+
|
81
|
+
The service is a [Rack app](https://rack.github.io/) instantiated with your handler impementation. For example:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
handler = HelloWorldHandler.new()
|
85
|
+
service = Example::HelloWorldService.new(handler)
|
86
|
+
|
87
|
+
require 'rack'
|
88
|
+
Rack::Handler::WEBrick.run service
|
89
|
+
```
|
90
|
+
|
91
|
+
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
|
+
|
78
93
|
### Unit Tests
|
79
94
|
|
80
95
|
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:
|
@@ -102,33 +117,18 @@ end
|
|
102
117
|
```
|
103
118
|
|
104
119
|
|
105
|
-
### Start the Service
|
106
|
-
|
107
|
-
The service is a [Rack app](https://rack.github.io/) instantiated with your handler impementation. For example:
|
108
|
-
|
109
|
-
```ruby
|
110
|
-
handler = HelloWorldHandler.new()
|
111
|
-
service = Example::HelloWorldService.new(handler)
|
112
|
-
|
113
|
-
require 'rack'
|
114
|
-
Rack::Handler::WEBrick.run service
|
115
|
-
```
|
116
|
-
|
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.
|
118
|
-
|
119
|
-
|
120
120
|
## Twirp Clients
|
121
121
|
|
122
|
-
|
122
|
+
Clients implement the same methods as the service. For Example:
|
123
123
|
|
124
124
|
```ruby
|
125
125
|
c = Example::HelloWorldClient.new("http://localhost:3000")
|
126
|
+
resp = c.hello(name: "World") # serialized as Protobuf
|
126
127
|
```
|
127
128
|
|
128
|
-
|
129
|
+
The response object can have `data` or an `error`.
|
129
130
|
|
130
131
|
```ruby
|
131
|
-
resp = c.hello(name: "World")
|
132
132
|
if resp.error
|
133
133
|
puts resp.error #=> <Twirp::Error code:... msg:"..." meta:{...}>
|
134
134
|
else
|
@@ -136,9 +136,9 @@ else
|
|
136
136
|
end
|
137
137
|
```
|
138
138
|
|
139
|
-
|
139
|
+
#### Configure Clients with Faraday
|
140
140
|
|
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.
|
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:
|
142
142
|
|
143
143
|
```ruby
|
144
144
|
conn = Faraday.new(:url => 'http://localhost:3000') do |c|
|
@@ -151,23 +151,22 @@ end
|
|
151
151
|
c = Example::HelloWorldClient.new(conn)
|
152
152
|
```
|
153
153
|
|
154
|
-
|
154
|
+
#### Protobuf or JSON
|
155
155
|
|
156
156
|
Clients use Protobuf by default. To use JSON, set the `content_type` option as 2nd argument:
|
157
157
|
|
158
158
|
```ruby
|
159
|
-
c = Example::HelloWorldClient.new(
|
159
|
+
c = Example::HelloWorldClient.new(conn, content_type: "application/json")
|
160
160
|
resp = c.hello(name: "World") # serialized as JSON
|
161
161
|
```
|
162
162
|
|
163
|
-
|
163
|
+
#### Add-hoc JSON requests
|
164
164
|
|
165
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
166
|
|
167
167
|
```ruby
|
168
|
-
c = Twirp::Client.new(
|
169
|
-
resp = c.json(:Hello, name: "World")
|
170
|
-
puts resp.data["message"]
|
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
|
171
170
|
```
|
172
171
|
|
173
172
|
|
data/lib/twirp/client.rb
CHANGED
@@ -11,21 +11,97 @@ module Twirp
|
|
11
11
|
# DSL to define a client with package, service and rpcs.
|
12
12
|
extend ServiceDSL
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
class << self # class methods
|
15
|
+
|
16
|
+
# DSL (alternative) to define a client from a Service class.
|
17
|
+
def client_for(svclass)
|
18
|
+
package svclass.package_name
|
19
|
+
service svclass.service_name
|
20
|
+
svclass.rpcs.each do |rpc_method, rpcdef|
|
21
|
+
rpc rpc_method, rpcdef[:input_class], rpcdef[:output_class], ruby_method: rpcdef[:ruby_method]
|
22
|
+
end
|
20
23
|
end
|
21
|
-
end
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
# Hook for ServiceDSL#rpc to define a new method client.<ruby_method>(input, opts).
|
26
|
+
def rpc_define_method(rpcdef)
|
27
|
+
unless method_defined? rpcdef[:ruby_method] # collision with existing rpc method
|
28
|
+
define_method rpcdef[:ruby_method] do |input|
|
29
|
+
rpc(rpcdef[:rpc_method], input)
|
30
|
+
end
|
31
|
+
end
|
27
32
|
end
|
28
|
-
|
33
|
+
|
34
|
+
def error_from_response(resp)
|
35
|
+
status = resp.status
|
36
|
+
|
37
|
+
if is_http_redirect? status
|
38
|
+
return twirp_redirect_error(status, resp.headers['Location'])
|
39
|
+
end
|
40
|
+
|
41
|
+
err_attrs = nil
|
42
|
+
begin
|
43
|
+
err_attrs = Encoding.decode_json(resp.body)
|
44
|
+
rescue JSON::ParserError
|
45
|
+
return twirp_error_from_intermediary(status, "Response is not JSON", resp.body)
|
46
|
+
end
|
47
|
+
|
48
|
+
code = err_attrs["code"]
|
49
|
+
if code.to_s.empty?
|
50
|
+
return twirp_error_from_intermediary(status, "Response is JSON but it has no \"code\" attribute", resp.body)
|
51
|
+
end
|
52
|
+
code = code.to_s.to_sym
|
53
|
+
if !Twirp::Error.valid_code?(code)
|
54
|
+
return Twirp::Error.internal("Invalid Twirp error code: #{code}", invalid_code: code.to_s, body: resp.body)
|
55
|
+
end
|
56
|
+
|
57
|
+
Twirp::Error.new(code, err_attrs["msg"], err_attrs["meta"])
|
58
|
+
end
|
59
|
+
|
60
|
+
# Error that was caused by an intermediary proxy like a load balancer.
|
61
|
+
# The HTTP errors code from non-twirp sources is mapped to equivalent twirp errors.
|
62
|
+
# The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md.
|
63
|
+
# Returned twirp Errors have some additional metadata for inspection.
|
64
|
+
def twirp_error_from_intermediary(status, reason, body)
|
65
|
+
code = case status
|
66
|
+
when 400 then :internal
|
67
|
+
when 401 then :unauthenticated
|
68
|
+
when 403 then :permission_denied
|
69
|
+
when 404 then :bad_route
|
70
|
+
when 429, 502, 503, 504 then :unavailable
|
71
|
+
else :unknown
|
72
|
+
end
|
73
|
+
|
74
|
+
Twirp::Error.new(code, code.to_s, {
|
75
|
+
http_error_from_intermediary: "true",
|
76
|
+
not_a_twirp_error_because: reason,
|
77
|
+
status_code: status.to_s,
|
78
|
+
body: body.to_s,
|
79
|
+
})
|
80
|
+
end
|
81
|
+
|
82
|
+
# Twirp clients should not follow redirects automatically, Twirp only handles
|
83
|
+
# POST requests, redirects should only happen on GET and HEAD requests.
|
84
|
+
def twirp_redirect_error(status, location)
|
85
|
+
msg = "Unexpected HTTP Redirect from location=#{location}"
|
86
|
+
Twirp::Error.new(:internal, msg, {
|
87
|
+
http_error_from_intermediary: "true",
|
88
|
+
not_a_twirp_error_because: "Redirects not allowed on Twirp requests",
|
89
|
+
status_code: status.to_s,
|
90
|
+
location: location.to_s,
|
91
|
+
})
|
92
|
+
end
|
93
|
+
|
94
|
+
def is_http_redirect?(status)
|
95
|
+
status >= 300 && status <= 399
|
96
|
+
end
|
97
|
+
|
98
|
+
def rpc_path(service_full_name, rpc_method)
|
99
|
+
"/#{service_full_name}/#{rpc_method}"
|
100
|
+
end
|
101
|
+
|
102
|
+
end # class << self
|
103
|
+
|
104
|
+
|
29
105
|
|
30
106
|
# Init with a Faraday connection, or a base_url that is used in a default connection.
|
31
107
|
# Clients use Content-Type="application/protobuf" by default. For JSON clinets use :content_type => "application/json".
|
@@ -48,10 +124,6 @@ module Twirp
|
|
48
124
|
end
|
49
125
|
end
|
50
126
|
|
51
|
-
def rpc_path(rpc_method)
|
52
|
-
"/#{@service_full_name}/#{rpc_method}"
|
53
|
-
end
|
54
|
-
|
55
127
|
# Make a remote procedure call to a defined rpc_method. The input can be a Proto message instance,
|
56
128
|
# or the attributes (Hash) to instantiate it. Returns a ClientResp instance with an instance of
|
57
129
|
# output_class, or a Twirp::Error. The input and output classes are the ones configued with the rpc DSL.
|
@@ -66,13 +138,13 @@ module Twirp
|
|
66
138
|
body = Encoding.encode(input, rpcdef[:input_class], @content_type)
|
67
139
|
|
68
140
|
resp = @conn.post do |r|
|
69
|
-
r.url
|
141
|
+
r.url "/#{@service_full_name}/#{rpc_method}"
|
70
142
|
r.headers['Content-Type'] = @content_type
|
71
143
|
r.body = body
|
72
144
|
end
|
73
145
|
|
74
146
|
if resp.status != 200
|
75
|
-
return ClientResp.new(nil, error_from_response(resp))
|
147
|
+
return ClientResp.new(nil, self.class.error_from_response(resp))
|
76
148
|
end
|
77
149
|
|
78
150
|
if resp.headers['Content-Type'] != @content_type
|
@@ -91,83 +163,18 @@ module Twirp
|
|
91
163
|
body = Encoding.encode_json(attrs)
|
92
164
|
|
93
165
|
resp = @conn.post do |r|
|
94
|
-
r.url
|
166
|
+
r.url "/#{@service_full_name}/#{rpc_method}"
|
95
167
|
r.headers['Content-Type'] = Encoding::JSON
|
96
168
|
r.body = body
|
97
169
|
end
|
98
170
|
|
99
171
|
if resp.status != 200
|
100
|
-
return ClientResp.new(nil, error_from_response(resp))
|
172
|
+
return ClientResp.new(nil, self.class.error_from_response(resp))
|
101
173
|
end
|
102
174
|
|
103
175
|
data = Encoding.decode_json(resp.body)
|
104
176
|
return ClientResp.new(data, nil)
|
105
177
|
end
|
106
|
-
|
107
|
-
def error_from_response(resp)
|
108
|
-
status = resp.status
|
109
|
-
|
110
|
-
if is_http_redirect? status
|
111
|
-
return twirp_redirect_error(status, resp.headers['Location'])
|
112
|
-
end
|
113
|
-
|
114
|
-
err_attrs = nil
|
115
|
-
begin
|
116
|
-
err_attrs = JSON.parse(resp.body)
|
117
|
-
rescue JSON::ParserError
|
118
|
-
return twirp_error_from_intermediary(status, "Response is not JSON", resp.body)
|
119
|
-
end
|
120
|
-
|
121
|
-
code = err_attrs["code"]
|
122
|
-
if code.to_s.empty?
|
123
|
-
return twirp_error_from_intermediary(status, "Response is JSON but it has no \"code\" attribute", resp.body)
|
124
|
-
end
|
125
|
-
code = code.to_s.to_sym
|
126
|
-
if !Twirp::Error.valid_code?(code)
|
127
|
-
return Twirp::Error.internal("Invalid Twirp error code: #{code}", invalid_code: code.to_s, body: resp.body)
|
128
|
-
end
|
129
|
-
|
130
|
-
Twirp::Error.new(code, err_attrs["msg"], err_attrs["meta"])
|
131
|
-
end
|
132
|
-
|
133
|
-
# Error that was caused by an intermediary proxy like a load balancer.
|
134
|
-
# The HTTP errors code from non-twirp sources is mapped to equivalent twirp errors.
|
135
|
-
# The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md.
|
136
|
-
# Returned twirp Errors have some additional metadata for inspection.
|
137
|
-
def twirp_error_from_intermediary(status, reason, body)
|
138
|
-
code = case status
|
139
|
-
when 400 then :internal
|
140
|
-
when 401 then :unauthenticated
|
141
|
-
when 403 then :permission_denied
|
142
|
-
when 404 then :bad_route
|
143
|
-
when 429, 502, 503, 504 then :unavailable
|
144
|
-
else :unknown
|
145
|
-
end
|
146
|
-
|
147
|
-
Twirp::Error.new(code, code.to_s, {
|
148
|
-
http_error_from_intermediary: "true",
|
149
|
-
not_a_twirp_error_because: reason,
|
150
|
-
status_code: status.to_s,
|
151
|
-
body: body.to_s,
|
152
|
-
})
|
153
|
-
end
|
154
|
-
|
155
|
-
# Twirp clients should not follow redirects automatically, Twirp only handles
|
156
|
-
# POST requests, redirects should only happen on GET and HEAD requests.
|
157
|
-
def twirp_redirect_error(status, location)
|
158
|
-
msg = "Unexpected HTTP Redirect from location=#{location}"
|
159
|
-
Twirp::Error.new(:internal, msg, {
|
160
|
-
http_error_from_intermediary: "true",
|
161
|
-
not_a_twirp_error_because: "Redirects not allowed on Twirp requests",
|
162
|
-
status_code: status.to_s,
|
163
|
-
location: location.to_s,
|
164
|
-
})
|
165
|
-
end
|
166
|
-
|
167
|
-
def is_http_redirect?(status)
|
168
|
-
status >= 300 && status <= 399
|
169
|
-
end
|
170
|
-
|
171
178
|
end
|
172
179
|
|
173
180
|
class ClientResp
|
data/lib/twirp/version.rb
CHANGED
data/test/client_test.rb
CHANGED
@@ -27,6 +27,28 @@ 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
|
34
|
+
|
35
|
+
# If one of the methods is being implemented through the DSL, the colision should be avoided
|
36
|
+
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)
|
39
|
+
|
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")]
|
43
|
+
})
|
44
|
+
resp = c.json(:Json, foo: "bar")
|
45
|
+
assert_equal "bar", resp.data["foo"]
|
46
|
+
|
47
|
+
# Adding any other rpc would work as expected
|
48
|
+
EmptyClient.rpc :Other, Example::Empty, Example::Empty, :ruby_method => :other
|
49
|
+
assert_equal num_mthds + 1, EmptyClient.instance_methods.size # new method added
|
50
|
+
end
|
51
|
+
|
30
52
|
|
31
53
|
# Call .rpc on Protobuf client
|
32
54
|
# ----------------------------
|