twirp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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