twirp 0.0.3 → 0.0.4

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: 76e48aace902f0cec12c1055dd664f659fb9dcf3
4
- data.tar.gz: 792f3fcc5a62cbd2adfd9f3d714b246e96c63bca
3
+ metadata.gz: 4d21a865d8176ee77a6651aabe59247ae763e1d5
4
+ data.tar.gz: 8d16237130c4e966f97b98f7f529f6eb82814bf0
5
5
  SHA512:
6
- metadata.gz: ea48c85b317496979d87015964d714a95db49093d5a81183b50928fbcdb453f4469fdf819b907012b1094f692fc08d3892b89bf55d870791f8607eded50ca06b
7
- data.tar.gz: bb59b2e6077814fa06b9b29fee4473155408eb0930ddb4ca00a2e088534e4a32b64450eda3721445c26634d5ed36ead624b35544c2c8988f070ffc066a7bf8ad
6
+ metadata.gz: 2194339fba1ba02716da045f69f55e4fe7e6631f5d6eb1c1b38d7039ef913bbd08c41c919d0872d0240dae238100bb685c6b40a747133887d7494d23d5d0b3f2
7
+ data.tar.gz: 595aab3aca050f20139ec2a1f5e8bff184fcfd771cc9fd008fd14056850228c79974d765da76c6c08e0e93e7a9c502ba76acfacdae7acff2020e561fe5f2d8a8
data/README.md CHANGED
@@ -54,19 +54,21 @@ require_relative 'gen/haberdasher_twirp.rb'
54
54
 
55
55
  class HaberdasherHandler
56
56
  def hello_world(req)
57
- return Example::HelloWorldResponse.new(message: "Hello #{req.name}")
57
+ return {message: "Hello #{req.name}"}
58
58
  end
59
59
  end
60
60
 
61
61
  handler = HaberdasherHandler.new()
62
- Rack::Handler::WEBrick.run Example::HaberdasherService.new(handler)
62
+ service = Example::HaberdasherService.new(handler)
63
+ Rack::Handler::WEBrick.run service
63
64
  ```
64
65
 
65
66
  You can also mount onto a rails service:
66
67
  ```ruby
67
68
  App::Application.routes.draw do
68
69
  handler = HaberdasherHandler.new()
69
- mount Example::HaberdasherService.new(handler), at: HaberdasherService::PATH_PREFIX
70
+ service = Example::HaberdasherService.new(handler)
71
+ mount service, at: HaberdasherService::PATH_PREFIX
70
72
  end
