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