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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4d21a865d8176ee77a6651aabe59247ae763e1d5
4
- data.tar.gz: 8d16237130c4e966f97b98f7f529f6eb82814bf0
3
+ metadata.gz: f3e14392e7046ad1e01f88323f3966e612dacc23
4
+ data.tar.gz: a12d8cc09c357bfd1501cb12594b83eacf2b0779
5
5
  SHA512:
6
- metadata.gz: 2194339fba1ba02716da045f69f55e4fe7e6631f5d6eb1c1b38d7039ef913bbd08c41c919d0872d0240dae238100bb685c6b40a747133887d7494d23d5d0b3f2
7
- data.tar.gz: 595aab3aca050f20139ec2a1f5e8bff184fcfd771cc9fd008fd14056850228c79974d765da76c6c08e0e93e7a9c502ba76acfacdae7acff2020e561fe5f2d8a8
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
- ### Haberdasher Example
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 Haberdasher {
30
- rpc HelloWorld(HelloWorldRequest) returns (HelloWorldResponse);
33
+ service HelloWorld {
34
+ rpc Hello(HelloRequest) returns (HelloResponse);
31
35
  }
32
36
 
33
- message HelloWorldRequest {
37
+ message HelloRequest {
34
38
  string name = 1;
35
39
  }
36
40
 
37
- message HelloWorldResponse {
41
+ message HelloResponse {
38
42
  string message = 1;
39
43
  }
40
-
41
44
  ```
42
45
 
43
- Run the `protoc` binary to generate `gen/haberdasher_pb.rb` and `gen/haberdasher_twirp.rb`.
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 an implementation of our haberdasher service and attach to a rack server:
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
- class HaberdasherHandler
56
- def hello_world(req)
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 service:
74
+ You can also mount onto a rails app:
75
+
67
76
  ```ruby
68
77
  App::Application.routes.draw do
69
- handler = HaberdasherHandler.new()
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
- Run `ruby main.rb` to start the server on port 8080:
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
- curl --request POST \
83
- --url http://localhost:8080/twirp/examples.Haberdasher/HelloWorld \
85
+ curl --request POST \
86
+ --url http://localhost:8080/example.HelloWorld/Hello \
84
87
  --header 'content-type: application/json' \
85
- --data '{
86
- "name": "World"
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: 'hello_world'
8
+ rpc :HelloWorld, HelloWorldRequest, HelloWorldResponse, :handler_method => :hello_world
10
9
  end
11
10
  end
@@ -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
- def hello_world(req)
7
- return {message: "Hello #{req.name}"}
8
- end
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
@@ -1,4 +1,3 @@
1
1
  require_relative 'twirp/version'
2
2
  require_relative 'twirp/error'
3
- require_relative 'twirp/exception'
4
3
  require_relative 'twirp/service'
@@ -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
- # Twirp::Error represents a valid error from a Twirp service
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
- # Wrap an arbitrary error as a Twirp :internal
35
- def self.InternalWith(err)
36
- self.new :internal, err.message, "cause" => err.class.name
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 MUST be one of the valid ERROR_CODES Symbols (e.g. :internal, :not_found, :permission_denied ...).
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 MUST be a Hash with String keys and values.
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 = validate_code(code)
65
+ @code = code.to_sym
45
66
  @msg = msg.to_s
46
67
  @meta = validate_meta(meta)
47
68
  end
48
69
 
49
- attr_reader :code
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 if @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
- def validate_code(code)
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 nil if !meta
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
- validate_meta_key_value(key, value)
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
@@ -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(package_name)
8
- @package_name = package_name.to_s
10
+ def package(name)
11
+ @package_name = name.to_s
9
12
  end
10
13
 
11
14
  # Configure service name.
12
- def service(service_name)
13
- @service_name = service_name.to_s
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(method_name, request_class, response_class, opts)
18
- if !request_class.is_a?(Class)
19
- raise ArgumentError.new("request_class must be a Protobuf Message class")
20
- end
21
- if !response_class.is_a?(Class)
22
- raise ArgumentError.new("response_class must be a Protobuf Message class")
23
- end
24
- if !opts || !opts[:handler_method]
25
- raise ArgumentError.new("opts[:handler_method] is mandatory")
26
- end
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
- # Get configured service name as String.
43
- # If not configured, it defaults to the class name.
40
+ # Service name as String.
41
+ # Defaults to the current class name.
44
42
  def service_name
45
- sname = @service_name.to_s
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
- # Path prefix that should be used to route requests to this service.
55
- # It is based on the package and service name, in the expected Twirp URL format.
56
- # The full URL would be: {BaseURL}/path_prefix/{MethodName}.
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
- # Service full name uniquelly identifies the service.
62
- # It is the service name prefixed by the package name,
63
- # for example "my.package.Haberdasher", or "Haberdasher" (if no package).
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
- # Register a before hook (not implemented)
82
- def before(&block)
83
- # TODO... and also after hooks
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(env)
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
- resp = @handler.send(rpc[:handler_method], proto_req)
97
- return rack_response_from_handler(rpc, content_type, resp)
98
- rescue Twirp::Exception => twerr
99
- error_response(twerr)
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
- def parse_rack_request(req)
114
- if req.request_method != "POST"
115
- return nil, nil, bad_route_error("HTTP request method must be POST", req)
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 = req.env["CONTENT_TYPE"]
131
+ content_type = rack_request.get_header("CONTENT_TYPE")
119
132
  if content_type != "application/json" && content_type != "application/protobuf"
120
- return nil, nil, bad_route_error("unexpected Content-Type: #{content_type.inspect}. Content-Type header must be one of \"application/json\" or \"application/protobuf\"", req)
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 = req.fullpath.split("/")
124
- if path_parts.size < 4 || path_parts[-2] != self.service_full_name || path_parts[-3] != "twirp"
125
- return nil, nil, bad_route_error("Invalid route. Expected format: POST {BaseURL}/twirp/(package.)?{Service}/{Method}", req)
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
- rpc = self.class.rpcs[method_name]
130
- if !rpc
131
- return nil, nil, bad_route_error("rpc method not found: #{method_name.inspect}", req)
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
- return rpc, content_type, nil
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 rack_response_from_handler(rpc, content_type, resp)
138
- if resp.is_a? Twirp::Error
139
- return error_response(resp)
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
- if resp.is_a? Hash # allow handlers to return just the attributes
143
- resp = rpc[:response_class].new(resp)
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
- if !resp # allow handlers to return nil or false as a reponse with zero-values
147
- resp = rpc[:response_class].new
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
- encoded_resp = encode_response(rpc[:response_class], content_type, resp)
151
- success_response(content_type, encoded_resp)
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 decode_request(request_class, content_type, body)
155
- case content_type
156
- when "application/json"
157
- request_class.decode_json(body)
158
- when "application/protobuf"
159
- request_class.decode(body)
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 encode_response(response_class, content_type, resp)
164
- case content_type
165
- when "application/json"
166
- response_class.encode_json(resp)
167
- when "application/protobuf"
168
- response_class.encode(resp)
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 success_response(content_type, encoded_resp)
173
- [200, {'Content-Type' => content_type}, [encoded_resp]]
174
- end
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
- def error_response(twirp_error)
177
- status = Twirp::ERROR_CODES_TO_HTTP_STATUS[twirp_error.code]
178
- headers = {'Content-Type' => 'application/json'}
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 bad_route_error(msg, req)
184
- meta_invalid_route = "#{req.request_method} #{req.fullpath}"
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