twirp 0.0.3 → 0.0.4

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