twirp 0.4.0 → 0.4.1
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 +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
|
# ----------------------------
|