71
73
  ```
72
74
 
data/example/main.rb CHANGED
@@ -4,7 +4,7 @@ require_relative 'gen/haberdasher_twirp.rb'
4
4
 
5
5
  class HaberdasherHandler
6
6
  def hello_world(req)
7
- return Example::HelloWorldResponse.new(message: "Hello #{req.name}")
7
+ return {message: "Hello #{req.name}"}
8
8
  end
9
9
  end
10
10
 
data/lib/twirp.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  require_relative 'twirp/version'
2
2
  require_relative 'twirp/error'
3
- require_relative 'twirp/service'
3
+ require_relative 'twirp/exception'
4
+ require_relative 'twirp/service'
data/lib/twirp/error.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'json'
2
+
1
3
  module Twirp
2
4
 
3
5
  # Valid Twirp error codes and their mapping to related HTTP status.
@@ -29,6 +31,11 @@ module Twirp
29
31
  # Twirp::Error represents a valid error from a Twirp service
30
32
  class Error
31
33
 
34
+ # Wrap an arbitrary error as a Twirp :internal
35
+ def self.InternalWith(err)
36
+ self.new :internal, err.message, "cause" => err.class.name
37
+ end
38
+
32
39
  # Initialize a Twirp::Error
33
40
  # The code MUST be one of the valid ERROR_CODES Symbols (e.g. :internal, :not_found, :permission_denied ...).
34
41
  # The msg is a String with the error message.
@@ -69,7 +76,11 @@ module Twirp
69
76
  end
70
77
 
71
78
  def to_json
72
- JSON.encode(as_json)
79
+ JSON.generate(as_json)
80
+ end
81
+
82
+ def to_s
83
+ "Twirp::Error code:#{code} msg:#{msg.inspect} meta:#{meta.inspect}"
73
84
  end
74
85
 
75
86
  private
@@ -97,7 +108,7 @@ module Twirp
97
108
 
98
109
  def validate_meta_key_value(key, value)
99
110
  if !key.is_a?(String) || !value.is_a?(String)
100
- raise ArgumentError.new("Twirp::Error meta must be a Hash with String keys and values")
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}")
101
112
  end
102
113
  end
103
114
 
@@ -0,0 +1,21 @@
1
+ require_relative "./error"
2
+
3
+ module Twirp
4
+
5
+ class Exception < StandardError
6
+
7
+ def initialize(code, msg, meta=nil)
8
+ @twerr = Twirp::Error.new(code, msg, meta)
9
+ end
10
+
11
+ def message
12
+ "#{code}: #{msg}"
13
+ end
14
+
15
+ def code; @twerr.code; end
16
+ def msg; @twerr.msg; end
17
+ def meta; @twerr.meta; end
18
+ def to_json; @twerr.to_json; end
19
+ def as_json; @twerr.as_json; end
20
+ end
21
+ end
data/lib/twirp/service.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  module Twirp
2
-
3
2
  class Service
4
3
 
5
4
  class << self
@@ -9,7 +8,7 @@ module Twirp
9
8
  @package_name = package_name.to_s
10
9
  end
11
10
 
12
- # Configure service name
11
+ # Configure service name.
13
12
  def service(service_name)
14
13
  @service_name = service_name.to_s
15
14
  end
@@ -35,107 +34,129 @@ module Twirp
35
34
  end
36
35
 
37
36
  # Get configured package name as String.
38
- # And empty value means that there's no package
37
+ # And empty value means that there's no package.
39
38
  def package_name
40
39
  @package_name.to_s
41
40
  end
42
41
 
43
42
  # Get configured service name as String.
44
- # If not configured, it defaults to the class name
43
+ # If not configured, it defaults to the class name.
45
44
  def service_name
46
45
  sname = @service_name.to_s
47
46
  sname.empty? ? self.name : sname
48
47
  end
49
48
 
50
- # Get configured metadata for rpc methods
49
+ # Get configured metadata for rpc methods.
51
50
  def rpcs
52
51
  @rpcs || {}
53
52
  end
54
53
 
55
54
  # Path prefix that should be used to route requests to this service.
56
55
  # It is based on the package and service name, in the expected Twirp URL format.
57
- # The full URL would be: {BaseURL}/path_prefix/{MethodName}
56
+ # The full URL would be: {BaseURL}/path_prefix/{MethodName}.
58
57
  def path_prefix
59
- if package_name.empty?
60
- service_name # e.g. "Haberdasher"
61
- else
62
- "#{package_name}.#{service_name}" # e.g. "the.haberdasher.pkg.Haberdasher"
63
- end
58
+ "/twirp/#{service_full_name}" # e.g. "twirp/Haberdasher"
59
+ end
60
+
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).
64
+ def service_full_name
65
+ package_name.empty? ? service_name : "#{package_name}.#{service_name}"
64
66
  end
65
- end
67
+
68
+ end # class << self
69
+
66
70
 
67
71
  # Instantiate a new service with a handler.
68
- # A handler implements each rpc method as a regular object method call.
72
+ # The handler must implemnt all rpc methods required by this service.
69
73
  def initialize(handler)
70
- # validate that the handler reponds to all expected methods
71
74
  self.class.rpcs.each do |method_name, rpc|
72
75
  if !handler.respond_to? rpc[:handler_method]
73
- raise ArgumentError.new("Handler must respond to .#{rpc[:handler_method]}(req) in order to handle the message #{method_name}.")
76
+ raise ArgumentError.new("Handler must respond to .#{rpc[:handler_method]}(input) in order to handle the message #{method_name}.")
74
77
  end
75
78
  end
76
-
77
79
  @handler = handler
78
80
  end
79
-
80
81
  # Register a before hook (not implemented)
81
82
  def before(&block)
82
83
  # TODO... and also after hooks
83
84
  end
84
85
 
85
- # A service instance is a Rack middleware block.
86
+ # Rack app handler.
86
87
  def call(env)
87
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)
95
+ 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)
100
+ end
101
+ end
102
+
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
88
110
 
111
+ private
112
+
113
+ def parse_rack_request(req)
89
114
  if req.request_method != "POST"
90
- return error_response(bad_route_error("Only POST method is allowed", req))
115
+ return nil, nil, bad_route_error("HTTP request method must be POST", req)
116
+ end
117
+
118
+ content_type = req.env["CONTENT_TYPE"]
119
+ 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)
91
121
  end
92
122
 
93
- method_name = req.fullpath.split("/").last
94
- rpc_method = self.class.rpcs[method_name]
95
- if !rpc_method
96
- return error_response(bad_route_error("rpc method not found: #{method_name.inspect}", req))
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)
97
126
  end
127
+ method_name = path_parts[-1]
98
128
 
99
- request_class = rpc_method[:request_class]
100
- response_class = rpc_method[:response_class]
101
-
102
- content_type = req.env["CONTENT_TYPE"]
103
- req_msg = decode_request(rpc_method[:request_class], content_type, req.body.read)
104
- if !req_msg
105
- return error_response(bad_route_error("unexpected Content-Type: #{content_type.inspect}", req))
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)
106
132
  end
107
133
 
108
- # Handle Twirp request
109
- # TODO: wrap with begin-rescue block
110
- resp_msg = @handler.send(rpc_method[:handler_method], req_msg)
134
+ return rpc, content_type, nil
135
+ end
111
136
 
112
- if resp_msg.is_a? Twirp::Error
113
- return error_response(resp_msg)
137
+ def rack_response_from_handler(rpc, content_type, resp)
138
+ if resp.is_a? Twirp::Error
139
+ return error_response(resp)
114
140
  end
115
141
 
116
- if resp_msg.is_a? Hash # allow handlers to respond with just the attributes
117
- resp_msg = response_class.new(resp_msg)
142
+ if resp.is_a? Hash # allow handlers to return just the attributes
143
+ resp = rpc[:response_class].new(resp)
118
144
  end
119
- encoded_resp = encode_response(response_class, content_type, resp_msg)
120
-
121
- return [200, {'Content-Type' => content_type}, [encoded_resp]]
122
145
 
123
- # TODO: add recue for any error in the method, wrap with Twith error
124
- end
146
+ if !resp # allow handlers to return nil or false as a reponse with zero-values
147
+ resp = rpc[:response_class].new
148
+ end
125
149
 
126
- # path prefix that can be used to mount this service
127
- def path_prefix
128
- self.class.path_prefix
150
+ encoded_resp = encode_response(rpc[:response_class], content_type, resp)
151
+ success_response(content_type, encoded_resp)
129
152
  end
130
153
 
131
- private
132
-
133
154
  def decode_request(request_class, content_type, body)
134
155
  case content_type
135
156
  when "application/json"
136
157
  request_class.decode_json(body)
137
158
  when "application/protobuf"
138
- request_type.decode(body)
159
+ request_class.decode(body)
139
160
  end
140
161
  end
141
162
 
@@ -148,6 +169,10 @@ module Twirp
148
169
  end
149
170
  end
150
171
 
172
+ def success_response(content_type, encoded_resp)
173
+ [200, {'Content-Type' => content_type}, [encoded_resp]]
174
+ end
175
+
151
176
  def error_response(twirp_error)
152
177
  status = Twirp::ERROR_CODES_TO_HTTP_STATUS[twirp_error.code]
153
178
  headers = {'Content-Type' => 'application/json'}
data/lib/twirp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Twirp
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -0,0 +1,58 @@
1
+ # Fake messages, services and hadndlers for tests.
2
+
3
+ require 'google/protobuf'
4
+ require_relative '../lib/twirp'
5
+
6
+ # Protobuf messages.
7
+ # An example of the result of the protoc ruby code generator.
8
+ Google::Protobuf::DescriptorPool.generated_pool.build do
9
+ add_message "example.Size" do
10
+ optional :inches, :int32, 1
11
+ end
12
+ add_message "example.Hat" do
13
+ optional :inches, :int32, 1
14
+ optional :color, :string, 2
15
+ end
16
+ add_message "example.Empty" do
17
+ end
18
+ end
19
+
20
+ module Example
21
+ Size = Google::Protobuf::DescriptorPool.generated_pool.lookup("example.Size").msgclass
22
+ Hat = Google::Protobuf::DescriptorPool.generated_pool.lookup("example.Hat").msgclass
23
+ Empty = Google::Protobuf::DescriptorPool.generated_pool.lookup("example.Empty").msgclass
24
+ end
25
+
26
+ # Twirp Service.
27
+ # An example of the result of the protoc twirp_ruby plugin code generator.
28
+ module Example
29
+ class Haberdasher < Twirp::Service
30
+ package "example"
31
+ service "Haberdasher"
32
+
33
+ rpc "MakeHat", Size, Hat, handler_method: :make_hat
34
+ end
35
+ end
36
+
37
+ # Example service handler.
38
+ # It would be provided by the developer as implementation for the service.
39
+ class HaberdasherHandler
40
+ # This fake can optionally be initialized with a block for the make_hat implementation.
41
+ def initialize(&block)
42
+ if block_given?
43
+ @make_hat_block = block
44
+ else # default implementation
45
+ @make_hat_block = Proc.new do |size|
46
+ {inches: size.inches, color: "white"}
47
+ end
48
+ end
49
+ end
50
+
51
+ def make_hat(size)
52
+ @make_hat_block.call(size)
53
+ end
54
+ end
55
+
56
+ # Twirp Service with no package and no rpc methods.
57
+ class EmptyService < Twirp::Service
58
+ end
data/test/service_test.rb CHANGED
@@ -1,105 +1,235 @@
1
1
  require 'minitest/autorun'
2
2
  require 'rack/mock'
3
-
4
3
  require 'google/protobuf'
5
- require_relative '../lib/twirp'
6
-
7
- # Protobuf messages.
8
- # This is what the protoc code generator would produce.
9
- Google::Protobuf::DescriptorPool.generated_pool.build do
10
- add_message "foopkg.DoFooRequest" do
11
- optional :foo, :string, 1
12
- end
13
- add_message "foopkg.DoFooResponse" do
14
- optional :bar, :string, 1
15
- end
16
- end
17
- module FooPkg
18
- DoFooRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("foopkg.DoFooRequest").msgclass
19
- DoFooResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("foopkg.DoFooResponse").msgclass
20
- end
21
-
22
- # Twirp Service.
23
- # This is wha the twirp_ruby protoc plugin code generator would produce.
24
- module FooPkg
25
- class FooService < Twirp::Service
26
- package "foopkg"
27
- service "FooService"
28
-
29
- rpc "DoFoo", DoFooRequest, DoFooResponse, handler_method: :do_foo
30
- end
31
- end
4
+ require 'json'
32
5
 
33
- # Example service handler.
34
- # This would be provided by the developer as implementation for the service.
35
- class FooHandler
36
- def do_foo(req)
37
- {bar: "Hello #{req.foo}"}
38
- end
39
- end
40
-
41
- # Twirp Service with no package and no rpc methods.
42
- class EmptyService < Twirp::Service
43
- end
6
+ require_relative '../lib/twirp'
7
+ require_relative './fake_services'
44
8
 
45
9
  class ServiceTest < Minitest::Test
46
10
 
11
+ # DSL rpc builds the proper rpc data on the service
47
12
  def test_rpc_methods
48
- # make sure that rpcs have been properly setup by the `rpc` DSL constructor
49
- assert_equal 1, FooPkg::FooService.rpcs.size
13
+ assert_equal 1, Example::Haberdasher.rpcs.size
50
14
  assert_equal({
51
- request_class: FooPkg::DoFooRequest,
52
- response_class: FooPkg::DoFooResponse,
53
- handler_method: :do_foo,
54
- }, FooPkg::FooService.rpcs["DoFoo"])
15
+ request_class: Example::Size,
16
+ response_class: Example::Hat,
17
+ handler_method: :make_hat,
18
+ }, Example::Haberdasher.rpcs["MakeHat"])
55
19
  end
56
20
 
21
+ # DSL package and service define the proper data on the service
57
22
  def test_package_service_getters
58
- assert_equal "foopkg", FooPkg::FooService.package_name
59
- assert_equal "FooService", FooPkg::FooService.service_name
60
- assert_equal "foopkg.FooService", FooPkg::FooService.path_prefix
23
+ assert_equal "example", Example::Haberdasher.package_name
24
+ assert_equal "Haberdasher", Example::Haberdasher.service_name
25
+ assert_equal "example.Haberdasher", Example::Haberdasher.service_full_name
26
+ assert_equal "/twirp/example.Haberdasher", Example::Haberdasher.path_prefix
61
27
 
62
28
  assert_equal "", EmptyService.package_name # defaults to empty string
63
29
  assert_equal "EmptyService", EmptyService.service_name # defaults to class name
64
- assert_equal "EmptyService", EmptyService.path_prefix # with no package is just the service name
30
+ assert_equal "EmptyService", EmptyService.service_full_name # with no package is just the service name
31
+ assert_equal "/twirp/EmptyService", EmptyService.path_prefix
65
32
  end
66
33
 
67
- def test_initialize_service
68
- # simple initialization
69
- svc = FooPkg::FooService.new(FooHandler.new)
34
+ def test_init_service
35
+ svc = Example::Haberdasher.new(HaberdasherHandler.new)
70
36
  assert svc.respond_to?(:call) # so it is a Proc that can be used as Rack middleware
71
-
72
- empty_svc = EmptyService.new(nil) # an empty service does not need a handler
73
- assert svc.respond_to?(:call)
37
+ assert_equal "example.Haberdasher", svc.service_full_name
38
+ assert_equal "/twirp/example.Haberdasher", svc.path_prefix
74
39
  end
75
40
 
76
- def test_path_prefix
77
- svc = FooPkg::FooService.new(FooHandler.new)
78
- assert_equal "foopkg.FooService", svc.path_prefix
41
+ def test_init_empty_service
42
+ empty_svc = EmptyService.new(nil) # an empty service does not need a handler
43
+ assert empty_svc.respond_to?(:call)
44
+ assert_equal "EmptyService", empty_svc.service_full_name
45
+ assert_equal "/twirp/EmptyService", empty_svc.path_prefix
79
46
  end
80
47
 
81
- def test_initialize_fails_on_invalid_handlers
48
+ def test_init_failures
82
49
  assert_raises ArgumentError do
83
- FooPkg::FooService.new() # handler is mandatory
50
+ Example::Haberdasher.new() # handler is mandatory
84
51
  end
85
52
 
86
- # verify that handler implements required methods
87
53
  err = assert_raises ArgumentError do
88
- FooPkg::FooService.new("fake handler")
54
+ Example::Haberdasher.new("fake handler")
89
55
  end
90
- assert_equal "Handler must respond to .do_foo(req) in order to handle the message DoFoo.", err.message
56
+ assert_equal "Handler must respond to .make_hat(input) in order to handle the message MakeHat.", err.message
91
57
  end
92
58
 
93
- def test_successful_simple_request
94
- env = Rack::MockRequest.env_for("/twirp/foopkg.FooService/DoFoo", method: "POST",
95
- input: '{"foo": "World"}', "CONTENT_TYPE" => "application/json")
59
+ def test_successful_json_request
60
+ env = json_req "/twirp/example.Haberdasher/MakeHat", inches: 10
61
+ status, headers, body = haberdasher_service.call(env)
96
62
 
97
- svc = FooPkg::FooService.new(FooHandler.new)
98
- status, headers, body = svc.call(env)
63
+ assert_equal 200, status
64
+ assert_equal 'application/json', headers['Content-Type']
65
+ assert_equal({"inches" => 10, "color" => "white"}, JSON.parse(body[0]))
66
+ end
67
+
68
+ def test_successful_proto_request
69
+ env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new(inches: 10)
70
+ status, headers, body = haberdasher_service.call(env)
99
71
 
100
72
  assert_equal 200, status
73
+ assert_equal 'application/protobuf', headers['Content-Type']
74
+ assert_equal Example::Hat.new(inches: 10, color: "white"), Example::Hat.decode(body[0])
75
+ end
76
+
77
+ def test_bad_route_with_wrong_rpc_method
78
+ env = json_req "/twirp/example.Haberdasher/MakeUnicorns", and_rainbows: true
79
+ status, headers, body = haberdasher_service.call(env)
80
+
81
+ assert_equal 404, status
101
82
  assert_equal 'application/json', headers['Content-Type']
102
- assert_equal body[0], '{"bar":"Hello World"}'
83
+ assert_equal({
84
+ "code" => 'bad_route',
85
+ "msg" => 'rpc method not found: "MakeUnicorns"',
86
+ "meta"=> {"twirp_invalid_route" => "POST /twirp/example.Haberdasher/MakeUnicorns"},
87
+ }, JSON.parse(body[0]))
103
88
  end
104
89
 
90
+ def test_bad_route_with_wrong_http_method
91
+ env = Rack::MockRequest.env_for "/twirp/example.Haberdasher/MakeHat",
92
+ method: "GET", input: '{"inches": 10}', "CONTENT_TYPE" => "application/json"
93
+ status, headers, body = haberdasher_service.call(env)
94
+
95
+ assert_equal 404, status
96
+ assert_equal 'application/json', headers['Content-Type']
97
+ assert_equal({
98
+ "code" => 'bad_route',
99
+ "msg" => 'HTTP request method must be POST',
100
+ "meta"=> {"twirp_invalid_route" => "GET /twirp/example.Haberdasher/MakeHat"},
101
+ }, JSON.parse(body[0]))
102
+ end
103
+
104
+ def test_bad_route_with_wrong_content_type
105
+ env = Rack::MockRequest.env_for "/twirp/example.Haberdasher/MakeHat",
106
+ method: "POST", input: 'free text', "CONTENT_TYPE" => "text/plain"
107
+ status, headers, body = haberdasher_service.call(env)
108
+
109
+ assert_equal 404, status
110
+ assert_equal 'application/json', headers['Content-Type']
111
+ assert_equal({
112
+ "code" => 'bad_route',
113
+ "msg" => 'unexpected Content-Type: "text/plain". Content-Type header must be one of "application/json" or "application/protobuf"',
114
+ "meta"=> {"twirp_invalid_route" => "POST /twirp/example.Haberdasher/MakeHat"},
115
+ }, JSON.parse(body[0]))
116
+ end
117
+
118
+ def test_bad_route_with_wrong_path_json
119
+ env = json_req "/wrongpath", {}
120
+ status, headers, body = haberdasher_service.call(env)
121
+
122
+ assert_equal 404, status
123
+ assert_equal 'application/json', headers['Content-Type']
124
+ assert_equal({
125
+ "code" => 'bad_route',
126
+ "msg" => 'Invalid route. Expected format: POST {BaseURL}/twirp/(package.)?{Service}/{Method}',
127
+ "meta"=> {"twirp_invalid_route" => "POST /wrongpath"},
128
+ }, JSON.parse(body[0]))
129
+ end
130
+
131
+ def test_bad_route_with_wrong_path_protobuf
132
+ env = proto_req "/another/wrong.Path/MakeHat", Example::Empty.new()
133
+ status, headers, body = haberdasher_service.call(env)
134
+
135
+ assert_equal 404, status
136
+ assert_equal 'application/json', headers['Content-Type'] # error responses are always JSON, even for Protobuf requests
137
+ assert_equal({
138
+ "code" => 'bad_route',
139
+ "msg" => 'Invalid route. Expected format: POST {BaseURL}/twirp/(package.)?{Service}/{Method}',
140
+ "meta"=> {"twirp_invalid_route" => "POST /another/wrong.Path/MakeHat"},
141
+ }, JSON.parse(body[0]))
142
+ end
143
+
144
+ # Handler should be able to return an instance of the proto message
145
+ def test_handler_returns_a_proto_message
146
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size|
147
+ Example::Hat.new(inches: 11)
148
+ end)
149
+
150
+ env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new
151
+ status, headers, body = svc.call(env)
152
+
153
+ assert_equal 200, status
154
+ assert_equal Example::Hat.new(inches: 11, color: ""), Example::Hat.decode(body[0])
155
+ end
156
+
157
+ # Handler should be able to return a hash with attributes
158
+ def test_handler_returns_hash_attributes
159
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size|
160
+ {inches: 11}
161
+ end)
162
+
163
+ env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new
164
+ status, headers, body = svc.call(env)
165
+
166
+ assert_equal 200, status
167
+ assert_equal Example::Hat.new(inches: 11, color: ""), Example::Hat.decode(body[0])
168
+ end
169
+
170
+ # Handler should be able to return nil, as a message with all zero-values
171
+ def test_handler_returns_nil
172
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size|
173
+ nil
174
+ end)
175
+
176
+ env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new
177
+ status, headers, body = svc.call(env)
178
+
179
+ assert_equal 200, status
180
+ assert_equal Example::Hat.new(inches: 0, color: ""), Example::Hat.decode(body[0])
181
+ end
182
+
183
+ # Handler should be able to return Twirp::Error values, that will trigger error responses
184
+ def test_handler_returns_twirp_error
185
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size|
186
+ return Twirp::Error.new(:invalid_argument, "I don't like that size")
187
+ end)
188
+
189
+ env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new(inches: 666)
190
+ status, headers, body = svc.call(env)
191
+ assert_equal 400, status
192
+ assert_equal 'application/json', headers['Content-Type'] # error responses are always JSON, even for Protobuf requests
193
+ assert_equal({
194
+ "code" => 'invalid_argument',
195
+ "msg" => "I don't like that size",
196
+ }, JSON.parse(body[0]))
197
+ end
198
+
199
+ # Handler should be able to raise a Twirp::Exception, that will trigger error responses
200
+ def test_handler_raises_twirp_exception
201
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size|
202
+ raise Twirp::Exception.new(:invalid_argument, "I don't like that size")
203
+ end)
204
+
205
+ env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new(inches: 666)
206
+ status, headers, body = svc.call(env)
207
+ assert_equal 400, status
208
+ assert_equal 'application/json', headers['Content-Type'] # error responses are always JSON, even for Protobuf requests
209
+ assert_equal({
210
+ "code" => 'invalid_argument',
211
+ "msg" => "I don't like that size",
212
+ }, JSON.parse(body[0]))
213
+ end
214
+
215
+
216
+ # Test Helpers
217
+ # ------------
218
+
219
+ def json_req(path, attrs)
220
+ Rack::MockRequest.env_for path, method: "POST",
221
+ input: JSON.generate(attrs),
222
+ "CONTENT_TYPE" => "application/json"
223
+ end
224
+
225
+ def proto_req(path, proto_message)
226
+ Rack::MockRequest.env_for path, method: "POST",
227
+ input: proto_message.class.encode(proto_message),
228
+ "CONTENT_TYPE" => "application/protobuf"
229
+ end
230
+
231
+ def haberdasher_service
232
+ Example::Haberdasher.new(HaberdasherHandler.new)
233
+ end
105
234
  end
235
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twirp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyrus A. Forbes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-02-16 00:00:00.000000000 Z
12
+ date: 2018-02-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: google-protobuf
@@ -61,10 +61,12 @@ files:
61
61
  - example/main.rb
62
62
  - lib/twirp.rb
63
63
  - lib/twirp/error.rb
64
+ - lib/twirp/exception.rb
64
65
  - lib/twirp/service.rb
65
66
  - lib/twirp/version.rb
66
67
  - protoc-gen-twirp_ruby/main.go
67
68
  - test/error_test.rb
69
+ - test/fake_services.rb
68
70
  - test/service_test.rb
69
71
  - twirp.gemspec
70
72
  homepage: https://github.com/cyrusaf/ruby-twirp
@@ -93,4 +95,5 @@ specification_version: 4
93
95
  summary: Twirp services in Ruby.
94
96
  test_files:
95
97
  - test/error_test.rb
98
+ - test/fake_services.rb
96
99
  - test/service_test.rb