twirp 0.0.4 → 0.1.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 +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
|