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 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