twirp 0.0.4 → 0.1.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 +111 -32
- data/example/gen/haberdasher_twirp.rb +1 -2
- data/example/main.rb +4 -3
- data/lib/twirp.rb +0 -1
- data/lib/twirp/error.rb +39 -55
- data/lib/twirp/service.rb +163 -110
- data/lib/twirp/version.rb +1 -1
- data/protoc-gen-twirp_ruby/main.go +3 -4
- data/test/error_test.rb +30 -13
- data/test/fake_services.rb +4 -12
- data/test/service_test.rb +641 -80
- metadata +2 -3
- data/lib/twirp/exception.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f3e14392e7046ad1e01f88323f3966e612dacc23
|
4
|
+
data.tar.gz: a12d8cc09c357bfd1501cb12594b83eacf2b0779
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cdf630358e9d36782f10f5069aa389fb97c588092cb6b2af89a72f5763b8d63f95d70d49d576bcf294de8ec495a71eb3f1ccbdd9cf53032d84108dccfdd5a7f1
|
7
|
+
data.tar.gz: c33a3b5aee715484475bffef8f325753d001a7c20ea69fbb180ffd4eb80db1123a6dd4c8f815905bf45c2a60fc3394b2965fdaa16a3d28696d1408b9d57a4104
|
data/README.md
CHANGED
@@ -4,85 +4,164 @@ Twirp services and clients in Ruby.
|
|
4
4
|
|
5
5
|
### Installation
|
6
6
|
Install the `twirp` gem:
|
7
|
+
|
7
8
|
```sh
|
8
9
|
➜ gem install twirp
|
9
10
|
```
|
10
11
|
|
11
12
|
Use `go get` to install the ruby_twirp protoc plugin:
|
13
|
+
|
12
14
|
```sh
|
13
15
|
➜ go get github.com/cyrusaf/ruby-twirp/protoc-gen-twirp_ruby
|
14
16
|
```
|
15
17
|
|
16
18
|
You will also need:
|
19
|
+
|
17
20
|
- [protoc](https://github.com/golang/protobuf), the protobuf compiler. You need
|
18
21
|
version 3+.
|
19
22
|
|
20
|
-
###
|
23
|
+
### HelloWorld Example
|
24
|
+
|
21
25
|
See the `example/` folder for the final product.
|
22
26
|
|
23
27
|
First create a basic `.proto` file:
|
28
|
+
|
24
29
|
```protobuf
|
25
|
-
// haberdasher.proto
|
26
30
|
syntax = "proto3";
|
27
31
|
package example;
|
28
32
|
|
29
|
-
service
|
30
|
-
rpc
|
33
|
+
service HelloWorld {
|
34
|
+
rpc Hello(HelloRequest) returns (HelloResponse);
|
31
35
|
}
|
32
36
|
|
33
|
-
message
|
37
|
+
message HelloRequest {
|
34
38
|
string name = 1;
|
35
39
|
}
|
36
40
|
|
37
|
-
message
|
41
|
+
message HelloResponse {
|
38
42
|
string message = 1;
|
39
43
|
}
|
40
|
-
|
41
44
|
```
|
42
45
|
|
43
|
-
Run the `protoc` binary to generate `
|
46
|
+
Run the `protoc` binary to auto-generate `helloworld_pb.rb` and `haberdasher_twirp.rb` files:
|
47
|
+
|
44
48
|
```sh
|
45
49
|
➜ protoc --proto_path=. ./haberdasher.proto --ruby_out=gen --twirp_ruby_out=gen
|
46
50
|
```
|
47
51
|
|
48
|
-
Write
|
52
|
+
Write a handler for the auto-generated service, this is your implementation:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
class HellowWorldHandler
|
56
|
+
def hello(input, env)
|
57
|
+
{message: "Hello #{input.name}"}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
Initialize the service with your handler and mount it as a Rack app:
|
63
|
+
|
49
64
|
```ruby
|
50
|
-
# main.rb
|
51
65
|
require 'rack'
|
52
66
|
require_relative 'gen/haberdasher_pb.rb'
|
53
67
|
require_relative 'gen/haberdasher_twirp.rb'
|
54
68
|
|
55
|
-
|
56
|
-
|
57
|
-
return {message: "Hello #{req.name}"}
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
handler = HaberdasherHandler.new()
|
62
|
-
service = Example::HaberdasherService.new(handler)
|
69
|
+
handler = HellowWorldHandler.new()
|
70
|
+
service = Example::HelloWorld.new(handler)
|
63
71
|
Rack::Handler::WEBrick.run service
|
64
72
|
```
|
65
73
|
|
66
|
-
You can also mount onto a rails
|
74
|
+
You can also mount onto a rails app:
|
75
|
+
|
67
76
|
```ruby
|
68
77
|
App::Application.routes.draw do
|
69
|
-
|
70
|
-
service = Example::HaberdasherService.new(handler)
|
71
|
-
mount service, at: HaberdasherService::PATH_PREFIX
|
78
|
+
mount service, at: service.full_name
|
72
79
|
end
|
73
80
|
```
|
74
81
|
|
75
|
-
|
76
|
-
```sh
|
77
|
-
➜ ruby main.rb
|
78
|
-
```
|
82
|
+
Twirp services accept both Protobuf and JSON messages. It is easy to `curl` your service to get a response:
|
79
83
|
|
80
|
-
`curl` your server to get a response:
|
81
84
|
```sh
|
82
|
-
|
83
|
-
--url http://localhost:8080/
|
85
|
+
curl --request POST \
|
86
|
+
--url http://localhost:8080/example.HelloWorld/Hello \
|
84
87
|
--header 'content-type: application/json' \
|
85
|
-
--data '{
|
86
|
-
|
87
|
-
|
88
|
+
--data '{"name":"World"}'
|
89
|
+
```
|
90
|
+
|
91
|
+
|
92
|
+
## Hooks
|
93
|
+
|
94
|
+
In the lifecycle of a request, the Twirp service starts by routing the request to a valid
|
95
|
+
RPC method. If routing fails, the `on_error` hook is called with a bad_route error.
|
96
|
+
If routing succeeds, the `before` hook is called before calling the RPC method handler,
|
97
|
+
and then either `on_success` or `on_error` depending if the response is a Twirp error or not.
|
98
|
+
|
99
|
+
```
|
100
|
+
routing -> before -> handler -> on_success
|
101
|
+
-> on_error
|
102
|
+
```
|
103
|
+
|
104
|
+
On every request, one and only one of `on_success` or `on_error` is called.
|
105
|
+
|
106
|
+
|
107
|
+
If exceptions are raised, the `exception_raised` hook is called. The exceptioni is wrapped with
|
108
|
+
an internal Twirp error, and if the `on_error` hook was not called yet, then it is called with
|
109
|
+
the wrapped exception.
|
110
|
+
|
111
|
+
|
112
|
+
```
|
113
|
+
routing -> before -> handler
|
114
|
+
! exception_raised -> on_error
|
115
|
+
```
|
116
|
+
|
117
|
+
Example code with hooks:
|
118
|
+
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class HaberdasherHandler
|
122
|
+
def make_hat(size, env)
|
123
|
+
return {}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
handler = HaberdasherHandler.new
|
128
|
+
svc = Example::Haberdasher.new(handler)
|
129
|
+
|
130
|
+
|
131
|
+
svc.before do |rack_env, env|
|
132
|
+
# Runs if properly routed to an rpc method, but before calling the method handler.
|
133
|
+
# This is the only place to read the Rack Env to access http request and middleware data.
|
134
|
+
# The Twirp env has the same routing info as in the handler method, e.g. :rpc_method, :input and :input_class.
|
135
|
+
# If it returns a Twirp::Error, the handler is not called and this error is returned instead.
|
136
|
+
# If an exception is raised, the exception_raised hook will be called and then on_error with the internal error.
|
137
|
+
end
|
138
|
+
|
139
|
+
svc.on_success do |env|
|
140
|
+
# Runs after the rpc method is handled, if it didn't return Twirp errors or raised exceptions.
|
141
|
+
# The env[:output] contains the serialized message of class env[:ouput_class]
|
142
|
+
# Returned values are ignored (even if it returns Twirp::Error).
|
143
|
+
# Exceptions should not happen here, but if an exception is raised the exception_raised hook will be
|
144
|
+
# called, however on_error will not (either on_success or on_error are called once per request).
|
145
|
+
end
|
146
|
+
|
147
|
+
svc.on_error do |twerr, env|
|
148
|
+
# Runs on error responses, that is:
|
149
|
+
# * routing errors (env does not have routing info here)
|
150
|
+
# * before filters returning Twirp errors or raising exceptions.
|
151
|
+
# * hander methods returning Twirp errors or raising exceptions.
|
152
|
+
# Raised exceptions are wrapped with Twirp::Error.internal_with(e).
|
153
|
+
# Returned values are ignored (even if it returns Twirp::Error).
|
154
|
+
# Exceptions should not happen here, but if an exception is raised the exception_raised hook will be
|
155
|
+
# called without calling on_error again later.
|
156
|
+
end
|
157
|
+
|
158
|
+
svc.exception_raised do |e, env|
|
159
|
+
# Runs if an exception was raised from the handler or any of the hooks.
|
160
|
+
environment = (ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym
|
161
|
+
case environment
|
162
|
+
when :development raise e
|
163
|
+
when :test
|
164
|
+
puts "[Error] #{e}\n#{e.backtrace.join("\n")}"
|
165
|
+
end
|
166
|
+
end
|
88
167
|
```
|
@@ -3,9 +3,8 @@ require 'twirp'
|
|
3
3
|
|
4
4
|
module Example
|
5
5
|
class HaberdasherService < Twirp::Service
|
6
|
-
PATH_PREFIX = "/twirp/example.Haberdasher"
|
7
6
|
package "example"
|
8
7
|
service "Haberdasher"
|
9
|
-
rpc :HelloWorld, HelloWorldRequest, HelloWorldResponse, handler_method:
|
8
|
+
rpc :HelloWorld, HelloWorldRequest, HelloWorldResponse, :handler_method => :hello_world
|
10
9
|
end
|
11
10
|
end
|
data/example/main.rb
CHANGED
@@ -3,11 +3,12 @@ require_relative 'gen/haberdasher_pb.rb'
|
|
3
3
|
require_relative 'gen/haberdasher_twirp.rb'
|
4
4
|
|
5
5
|
class HaberdasherHandler
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
def hello_world(req, env)
|
7
|
+
{message: "Hello #{req.name}"}
|
8
|
+
end
|
9
9
|
end
|
10
10
|
|
11
11
|
handler = HaberdasherHandler.new()
|
12
12
|
service = Example::HaberdasherService.new(handler)
|
13
|
+
|
13
14
|
Rack::Handler::WEBrick.run service
|
data/lib/twirp.rb
CHANGED
data/lib/twirp/error.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'json'
|
2
|
-
|
3
1
|
module Twirp
|
4
2
|
|
5
3
|
# Valid Twirp error codes and their mapping to related HTTP status.
|
@@ -28,90 +26,76 @@ module Twirp
|
|
28
26
|
# List of all valid error codes in Twirp
|
29
27
|
ERROR_CODES = ERROR_CODES_TO_HTTP_STATUS.keys
|
30
28
|
|
31
|
-
|
29
|
+
def valid_error_code?(code)
|
30
|
+
ERROR_CODES_TO_HTTP_STATUS.key? code # one of the valid symbols
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# Twirp::Error represents an error response from a Twirp service.
|
35
|
+
# Twirp::Error is not an Exception to be raised, but a value to be returned
|
36
|
+
# by service handlers and received by clients.
|
32
37
|
class Error
|
33
38
|
|
34
|
-
#
|
35
|
-
|
36
|
-
|
39
|
+
# Use this constructors to ensure the errors have valid error codes. Example:
|
40
|
+
# Twirp::Error.internal("boom")
|
41
|
+
# Twirp::Error.invalid_argument("foo is mandatory", argument: "foo")
|
42
|
+
# Twirp::Error.permission_denied("thou shall not pass!", target: "Balrog")
|
43
|
+
ERROR_CODES.each do |code|
|
44
|
+
define_singleton_method code do |msg, meta=nil|
|
45
|
+
new(code, msg, meta)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Wrap another error as a Twirp::Error :internal.
|
50
|
+
def self.internal_with(err)
|
51
|
+
twerr = internal err.message, cause: err.class.name
|
52
|
+
twerr.cause = err
|
53
|
+
twerr
|
37
54
|
end
|
38
55
|
|
56
|
+
attr_reader :code, :msg, :meta
|
57
|
+
|
58
|
+
attr_accessor :cause # used when wrapping another error, but this is not serialized
|
59
|
+
|
39
60
|
# Initialize a Twirp::Error
|
40
|
-
# The code
|
61
|
+
# The code must be one of the valid ERROR_CODES Symbols (e.g. :internal, :not_found, :permission_denied ...).
|
41
62
|
# The msg is a String with the error message.
|
42
|
-
# The meta is optional error metadata, if included it
|
63
|
+
# The meta is optional error metadata, if included it must be a Hash with String values.
|
43
64
|
def initialize(code, msg, meta=nil)
|
44
|
-
@code =
|
65
|
+
@code = code.to_sym
|
45
66
|
@msg = msg.to_s
|
46
67
|
@meta = validate_meta(meta)
|
47
68
|
end
|
48
69
|
|
49
|
-
|
50
|
-
attr_reader :msg
|
51
|
-
|
52
|
-
def meta(key)
|
53
|
-
@meta ||= {}
|
54
|
-
@meta[key]
|
55
|
-
end
|
56
|
-
|
57
|
-
def add_meta(key, value)
|
58
|
-
validate_meta_key_value(key, value)
|
59
|
-
@meta ||= {}
|
60
|
-
@meta[key] = value
|
61
|
-
end
|
62
|
-
|
63
|
-
def delete_meta(key)
|
64
|
-
@meta ||= {}
|
65
|
-
@meta.delete(key.to_s)
|
66
|
-
@meta = nil if @meta.size == 0
|
67
|
-
end
|
68
|
-
|
69
|
-
def as_json
|
70
|
+
def to_h
|
70
71
|
h = {
|
71
72
|
code: @code,
|
72
73
|
msg: @msg,
|
73
74
|
}
|
74
|
-
h[:meta] = @meta
|
75
|
+
h[:meta] = @meta unless @meta.empty?
|
75
76
|
h
|
76
77
|
end
|
77
78
|
|
78
|
-
def to_json
|
79
|
-
JSON.generate(as_json)
|
80
|
-
end
|
81
|
-
|
82
79
|
def to_s
|
83
|
-
"Twirp::Error code:#{code} msg:#{msg.inspect} meta:#{meta.inspect}"
|
80
|
+
"<Twirp::Error code:#{code.inspect} msg:#{msg.inspect} meta:#{meta.inspect}>"
|
84
81
|
end
|
85
82
|
|
86
|
-
private
|
87
83
|
|
88
|
-
|
89
|
-
if !code.is_a? Symbol
|
90
|
-
raise ArgumentError.new("Twirp::Error code must be a Symbol, but it is a #{code.class.to_s}")
|
91
|
-
end
|
92
|
-
if !ERROR_CODES_TO_HTTP_STATUS.has_key? code
|
93
|
-
raise ArgumentError.new("Twirp::Error code :#{code} is invalid. Expected one of #{ERROR_CODES.inspect}")
|
94
|
-
end
|
95
|
-
code
|
96
|
-
end
|
84
|
+
private
|
97
85
|
|
98
86
|
def validate_meta(meta)
|
99
|
-
return
|
87
|
+
return {} if !meta
|
88
|
+
|
100
89
|
if !meta.is_a? Hash
|
101
90
|
raise ArgumentError.new("Twirp::Error meta must be a Hash, but it is a #{meta.class.to_s}")
|
102
91
|
end
|
103
92
|
meta.each do |key, value|
|
104
|
-
|
93
|
+
if !value.is_a?(String)
|
94
|
+
raise ArgumentError.new("Twirp::Error meta values must be Strings, but key #{key.inspect} has the value <#{value.class.to_s}> #{value.inspect}")
|
95
|
+
end
|
105
96
|
end
|
106
97
|
meta
|
107
98
|
end
|
108
99
|
|
109
|
-
def validate_meta_key_value(key, value)
|
110
|
-
if !key.is_a?(String) || !value.is_a?(String)
|
111
|
-
raise ArgumentError.new("Twirp::Error meta must be a Hash with String keys and values. Invalid key<#{key.class}>: #{key.inspect}, value<#{value.class}>: #{value.inspect}")
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
100
|
end
|
116
|
-
|
117
101
|
end
|
data/lib/twirp/service.rb
CHANGED
@@ -1,35 +1,33 @@
|
|
1
|
+
require "json"
|
2
|
+
|
1
3
|
module Twirp
|
4
|
+
|
2
5
|
class Service
|
3
6
|
|
4
7
|
class << self
|
5
8
|
|
6
9
|
# Configure service package name.
|
7
|
-
def package(
|
8
|
-
@package_name =
|
10
|
+
def package(name)
|
11
|
+
@package_name = name.to_s
|
9
12
|
end
|
10
13
|
|
11
14
|
# Configure service name.
|
12
|
-
def service(
|
13
|
-
@service_name =
|
15
|
+
def service(name)
|
16
|
+
@service_name = name.to_s
|
14
17
|
end
|
15
18
|
|
16
19
|
# Configure service routing to handle rpc calls.
|
17
|
-
def rpc(
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
@rpcs ||= {}
|
29
|
-
@rpcs[method_name.to_s] = {
|
30
|
-
request_class: request_class,
|
31
|
-
response_class: response_class,
|
32
|
-
handler_method: opts[:handler_method],
|
20
|
+
def rpc(rpc_method, input_class, output_class, opts)
|
21
|
+
raise ArgumentError.new("input_class must be a Protobuf Message class") unless input_class.is_a?(Class)
|
22
|
+
raise ArgumentError.new("output_class must be a Protobuf Message class") unless output_class.is_a?(Class)
|
23
|
+
raise ArgumentError.new("opts[:handler_method] is mandatory") unless opts && opts[:handler_method]
|
24
|
+
|
25
|
+
@base_envs ||= {}
|
26
|
+
@base_envs[rpc_method.to_s] = {
|
27
|
+
rpc_method: rpc_method.to_sym,
|
28
|
+
input_class: input_class,
|
29
|
+
output_class: output_class,
|
30
|
+
handler_method: opts[:handler_method].to_sym,
|
33
31
|
}
|
34
32
|
end
|
35
33
|
|
@@ -39,150 +37,205 @@ module Twirp
|
|
39
37
|
@package_name.to_s
|
40
38
|
end
|
41
39
|
|
42
|
-
#
|
43
|
-
#
|
40
|
+
# Service name as String.
|
41
|
+
# Defaults to the current class name.
|
44
42
|
def service_name
|
45
|
-
|
46
|
-
sname.empty? ? self.name : sname
|
47
|
-
end
|
48
|
-
|
49
|
-
# Get configured metadata for rpc methods.
|
50
|
-
def rpcs
|
51
|
-
@rpcs || {}
|
43
|
+
(@service_name || self.name).to_s
|
52
44
|
end
|
53
45
|
|
54
|
-
#
|
55
|
-
|
56
|
-
|
57
|
-
def path_prefix
|
58
|
-
"/twirp/#{service_full_name}" # e.g. "twirp/Haberdasher"
|
46
|
+
# Base Twirp environments for each rpc method.
|
47
|
+
def base_envs
|
48
|
+
@base_envs || {}
|
59
49
|
end
|
60
50
|
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
51
|
+
# Package and servicce name, as a unique identifier for the service,
|
52
|
+
# for example "example.v3.Haberdasher" (package "example.v3", service "Haberdasher").
|
53
|
+
# This can be used as a path prefix to route requests to the service, because a Twirp URL is:
|
54
|
+
# "#{BaseURL}/#{ServiceFullName}/#{Method]"
|
64
55
|
def service_full_name
|
65
56
|
package_name.empty? ? service_name : "#{package_name}.#{service_name}"
|
66
57
|
end
|
67
58
|
|
59
|
+
# Raise exceptions instead of handling them with exception_raised hooks.
|
60
|
+
# Useful during tests to easily debug and catch unexpected exceptions.
|
61
|
+
# Default false.
|
62
|
+
attr_accessor :raise_exceptions
|
63
|
+
|
68
64
|
end # class << self
|
69
65
|
|
70
66
|
|
71
|
-
# Instantiate a new service with a handler.
|
72
|
-
# The handler must implemnt all rpc methods required by this service.
|
73
67
|
def initialize(handler)
|
74
|
-
self.class.rpcs.each do |method_name, rpc|
|
75
|
-
if !handler.respond_to? rpc[:handler_method]
|
76
|
-
raise ArgumentError.new("Handler must respond to .#{rpc[:handler_method]}(input) in order to handle the message #{method_name}.")
|
77
|
-
end
|
78
|
-
end
|
79
68
|
@handler = handler
|
69
|
+
|
70
|
+
@before = []
|
71
|
+
@on_success = []
|
72
|
+
@on_error = []
|
73
|
+
@exception_raised = []
|
74
|
+
end
|
75
|
+
|
76
|
+
def name
|
77
|
+
self.class.service_name
|
80
78
|
end
|
81
|
-
|
82
|
-
def
|
83
|
-
#
|
79
|
+
|
80
|
+
def full_name
|
81
|
+
self.class.service_full_name # use to route requests to this servie
|
84
82
|
end
|
85
83
|
|
84
|
+
# Setup hook blocks
|
85
|
+
def before(&block) @before << block; end
|
86
|
+
def on_success(&block) @on_success << block; end
|
87
|
+
def on_error(&block) @on_error << block; end
|
88
|
+
def exception_raised(&block) @exception_raised << block; end
|
89
|
+
|
86
90
|
# Rack app handler.
|
87
|
-
def call(
|
88
|
-
req = Rack::Request.new(env)
|
89
|
-
rpc, content_type, bad_route = parse_rack_request(req)
|
90
|
-
if bad_route
|
91
|
-
return error_response(bad_route)
|
92
|
-
end
|
93
|
-
|
94
|
-
proto_req = decode_request(rpc[:request_class], content_type, req.body.read)
|
91
|
+
def call(rack_env)
|
95
92
|
begin
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
93
|
+
env = {}
|
94
|
+
bad_route = route_request(rack_env, env)
|
95
|
+
return error_response(bad_route, env) if bad_route
|
96
|
+
|
97
|
+
@before.each do |hook|
|
98
|
+
result = hook.call(rack_env, env)
|
99
|
+
return error_response(result, env) if result.is_a? Twirp::Error
|
100
|
+
end
|
101
|
+
|
102
|
+
output = call_handler(env)
|
103
|
+
return error_response(output, env) if output.is_a? Twirp::Error
|
104
|
+
return success_response(output, env)
|
105
|
+
|
106
|
+
rescue => e
|
107
|
+
raise e if self.class.raise_exceptions
|
108
|
+
begin
|
109
|
+
@exception_raised.each{|hook| hook.call(e, env) }
|
110
|
+
rescue => hook_e
|
111
|
+
e = hook_e
|
112
|
+
end
|
113
|
+
|
114
|
+
twerr = Twirp::Error.internal_with(e)
|
115
|
+
return error_response(twerr, env)
|
100
116
|
end
|
101
117
|
end
|
102
118
|
|
103
|
-
def path_prefix
|
104
|
-
self.class.path_prefix
|
105
|
-
end
|
106
|
-
|
107
|
-
def service_full_name
|
108
|
-
self.class.service_full_name
|
109
|
-
end
|
110
119
|
|
111
120
|
private
|
112
121
|
|
113
|
-
|
114
|
-
|
115
|
-
|
122
|
+
# Parse request and fill env with rpc data.
|
123
|
+
# Returns a bad_route error if something went wrong.
|
124
|
+
def route_request(rack_env, env)
|
125
|
+
rack_request = Rack::Request.new(rack_env)
|
126
|
+
|
127
|
+
if rack_request.request_method != "POST"
|
128
|
+
return bad_route_error("HTTP request method must be POST", rack_request)
|
116
129
|
end
|
117
130
|
|
118
|
-
content_type =
|
131
|
+
content_type = rack_request.get_header("CONTENT_TYPE")
|
119
132
|
if content_type != "application/json" && content_type != "application/protobuf"
|
120
|
-
return
|
133
|
+
return bad_route_error("unexpected Content-Type: #{content_type.inspect}. Content-Type header must be one of application/json or application/protobuf", rack_request)
|
121
134
|
end
|
135
|
+
env[:content_type] = content_type
|
122
136
|
|
123
|
-
path_parts =
|
124
|
-
if path_parts.size <
|
125
|
-
return
|
137
|
+
path_parts = rack_request.fullpath.split("/")
|
138
|
+
if path_parts.size < 3 || path_parts[-2] != self.full_name
|
139
|
+
return bad_route_error("Invalid route. Expected format: POST {BaseURL}/#{self.full_name}/{Method}", rack_request)
|
126
140
|
end
|
127
141
|
method_name = path_parts[-1]
|
128
142
|
|
129
|
-
|
130
|
-
if !
|
131
|
-
return
|
143
|
+
base_env = self.class.base_envs[method_name]
|
144
|
+
if !base_env
|
145
|
+
return bad_route_error("Invalid rpc method #{method_name.inspect}", rack_request)
|
146
|
+
end
|
147
|
+
env.merge!(base_env) # :rpc_method, :input_class, :output_class, :handler_method
|
148
|
+
|
149
|
+
input = nil
|
150
|
+
begin
|
151
|
+
input = decode_input(rack_request.body.read, env[:input_class], content_type)
|
152
|
+
rescue => e
|
153
|
+
return bad_route_error("Invalid request body for rpc method #{method_name.inspect} with Content-Type=#{content_type}", rack_request)
|
132
154
|
end
|
133
155
|
|
134
|
-
|
156
|
+
env[:input] = input
|
157
|
+
env[:http_response_headers] = {}
|
158
|
+
return
|
159
|
+
end
|
160
|
+
|
161
|
+
def bad_route_error(msg, req)
|
162
|
+
Twirp::Error.bad_route msg, twirp_invalid_route: "#{req.request_method} #{req.fullpath}"
|
135
163
|
end
|
136
164
|
|
137
|
-
def
|
138
|
-
|
139
|
-
|
165
|
+
def decode_input(body, input_class, content_type)
|
166
|
+
case content_type
|
167
|
+
when "application/protobuf" then input_class.decode(body)
|
168
|
+
when "application/json" then input_class.decode_json(body)
|
140
169
|
end
|
170
|
+
end
|
141
171
|
|
142
|
-
|
143
|
-
|
172
|
+
def encode_output(output, output_class, content_type)
|
173
|
+
case content_type
|
174
|
+
when "application/protobuf" then output_class.encode(output)
|
175
|
+
when "application/json" then output_class.encode_json(output)
|
144
176
|
end
|
177
|
+
end
|
145
178
|
|
146
|
-
|
147
|
-
|
179
|
+
# Call handler method and return a Protobuf Message or a Twirp::Error.
|
180
|
+
def call_handler(env)
|
181
|
+
handler_method = env[:handler_method]
|
182
|
+
if !@handler.respond_to?(handler_method)
|
183
|
+
return Twirp::Error.unimplemented("Handler method #{handler_method} is not implemented.")
|
148
184
|
end
|
149
185
|
|
150
|
-
|
151
|
-
|
186
|
+
out = @handler.send(handler_method, env[:input], env)
|
187
|
+
case out
|
188
|
+
when env[:output_class], Twirp::Error
|
189
|
+
out
|
190
|
+
when Hash
|
191
|
+
env[:output_class].new(out)
|
192
|
+
else
|
193
|
+
Twirp::Error.internal("Handler method #{handler_method} expected to return one of #{env[:output_class].name}, Hash or Twirp::Error, but returned #{out.class.name}.")
|
194
|
+
end
|
152
195
|
end
|
153
196
|
|
154
|
-
def
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
197
|
+
def success_response(output, env)
|
198
|
+
begin
|
199
|
+
env[:output] = output
|
200
|
+
@on_success.each{|hook| hook.call(env) }
|
201
|
+
|
202
|
+
headers = env[:http_response_headers].merge('Content-Type' => env[:content_type])
|
203
|
+
resp_body = encode_output(output, env[:output_class], env[:content_type])
|
204
|
+
[200, headers, [resp_body]]
|
205
|
+
|
206
|
+
rescue => e
|
207
|
+
return exception_response(e, env)
|
160
208
|
end
|
161
209
|
end
|
162
210
|
|
163
|
-
def
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
211
|
+
def error_response(twerr, env)
|
212
|
+
begin
|
213
|
+
@on_error.each{|hook| hook.call(twerr, env) }
|
214
|
+
|
215
|
+
status = Twirp::ERROR_CODES_TO_HTTP_STATUS[twerr.code]
|
216
|
+
resp_body = JSON.generate(twerr.to_h)
|
217
|
+
[status, error_response_headers, [resp_body]]
|
218
|
+
|
219
|
+
rescue => e
|
220
|
+
return exception_response(e, env)
|
169
221
|
end
|
170
222
|
end
|
171
223
|
|
172
|
-
def
|
173
|
-
|
174
|
-
|
224
|
+
def exception_response(e, env)
|
225
|
+
raise e if self.class.raise_exceptions
|
226
|
+
begin
|
227
|
+
@exception_raised.each{|hook| hook.call(e, env) }
|
228
|
+
rescue => hook_e
|
229
|
+
e = hook_e
|
230
|
+
end
|
175
231
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
resp_body = twirp_error.to_json
|
180
|
-
[status, headers, [resp_body]]
|
232
|
+
twerr = Twirp::Error.internal_with(e)
|
233
|
+
resp_body = JSON.generate(twerr.to_h)
|
234
|
+
[500, error_response_headers, [resp_body]]
|
181
235
|
end
|
182
236
|
|
183
|
-
def
|
184
|
-
|
185
|
-
Twirp::Error.new(:bad_route, msg, "twirp_invalid_route" => meta_invalid_route)
|
237
|
+
def error_response_headers
|
238
|
+
{'Content-Type' => 'application/json'}
|
186
239
|
end
|
187
240
|
|
188
241
|
end
|