twirp 0.2.0 → 0.3.0

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: de95a607dde7addc708877a41f8b0f99453f1653
4
- data.tar.gz: 7e84e7a36ad28d2d79c8d1ed793194fb3b549552
3
+ metadata.gz: 1e62a59d0a5485723751b5d1f15452c79d50a338
4
+ data.tar.gz: 54444353c6556ad93ac5d3eee42a844f0d9ee106
5
5
  SHA512:
6
- metadata.gz: 7e553cb895062292854559ece9f472213cd66a4fd61bd81b671f9645d51d65057078692c4c6ef2ebf96697265efd0cd3d212b1d069cb08a588937e4a64770760
7
- data.tar.gz: 2ad9fcca77b96fa7e5740be188c18d5f08e514acfb08c91bba1b785a5061f3cd2b325b074466076273ac94ae1b627ca0dd5f9dc247138fb7019205692a6304ed
6
+ metadata.gz: 0c6a92c2d93ef86c8ecffc4ce1a0581eb9b12bca892e018113ab5e4ac0d0c5bce949b1fe19fe8d14b53887f9273196832a4a3d99ed71d0bde07ccfbab5ca5c5d
7
+ data.tar.gz: 1ae6b8cb525b577e4b2eeb92114100f0d7b0444d3e5e2226dea24467934d464db92e304b6ed9899cbeff6aace09b1cf4b795448c0d10dac3066567cbfffa0e8f
data/README.md CHANGED
@@ -1,150 +1,133 @@
1
1
  # Twirp
2
2
 
