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 +4 -4
- data/README.md +5 -3
- data/example/main.rb +1 -1
- data/lib/twirp.rb +2 -1
- data/lib/twirp/error.rb +13 -2
- data/lib/twirp/exception.rb +21 -0
- data/lib/twirp/service.rb +73 -48
- data/lib/twirp/version.rb +1 -1
- data/test/fake_services.rb +58 -0
- data/test/service_test.rb +199 -69
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d21a865d8176ee77a6651aabe59247ae763e1d5
|
4
|
+
data.tar.gz: 8d16237130c4e966f97b98f7f529f6eb82814bf0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
57
|
+
return {message: "Hello #{req.name}"}
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
61
|
handler = HaberdasherHandler.new()
|
62
|
-
|
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
|
-
|
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
data/lib/twirp.rb
CHANGED
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.
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
67
|
+
|
68
|
+
end # class << self
|
69
|
+
|
66
70
|
|
67
71
|
# Instantiate a new service with a handler.
|
68
|
-
#
|
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]}(
|
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
|
-
#
|
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
|
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
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
100
|
-
|
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
|
-
|
109
|
-
|
110
|
-
resp_msg = @handler.send(rpc_method[:handler_method], req_msg)
|
134
|
+
return rpc, content_type, nil
|
135
|
+
end
|
111
136
|
|
112
|
-
|
113
|
-
|
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
|
117
|
-
|
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
|
-
#
|
124
|
-
|
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
|
-
|
127
|
-
|
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
|
-
|
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
@@ -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
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
49
|
-
assert_equal 1, FooPkg::FooService.rpcs.size
|
13
|
+
assert_equal 1, Example::Haberdasher.rpcs.size
|
50
14
|
assert_equal({
|
51
|
-
request_class:
|
52
|
-
response_class:
|
53
|
-
handler_method: :
|
54
|
-
},
|
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 "
|
59
|
-
assert_equal "
|
60
|
-
assert_equal "
|
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.
|
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
|
68
|
-
|
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
|
-
|
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
|
77
|
-
|
78
|
-
|
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
|
48
|
+
def test_init_failures
|
82
49
|
assert_raises ArgumentError do
|
83
|
-
|
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
|
-
|
54
|
+
Example::Haberdasher.new("fake handler")
|
89
55
|
end
|
90
|
-
assert_equal "Handler must respond to .
|
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
|
94
|
-
env =
|
95
|
-
|
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
|
-
|
98
|
-
|
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
|
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.
|
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-
|
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
|