3
- Ruby implementation for for [Twirp](https://github.com/twitchtv/twirp). It includes:
3
+ Twirp allows to easily define RPC services and clients that communicate using Protobuf or JSON over HTTP.
4
4
 
5
- * [twirp gem](https://rubygems.org/gems/twirp) with classes for:
6
- * Twirp::Error
7
- * Twirp::Service
8
- * Twirp::Client
9
- * `protoc-gen-twirp_ruby` protoc plugin for code generation.
5
+ The [Twirp protocol](https://twitchtv.github.io/twirp/docs/spec_v5.html) is implemented in multiple languages. This means that you can write your service in one language and automatically generate clients in other languages. Refer to the [Golang implementation](https://github.com/twitchtv/twirp) for more details on the project.
10
6
 
7
+ ## Install
11
8
 
12
- ## Installation
9
+ Add `gem "twirp"` to your Gemfile, or install with `gem install twirp`.
13
10
 
14
- Add `gem "twirp"` to your Gemfile, or install with:
11
+ For code generation, you also need [protoc](https://github.com/golang/protobuf) (version 3+).
15
12
 
16
- ```sh
17
- ➜ gem install twirp
18
- ```
19
-
20
- For code generation, Make sure that you have the [protobuf compiler](https://github.com/golang/protobuf) (install version 3+).
21
- And then use `go get` to install the ruby_twirp protoc plugin:
13
+ ## Service DSL
22
14
 
23
- ```sh
24
- ➜ go get -u github.com/cyrusaf/ruby-twirp/protoc-gen-twirp_ruby
25
- ```
15
+ A Twirp service defines RPC methods to send and receive Protobuf messages. For example, a `HelloWorld` service:
26
16
 
17
+ ```ruby
18
+ require 'twirp'
27
19
 
28
- ## Usage
20
+ module Example
21
+ class HelloWorldService < Twirp::Service
22
+ package "example"
23
+ service "HelloWorld"
24
+ rpc :Hello, HelloRequest, HelloResponse, :ruby_method => :hello
25
+ end
29
26
 
30
- Let's make a `HelloWorld` service in Twirp.
27
+ class HelloWorldClient < Twirp::Client
28
+ client_for HelloWorldService
29
+ end
30
+ end
31
+ ```
31
32
 
32
- ### Code Generation
33
+ The `HelloRequest` and `HelloResponse` messages are expected to be [google-protobuf](https://github.com/google/protobuf/tree/master/ruby) messages, which can also be defined from their DSL or auto-generated.
33
34
 
34
- Starting with a [Protobuf](https://developers.google.com/protocol-buffers/docs/proto3) file:
35
35
 
36
- ```protobuf
37
- // hello_world/service.proto
36
+ ## Code Generation
38
37
 
39
- syntax = "proto3";
40
- package example;
38
+ RPC messages and the service definition can be auto-generated form a `.proto` file.
41
39
 
42
- service HelloWorld {
43
- rpc Hello(HelloRequest) returns (HelloResponse);
44
- }
40
+ Code generation works with [protoc](https://github.com/golang/protobuf) (the protobuf compiler)
41
+ using the `--ruby_out` option to generate messages and `--twirp_ruby_out` to generate services and clients.
45
42
 
46
- message HelloRequest {
47
- string name = 1;
48
- }
43
+ Make sure to install `protoc` (version 3+). Then use `go get` (Golang) to install the ruby_twirp protoc plugin:
49
44
 
50
- message HelloResponse {
51
- string message = 1;
52
- }
45
+ ```sh
46
+ go get -u github.com/cyrusaf/ruby-twirp/protoc-gen-twirp_ruby
53
47
  ```
54
48
 
55
- Run the `protoc` binary with the `twirp_ruby` plugin to auto-generate code:
49
+ Given a [Protobuf](https://developers.google.com/protocol-buffers/docs/proto3) file like [example/hello_world/service.proto](example/hello_world/service.proto), you can auto-generate proto and twirp files with the command:
56
50
 
57
51
  ```sh
58
- protoc --proto_path=. ./hello_world/service.proto --ruby_out=. --twirp_ruby_out=.
52
+ protoc --proto_path=. --ruby_out=. --twirp_ruby_out=. ./example/hello_world/service.proto
59
53
  ```
60
54
 
61
- This will generate `/hello_world/service_pb.rb` and `hello_world/service_twirp.rb`. The generated code looks something like this:
55
+
56
+ ## Twirp Service
57
+
58
+ A Twirp service delegates into a service handler to implement each rpc method. For example a handler for `HelloWorld`:
62
59
 
63
60
  ```ruby
64
- # Code generated by protoc-gen-twirp_ruby, DO NOT EDIT.
65
- require 'twirp'
61
+ class HelloWorldHandler
66
62
 
67
- module Example
68
- class HelloWorldService < Twirp::Service
69
- package "example"
70
- service "HelloWorld"
71
- rpc :Hello, HelloRequest, HelloResponse, :ruby_method => :hello
63
+ def hello(req, env)
64
+ if req.name.empty?
65
+ Twirp::Error.invalid_argument("name is mandatory")
66
+ else
67
+ {message: "Hello #{req.name}"}
68
+ end
72
69
  end
73
70
 
74
- class HelloWorldClient < Twirp::Client
75
- client_for HelloWorldService
76
- end
77
71
  end
78
72
  ```
79
73
 
80
- If you don't have Proto files, or don't like the code-generation step, you can always define your service and/or client using the DSL.
74
+ The `req` argument is the request message (input), and the returned value is expected to be the response message, or a `Twirp::Error`.
81
75
 
76
+ The `env` argument contains metadata related to the request (e.g. `env[:output_class]`), and other fields that could have been set from before-hooks (e.g. `env[:user_id]` from authentication).
82
77
 
83
- #### Implement the Service Handler
78
+ ### Unit Tests
84
79
 
85
- The Service Handler is a simple class that has one method to handle each rpc call.
86
- For each method, the `intput` is an instance of the protobuf request message. The Twirp `env`
87
- contains metadata related to the request, and other fields that could have been set from before
88
- hooks (e.g. `env[:user_id]` from authentication).
80
+ Twirp already takes care of HTTP routing and serialization, you don't really need to test that part, insteadof that, focus on testing the handler using the method `.call_rpc(rpc_method, attrs={}, env={})` on the service:
89
81
 
90
82
  ```ruby
91
- class HelloWorldHandler
92
- def hello(input, env)
93
- {message: "Hello #{input.name}"}
83
+ require 'minitest/autorun'
84
+
85
+ class HelloWorldHandlerTest < Minitest::Test
86
+
87
+ def test_hello_responds_with_name
88
+ resp = service.call_rpc :Hello, name: "World"
89
+ assert_equal "Hello World", resp.message
90
+ end
91
+
92
+ def test_hello_name_is_mandatory
93
+ twerr = service.call_rpc :Hello, name: ""
94
+ assert_equal :invalid_argument, twerr.code
95
+ end
96
+
97
+ def service
98
+ handler = HelloWorldHandler.new()
99
+ Example::HelloWorldService.new(handler)
94
100
  end
95
101
  end
96
102
  ```
97
103
 
98
- ### Mount the service to receive HTTP requests
99
104
 
100
- The service is a Rack app instantiated with your handler impementation.
105
+ ### Start the Service
101
106
 
102
- ```ruby
103
- require 'rack'
107
+ The service is a [Rack app](https://rack.github.io/) instantiated with your handler impementation. For example:
104
108
 
105
- handler = HelloWorldHandler.new() # your implementation
106
- service = Example::HelloWorldService.new(handler) # twirp-generated
109
+ ```ruby
110
+ handler = HelloWorldHandler.new()
111
+ service = Example::HelloWorldService.new(handler)
107
112
 
113
+ require 'rack'
108
114
  Rack::Handler::WEBrick.run service
109
115
  ```
110
116
 
111
- Since it is a Rack app, it can easily be mounted onto a Rails route with `mount service, at: service.full_name`.
117
+ Rack apps can also be mounted as Rails routes (e.g. `mount service, at: service.full_name`) and are compatible with many other HTTP frameworks.
112
118
 
113
- Now you can start the server and `curl` with JSON to test if everything works:
114
119
 
115
- ```sh
116
- ➜ curl --request POST \
117
- --url http://localhost:8080/example.HelloWorld/Hello \
118
- --header 'Content-Type: application/json' \
119
- --data '{"name": "World"}'
120
- ```
120
+ ## Twirp Clients
121
121
 
122
- ### Unit testing the Service Handler
123
-
124
- Twirp already takes care of HTTP routing and serialization, you don't really need to build fake HTTP requests in your tests.
125
- Instead, you should focus on testing your Service Handler. For convenience, the Twirp Service has the method
126
- `.call_rpc(rpc_method, attrs={}, env={})` to call the handler with a fake Twirp env and making sure that the handler output is valid.
122
+ Instantiate the client with the base_url:
127
123
 
128
124
  ```ruby
129
- require 'minitest/autorun'
130
-
131
- class HelloWorldHandlerTest < Minitest::Test
132
- def test_hello_responds_with_name
133
- service = Example::HelloWorld.new(HelloWorldHandler.new())
134
- out = service.call_rpc :Hello, name: "World"
135
- assert_equal "Hello World", out.message
136
- end
137
- end
125
+ c = Example::HelloWorldClient.new("http://localhost:3000")
138
126
  ```
139
127
 
140
-
141
- ## Clients
142
-
143
- Generated clients implement the methods defined in the proto file. The response object contains `data` with an instance of the response class if successfull,
144
- or an `error` with an instance of `Twirp::Error` if there was a problem. For example, with the HelloWorld generated client:
128
+ Clients implement the same methods as in the service and return a response object with `data` or `error`:
145
129
 
146
130
  ```ruby
147
- c = Example::HelloWorldClient.new("http://localhost:3000")
148
131
  resp = c.hello(name: "World")
149
132
  if resp.error
150
133
  puts resp.error #=> <Twirp::Error code:... msg:"..." meta:{...}>
@@ -153,33 +136,27 @@ else
153
136
  end
154
137
  ```
155
138
 
156
- You can also use the DSL to define your own client if you don't have easy access to the proto file or generated code:
139
+ ### Protobuf or JSON
157
140
 
158
- ```ruby
159
- class MyClient < Twirp::Client
160
- package "example"
161
- service "MyService"
162
- rpc :MyMethod, ReqClass, RespClass, :ruby_method => :my_method
163
- end
141
+ Clients use Protobuf by default. To use JSON, set the `content_type` option:
164
142
 
165
- c = MyClient.new("http://localhost:3000")
166
- resp = c.my_method(ReqClass.new())
143
+ ```ruby
144
+ c = Example::HelloWorldClient.new("http://localhost:3000", content_type: "application/json")
167
145
  ```
168
146
 
147
+ ### Configure Clients with Faraday
169
148
 
170
- ### Configure client with Faraday
171
-
172
- A Twirp client takes care of routing, serialization and error handling.
173
-
174
- Other advanced HTTP options can be configured with [Faraday](https://github.com/lostisland/faraday) middleware. For example:
149
+ While Twirp takes care of routing, serialization and error handling, other advanced HTTP options can be configured with [Faraday](https://github.com/lostisland/faraday) middleware. For example:
175
150
 
176
151
  ```ruby
177
- c = MyClient.new(Faraday.new(:url => 'http://localhost:3000') do |c|
178
- c.use Faraday::Request::Retry # configure retries
152
+ conn = Faraday.new(:url => 'http://localhost:3000') do |c|
153
+ c.use Faraday::Request::Retry
179
154
  c.use Faraday::Request::BasicAuthentication, 'login', 'pass'
180
155
  c.use Faraday::Response::Logger # log to STDOUT
181
- c.use Faraday::Adapter::NetHttp # multiple adapters for different HTTP libraries
182
- end)
156
+ c.use Faraday::Adapter::NetHttp # can use different HTTP libraries
157
+ end
158
+
159
+ c = Example::HelloWorldClient.new(conn)
183
160
  ```
184
161
 
185
162
  ## Server Hooks
data/lib/twirp/client.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  require 'faraday'
2
2
  require 'json'
3
3
 
4
- require_relative "error"
5
- require_relative "service_dsl"
4
+ require_relative 'encoding'
5
+ require_relative 'error'
6
+ require_relative 'service_dsl'
6
7
 
7
8
  module Twirp
8
9
 
@@ -23,37 +24,49 @@ module Twirp
23
24
  # Hook for ServiceDSL#rpc to define a new method client.<ruby_method>(input, opts).
24
25
  def self.rpc_define_method(rpcdef)
25
26
  define_method rpcdef[:ruby_method] do |input|
26
- call_rpc(rpcdef[:rpc_method], input)
27
+ rpc(rpcdef[:rpc_method], input)
27
28
  end
28
29
  end
29
30
 
30
- # Init with a Faraday connection.
31
- def initialize(conn)
31
+ # Init with a Faraday connection, or a base_url that is used in a default connection.
32
+ # Clients use Content-Type="application/protobuf" by default. For JSON clinets use :content_type => "application/json".
33
+ def initialize(conn, opts={})
32
34
  @conn = case conn
33
- when String then Faraday.new(url: conn) # init with hostname
34
- when Faraday::Connection then conn # inith with connection
35
- else raise ArgumentError.new("Expected hostname String or Faraday::Connection")
35
+ when String then Faraday.new(url: conn) # init with hostname
36
+ when Faraday::Connection then conn # init with connection
37
+ else raise ArgumentError.new("Invalid conn #{conn.inspect}. Expected String hostname or Faraday::Connection")
38
+ end
39
+
40
+ @content_type = (opts[:content_type] || Encoding::PROTO)
41
+ if !Encoding.valid_content_type?(@content_type)
42
+ raise ArgumentError.new("Invalid content_type #{@content_type.inspect}. Expected one of #{Encoding.valid_content_types.inspect}")
36
43
  end
37
44
  end
38
45
 
39
- def service_full_name; self.class.service_full_name; end
46
+ def service_full_name
47
+ self.class.service_full_name
48
+ end
40
49
 
41
50
  def rpc_path(rpc_method)
42
51
  "/#{service_full_name}/#{rpc_method}"
43
52
  end
44
53
 
45
- def call_rpc(rpc_method, input)
54
+ # Make a remote procedure call to a defined rpc_method. The input can be a Proto message instance,
55
+ # or the attributes (Hash) to instantiate it. Returns a ClientResp instance with an instance of
56
+ # output_class, or a Twirp::Error. The input and output classes are the ones configued with the rpc DSL.
57
+ # If rpc_method was not defined with the rpc DSL then a response with a bad_route error is returned instead.
58
+ def rpc(rpc_method, input)
46
59
  rpcdef = self.class.rpcs[rpc_method.to_s]
47
60
  if !rpcdef
48
61
  return ClientResp.new(nil, Twirp::Error.bad_route("rpc not defined on this client"))
49
62
  end
50
63
 
51
64
  input = rpcdef[:input_class].new(input) if input.is_a? Hash
52
- body = rpcdef[:input_class].encode(input)
65
+ body = Encoding.encode(input, rpcdef[:input_class], @content_type)
53
66
 
54
67
  resp = @conn.post do |r|
55
68
  r.url rpc_path(rpc_method)
56
- r.headers['Content-Type'] = 'application/protobuf'
69
+ r.headers['Content-Type'] = @content_type
57
70
  r.body = body
58
71
  end
59
72
 
@@ -61,11 +74,11 @@ module Twirp
61
74
  return ClientResp.new(nil, error_from_response(resp))
62
75
  end
63
76
 
64
- if resp.headers['Content-Type'] != 'application/protobuf'
65
- return ClientResp.new(nil, Twirp::Error.internal("Expected response Content-Type \"application/protobuf\" but found #{resp.headers['Content-Type'].inspect}"))
77
+ if resp.headers['Content-Type'] != @content_type
78
+ return ClientResp.new(nil, Twirp::Error.internal("Expected response Content-Type #{@content_type.inspect} but found #{resp.headers['Content-Type'].inspect}"))
66
79
  end
67
80
 
68
- data = rpcdef[:output_class].decode(resp.body)
81
+ data = Encoding.decode(resp.body, rpcdef[:output_class], @content_type)
69
82
  return ClientResp.new(data, nil)
70
83
  end
71
84
 
@@ -79,7 +92,7 @@ module Twirp
79
92
  err_attrs = nil
80
93
  begin
81
94
  err_attrs = JSON.parse(resp.body)
82
- rescue JSON::ParserError => e
95
+ rescue JSON::ParserError
83
96
  return twirp_error_from_intermediary(status, "Response is not JSON", resp.body)
84
97
  end
85
98
 
@@ -109,7 +122,7 @@ module Twirp
109
122
  else :unknown
110
123
  end
111
124
 
112
- twerr = Twirp::Error.new(code, code.to_s, {
125
+ Twirp::Error.new(code, code.to_s, {
113
126
  http_error_from_intermediary: "true",
114
127
  not_a_twirp_error_because: reason,
115
128
  status_code: status.to_s,
@@ -0,0 +1,32 @@
1
+ module Twirp
2
+
3
+ module Encoding
4
+ JSON = "application/json"
5
+ PROTO = "application/protobuf"
6
+
7
+ def self.decode(bytes, msg_class, content_type)
8
+ case content_type
9
+ when JSON then msg_class.decode_json(bytes)
10
+ when PROTO then msg_class.decode(bytes)
11
+ else raise ArgumentError.new("Invalid content_type")
12
+ end
13
+ end
14
+
15
+ def self.encode(msg_obj, msg_class, content_type)
16
+ case content_type
17
+ when JSON then msg_class.encode_json(msg_obj)
18
+ when PROTO then msg_class.encode(msg_obj)
19
+ else raise ArgumentError.new("Invalid content_type")
20
+ end
21
+ end
22
+
23
+ def self.valid_content_type?(content_type)
24
+ content_type == Encoding::JSON || content_type == Encoding::PROTO
25
+ end
26
+
27
+ def self.valid_content_types
28
+ [Encoding::JSON, Encoding::PROTO]
29
+ end
30
+ end
31
+
32
+ end
data/lib/twirp/service.rb CHANGED
@@ -1,7 +1,8 @@
1
- require "json"
1
+ require 'json'
2
2
 
3
- require_relative "error"
4
- require_relative "service_dsl"
3
+ require_relative 'encoding'
4
+ require_relative 'error'
5
+ require_relative 'service_dsl'
5
6
 
6
7
  module Twirp
7
8
 
@@ -42,12 +43,12 @@ module Twirp
42
43
  env = {}
43
44
  bad_route = route_request(rack_env, env)
44
45
  return error_response(bad_route, env) if bad_route
45
-
46
+
46
47
  @before.each do |hook|
47
48
  result = hook.call(rack_env, env)
48
49
  return error_response(result, env) if result.is_a? Twirp::Error
49
50
  end
50
-
51
+
51
52
  output = call_handler(env)
52
53
  return error_response(output, env) if output.is_a? Twirp::Error
53
54
  return success_response(output, env)
@@ -59,7 +60,7 @@ module Twirp
59
60
  rescue => hook_e
60
61
  e = hook_e
61
62
  end
62
-
63
+
63
64
  twerr = Twirp::Error.internal_with(e)
64
65
  return error_response(twerr, env)
65
66
  end
@@ -77,7 +78,7 @@ module Twirp
77
78
  env = env.merge(base_env)
78
79
  input = env[:input_class].new(input) if input.is_a? Hash
79
80
  env[:input] = input
80
- env[:content_type] ||= "application/protobuf"
81
+ env[:content_type] ||= Encoding::PROTO
81
82
  env[:http_response_headers] = {}
82
83
  call_handler(env)
83
84
  end
@@ -95,11 +96,11 @@ module Twirp
95
96
  end
96
97
 
97
98
  content_type = rack_request.get_header("CONTENT_TYPE")
98
- if content_type != "application/json" && content_type != "application/protobuf"
99
- return bad_route_error("unexpected Content-Type: #{content_type.inspect}. Content-Type header must be one of application/json or application/protobuf", rack_request)
99
+ if !Encoding.valid_content_type?(content_type)
100
+ return bad_route_error("Unexpected Content-Type: #{content_type.inspect}. Content-Type header must be one of #{Encoding.valid_content_types.inspect}", rack_request)
100
101
  end
101
102
  env[:content_type] = content_type
102
-
103
+
103
104
  path_parts = rack_request.fullpath.split("/")
104
105
  if path_parts.size < 3 || path_parts[-2] != self.full_name
105
106
  return bad_route_error("Invalid route. Expected format: POST {BaseURL}/#{self.full_name}/{Method}", rack_request)
@@ -114,8 +115,8 @@ module Twirp
114
115
 
115
116
  input = nil
116
117
  begin
117
- input = decode_input(rack_request.body.read, env[:input_class], content_type)
118
- rescue => e
118
+ input = Encoding.decode(rack_request.body.read, env[:input_class], content_type)
119
+ rescue
119
120
  return bad_route_error("Invalid request body for rpc method #{method_name.inspect} with Content-Type=#{content_type}", rack_request)
120
121
  end
121
122
 
@@ -128,19 +129,7 @@ module Twirp
128
129
  Twirp::Error.bad_route msg, twirp_invalid_route: "#{req.request_method} #{req.fullpath}"
129
130
  end
130
131
 
131
- def decode_input(bytes, input_class, content_type)
132
- case content_type
133
- when "application/protobuf" then input_class.decode(bytes)
134
- when "application/json" then input_class.decode_json(bytes)
135
- end
136
- end
137
132
 
138
- def encode_output(obj, output_class, content_type)
139
- case content_type
140
- when "application/protobuf" then output_class.encode(obj)
141
- when "application/json" then output_class.encode_json(obj)
142
- end
143
- end
144
133
 
145
134
  # Call handler method and return a Protobuf Message or a Twirp::Error.
146
135
  def call_handler(env)
@@ -166,7 +155,7 @@ module Twirp
166
155
  @on_success.each{|hook| hook.call(env) }
167
156
 
168
157
  headers = env[:http_response_headers].merge('Content-Type' => env[:content_type])
169
- resp_body = encode_output(output, env[:output_class], env[:content_type])
158
+ resp_body = Encoding.encode(output, env[:output_class], env[:content_type])
170
159
  [200, headers, [resp_body]]
171
160
 
172
161
  rescue => e
@@ -201,7 +190,8 @@ module Twirp
201
190
  end
202
191
 
203
192
  def error_response_headers
204
- {'Content-Type' => 'application/json'}
193
+ # Twirp errors are always JSON, even if the request was protobuf
194
+ {'Content-Type' => Encoding::JSON}
205
195
  end
206
196
 
207
197
  end
@@ -4,12 +4,12 @@ module Twirp
4
4
 
5
5
  # Configure service package name.
6
6
  def package(name)
7
- @package = name
7
+ @package = name.to_s
8
8
  end
9
9
 
10
10
  # Configure service name.
11
11
  def service(name)
12
- @service = name
12
+ @service = name.to_s
13
13
  end
14
14
 
15
15
  # Configure service rpc methods.
data/lib/twirp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Twirp
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/test/client_test.rb CHANGED
@@ -27,39 +27,39 @@ class ClientTest < Minitest::Test
27
27
  end
28
28
  end
29
29
 
30
- def test_call_rpc_success
30
+ def test_rpc_success
31
31
  c = FooClient.new(conn_stub("/Foo/Foo") {|req|
32
32
  [200, protoheader, proto(Foo, foo: "out")]
33
33
  })
34
- resp = c.call_rpc(:Foo, foo: "in")
34
+ resp = c.rpc :Foo, foo: "in"
35
35
  assert_nil resp.error
36
36
  refute_nil resp.data
37
37
  assert_equal "out", resp.data.foo
38
38
  end
39
39
 
40
- def test_call_rpc_error
40
+ def test_rpc_error
41
41
  c = FooClient.new(conn_stub("/Foo/Foo") {|req|
42
42
  [400, {}, json(code: "invalid_argument", msg: "dont like empty")]
43
43
  })
44
- resp = c.call_rpc(:Foo, foo: "")
44
+ resp = c.rpc :Foo, foo: ""
45
45
  assert_nil resp.data
46
46
  refute_nil resp.error
47
47
  assert_equal :invalid_argument, resp.error.code
48
48
  assert_equal "dont like empty", resp.error.msg
49
49
  end
50
50
 
51
- def test_call_rpc_serialization_exception
51
+ def test_rpc_serialization_exception
52
52
  c = FooClient.new(conn_stub("/Foo/Foo") {|req|
53
53
  [200, protoheader, "badstuff"]
54
54
  })
55
55
  assert_raises Google::Protobuf::ParseError do
56
- resp = c.call_rpc(:Foo, foo: "in")
56
+ c.rpc :Foo, foo: "in"
57
57
  end
58
58
  end
59
59
 
60
- def test_call_rpc_invalid_method
60
+ def test_rpc_invalid_method
61
61
  c = FooClient.new("http://localhost")
62
- resp = c.call_rpc(:OtherStuff, foo: "noo")
62
+ resp = c.rpc :OtherStuff, foo: "noo"
63
63
  assert_nil resp.data
64
64
  refute_nil resp.error
65
65
  assert_equal :bad_route, resp.error.code
@@ -174,6 +174,64 @@ class ClientTest < Minitest::Test
174
174
  assert_equal '{"msg":"I have no code of honor"}', resp.error.meta[:body]
175
175
  end
176
176
 
177
+ def test_json_success
178
+ c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
179
+ [200, jsonheader, '{"inches": 99, "color": "red"}']
180
+ }, content_type: "application/json")
181
+
182
+ resp = c.make_hat({})
183
+ assert_nil resp.error
184
+ assert_equal 99, resp.data.inches
185
+ assert_equal "red", resp.data.color
186
+ end
187
+
188
+ def test_json_serialized_request_body_attrs
189
+ c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
190
+ assert_equal "application/json", req.request_headers['Content-Type']
191
+ assert_equal '{"inches":666}', req.body # body is valid json
192
+ [200, jsonheader, '{}']
193
+ }, content_type: "application/json")
194
+
195
+ resp = c.make_hat(inches: 666)
196
+ assert_nil resp.error
197
+ refute_nil resp.data
198
+ end
199
+
200
+ def test_json_serialized_request_body_object
201
+ c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
202
+ assert_equal "application/json", req.request_headers['Content-Type']
203
+ assert_equal '{"inches":666}', req.body # body is valid json
204
+ [200, jsonheader, '{}']
205
+ }, content_type: "application/json")
206
+
207
+ resp = c.make_hat(Example::Size.new(inches: 666))
208
+ assert_nil resp.error
209
+ refute_nil resp.data
210
+ end
211
+
212
+ def test_json_error
213
+ c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
214
+ [500, {}, json(code: "internal", msg: "something went wrong")]
215
+ }, content_type: "application/json")
216
+
217
+ resp = c.make_hat(inches: 1)
218
+ assert_nil resp.data
219
+ refute_nil resp.error
220
+ assert_equal :internal, resp.error.code
221
+ assert_equal "something went wrong", resp.error.msg
222
+ end
223
+
224
+ def test_json_missing_response_header
225
+ c = Example::HaberdasherClient.new(conn_stub("/example.Haberdasher/MakeHat") {|req|
226
+ [200, {}, json(inches: 99, color: "red")]
227
+ }, content_type: "application/json")
228
+
229
+ resp = c.make_hat({})
230
+ refute_nil resp.error
231
+ assert_equal :internal, resp.error.code
232
+ assert_equal 'Expected response Content-Type "application/json" but found nil', resp.error.msg
233
+ end
234
+
177
235
 
178
236
  # Test Helpers
179
237
  # ------------
@@ -182,6 +240,10 @@ class ClientTest < Minitest::Test
182
240
  {'Content-Type' => 'application/protobuf'}
183
241
  end
184
242
 
243
+ def jsonheader
244
+ {'Content-Type' => 'application/json'}
245
+ end
246
+
185
247
  def proto(clss, attrs={})
186
248
  clss.encode(clss.new(attrs))
187
249
  end
data/test/service_test.rb CHANGED
@@ -100,7 +100,7 @@ class ServiceTest < Minitest::Test
100
100
  assert_equal 'application/json', headers['Content-Type']
101
101
  assert_equal({
102
102
  "code" => 'bad_route',
103
- "msg" => 'unexpected Content-Type: "text/plain". Content-Type header must be one of application/json or application/protobuf',
103
+ "msg" => 'Unexpected Content-Type: "text/plain". Content-Type header must be one of ["application/json", "application/protobuf"]',
104
104
  "meta" => {"twirp_invalid_route" => "POST /example.Haberdasher/MakeHat"},
105
105
  }, JSON.parse(body[0]))
106
106
  end
@@ -178,7 +178,7 @@ class ServiceTest < Minitest::Test
178
178
  end
179
179
 
180
180
  rack_env = json_req "/example.Haberdasher/BadRouteMethod", inches: 10
181
- status, headers, body = svc.call(rack_env)
181
+ status, _, body = svc.call(rack_env)
182
182
 
183
183
  assert_equal 404, status
184
184
  refute before_called
@@ -188,7 +188,7 @@ class ServiceTest < Minitest::Test
188
188
 
189
189
  def test_long_base_url
190
190
  rack_env = json_req "long-ass/base/url/twirp/example.Haberdasher/MakeHat", {inches: 10}
191
- status, headers, body = haberdasher_service.call(rack_env)
191
+ status, _, body = haberdasher_service.call(rack_env)
192
192
 
193
193
  assert_equal 200, status
194
194
  end
@@ -200,7 +200,7 @@ class ServiceTest < Minitest::Test
200
200
  end)
201
201
 
202
202
  rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new
203
- status, headers, body = svc.call(rack_env)
203
+ status, _, body = svc.call(rack_env)
204
204
 
205
205
  assert_equal 200, status
206
206
  assert_equal Example::Hat.new(inches: 11, color: ""), Example::Hat.decode(body[0])
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.2.0
4
+ version: 0.3.0
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-04-07 00:00:00.000000000 Z
12
+ date: 2018-04-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: google-protobuf
@@ -73,6 +73,7 @@ files:
73
73
  - README.md
74
74
  - lib/twirp.rb
75
75
  - lib/twirp/client.rb
76
+ - lib/twirp/encoding.rb
76
77
  - lib/twirp/error.rb
77
78
  - lib/twirp/service.rb
78
79
  - lib/twirp/service_dsl.rb