twirp 0.4.0 → 0.4.1

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: 8be53839103af850b802185600fb7496556d3334
4
- data.tar.gz: '08333f8e2be8996b92d9d25368eaa03d0470ef51'
3
+ metadata.gz: a30f71eb6c946fa8356d486b1decd82733bbb3af
4
+ data.tar.gz: db3c570238ac24017af314eedb327fd8f5f07bb0
5
5
  SHA512:
6
- metadata.gz: d4a61196709d7b86207d20618046c87eed889cdb1d5f15693019aad4e25f7df2aecee842b17d2e546cd84212718301e5938fddd4fd274d7e982c6d5354ac6ce1
7
- data.tar.gz: b51466076296ddc2a4ee33edfd4557c4236408786902556c77a49e478c0e0a71d4dd603e9a788a141e98739fe15ab252e62f00a96d3bae024386909e27f68aa7
6
+ metadata.gz: a59ff7c4ba349bce47e06a3a47d68eccfd411f43b63df34948593ab0d7987a596a766df398b633a8b31890cbadd09297c89d7619bd7fb1990d4538ae65c9c297
7
+ data.tar.gz: 39b024e1ba715058c2ad07c7b3e5942d0d3aae47960a02402b1f497c3bd6fdb1bdc6cd19c88005a8844238c2686e6d40251aeeeca13ed0cadb50f499f2c3c216
data/README.md CHANGED
@@ -53,19 +53,19 @@ protoc --proto_path=. --ruby_out=. --twirp_ruby_out=. ./example/hello_world/serv
53
53
  ```
54
54
 
55
55
 
56
- ## Twirp Service
56
+ ## Twirp Service Handler
57
57
 
58
- A Twirp service delegates into a service handler to implement each rpc method. For example a handler for `HelloWorld`:
58
+ A handler is a simple class that implements each rpc method. For example a handler for `HelloWorld`:
59
59
 
60
60
  ```ruby
61
61
  class HelloWorldHandler
62
62
 
63
63
  def hello(req, env)
64
64
  if req.name.empty?
65
- Twirp::Error.invalid_argument("name is mandatory")
66
- else
67
- {message: "Hello #{req.name}"}
65
+ return Twirp::Error.invalid_argument("is mandatory", argument: "name")
68
66
  end
67
+
68
+ {message: "Hello #{req.name}"}
69
69
  end
70
70
 
71
71
  end
@@ -75,6 +75,21 @@ The `req` argument is the request message (input), and the returned value is exp
75
75
 
76
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).
77
77
 
78
+
79
+ ### Start the Service
80
+
81
+ The service is a [Rack app](https://rack.github.io/) instantiated with your handler impementation. For example:
82
+
83
+ ```ruby
84
+ handler = HelloWorldHandler.new()
85
+ service = Example::HelloWorldService.new(handler)
86
+
87
+ require 'rack'
88
+ Rack::Handler::WEBrick.run service
89
+ ```
90
+
91
+ 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.
92
+
78
93
  ### Unit Tests
79
94
 
80
95
  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:
@@ -102,33 +117,18 @@ end
102
117
  ```
103
118
 
104
119
 
105
- ### Start the Service
106
-
107
- The service is a [Rack app](https://rack.github.io/) instantiated with your handler impementation. For example:
108
-
109
- ```ruby
110
- handler = HelloWorldHandler.new()
111
- service = Example::HelloWorldService.new(handler)
112
-
113
- require 'rack'
114
- Rack::Handler::WEBrick.run service
115
- ```
116
-
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.
118
-
119
-
120
120
  ## Twirp Clients
121
121
 
122
- Instantiate the client with the base_url:
122
+ Clients implement the same methods as the service. For Example:
123
123
 
124
124
  ```ruby
125
125
  c = Example::HelloWorldClient.new("http://localhost:3000")
126
+ resp = c.hello(name: "World") # serialized as Protobuf
126
127
  ```
127
128
 
128
- Clients implement the same methods as in the service and return a response object with `data` or `error`:
129
+ The response object can have `data` or an `error`.
129
130
 
130
131
  ```ruby
131
- resp = c.hello(name: "World")
132
132
  if resp.error
133
133
  puts resp.error #=> <Twirp::Error code:... msg:"..." meta:{...}>
134
134
  else
@@ -136,9 +136,9 @@ else
136
136
  end
137
137
  ```
138
138
 
139
- ### Configure Clients with Faraday
139
+ #### Configure Clients with Faraday
140
140
 
141
- 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:
141
+ 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. Clients can be initialized with a Faraday connection:
142
142
 
143
143
  ```ruby
144
144
  conn = Faraday.new(:url => 'http://localhost:3000') do |c|
@@ -151,23 +151,22 @@ end
151
151
  c = Example::HelloWorldClient.new(conn)
152
152
  ```
153
153
 
154
- ### Protobuf or JSON
154
+ #### Protobuf or JSON
155
155
 
156
156
  Clients use Protobuf by default. To use JSON, set the `content_type` option as 2nd argument:
157
157
 
158
158
  ```ruby
159
- c = Example::HelloWorldClient.new("http://localhost:3000", content_type: "application/json")
159
+ c = Example::HelloWorldClient.new(conn, content_type: "application/json")
160
160
  resp = c.hello(name: "World") # serialized as JSON
161
161
  ```
162
162
 
163
- ### Add-hoc JSON requests
163
+ #### Add-hoc JSON requests
164
164
 
165
165
  If you just want to make a few quick requests from the console, you can instantiate a plain client and make `.json` calls:
166
166
 
167
167
  ```ruby
168
- c = Twirp::Client.new("http://localhost:3000", package: "example", service: "HelloWorld")
169
- resp = c.json(:Hello, name: "World")
170
- puts resp.data["message"]
168
+ c = Twirp::Client.new(conn, package: "example", service: "HelloWorld")
169
+ resp = c.json(:Hello, name: "World") # serialized as JSON, resp.data is a Hash
171
170
  ```
172
171
 
173
172
 
@@ -11,21 +11,97 @@ module Twirp
11
11
  # DSL to define a client with package, service and rpcs.
12
12
  extend ServiceDSL
13
13
 
14
- # DSL (alternative) to define a client from a Service class.
15
- def self.client_for(svclass)
16
- package svclass.package_name
17
- service svclass.service_name
18
- svclass.rpcs.each do |rpc_method, rpcdef|
19
- rpc rpc_method, rpcdef[:input_class], rpcdef[:output_class], ruby_method: rpcdef[:ruby_method]
14
+ class << self # class methods
15
+
16
+ # DSL (alternative) to define a client from a Service class.
17
+ def client_for(svclass)
18
+ package svclass.package_name
19
+ service svclass.service_name
20
+ svclass.rpcs.each do |rpc_method, rpcdef|
21
+ rpc rpc_method, rpcdef[:input_class], rpcdef[:output_class], ruby_method: rpcdef[:ruby_method]
22
+ end
20
23
  end
21
- end
22
24
 
23
- # Hook for ServiceDSL#rpc to define a new method client.<ruby_method>(input, opts).
24
- def self.rpc_define_method(rpcdef)
25
- define_method rpcdef[:ruby_method] do |input|
26
- rpc(rpcdef[:rpc_method], input)
25
+ # Hook for ServiceDSL#rpc to define a new method client.<ruby_method>(input, opts).
26
+ def rpc_define_method(rpcdef)
27
+ unless method_defined? rpcdef[:ruby_method] # collision with existing rpc method
28
+ define_method rpcdef[:ruby_method] do |input|
29
+ rpc(rpcdef[:rpc_method], input)
30
+ end
31
+ end
27
32
  end
28
- end
33
+
34
+ def error_from_response(resp)
35
+ status = resp.status
36
+
37
+ if is_http_redirect? status
38
+ return twirp_redirect_error(status, resp.headers['Location'])
39
+ end
40
+
41
+ err_attrs = nil
42
+ begin
43
+ err_attrs = Encoding.decode_json(resp.body)
44
+ rescue JSON::ParserError
45
+ return twirp_error_from_intermediary(status, "Response is not JSON", resp.body)
46
+ end
47
+
48
+ code = err_attrs["code"]
49
+ if code.to_s.empty?
50
+ return twirp_error_from_intermediary(status, "Response is JSON but it has no \"code\" attribute", resp.body)
51
+ end
52
+ code = code.to_s.to_sym
53
+ if !Twirp::Error.valid_code?(code)
54
+ return Twirp::Error.internal("Invalid Twirp error code: #{code}", invalid_code: code.to_s, body: resp.body)
55
+ end
56
+
57
+ Twirp::Error.new(code, err_attrs["msg"], err_attrs["meta"])
58
+ end
59
+
60
+ # Error that was caused by an intermediary proxy like a load balancer.
61
+ # The HTTP errors code from non-twirp sources is mapped to equivalent twirp errors.
62
+ # The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md.
63
+ # Returned twirp Errors have some additional metadata for inspection.
64
+ def twirp_error_from_intermediary(status, reason, body)
65
+ code = case status
66
+ when 400 then :internal
67
+ when 401 then :unauthenticated
68
+ when 403 then :permission_denied
69
+ when 404 then :bad_route
70
+ when 429, 502, 503, 504 then :unavailable
71
+ else :unknown
72
+ end
73
+
74
+ Twirp::Error.new(code, code.to_s, {
75
+ http_error_from_intermediary: "true",
76
+ not_a_twirp_error_because: reason,
77
+ status_code: status.to_s,
78
+ body: body.to_s,
79
+ })
80
+ end
81
+
82
+ # Twirp clients should not follow redirects automatically, Twirp only handles
83
+ # POST requests, redirects should only happen on GET and HEAD requests.
84
+ def twirp_redirect_error(status, location)
85
+ msg = "Unexpected HTTP Redirect from location=#{location}"
86
+ Twirp::Error.new(:internal, msg, {
87
+ http_error_from_intermediary: "true",
88
+ not_a_twirp_error_because: "Redirects not allowed on Twirp requests",
89
+ status_code: status.to_s,
90
+ location: location.to_s,
91
+ })
92
+ end
93
+
94
+ def is_http_redirect?(status)
95
+ status >= 300 && status <= 399
96
+ end
97
+
98
+ def rpc_path(service_full_name, rpc_method)
99
+ "/#{service_full_name}/#{rpc_method}"
100
+ end
101
+
102
+ end # class << self
103
+
104
+
29
105
 
30
106
  # Init with a Faraday connection, or a base_url that is used in a default connection.
31
107
  # Clients use Content-Type="application/protobuf" by default. For JSON clinets use :content_type => "application/json".
@@ -48,10 +124,6 @@ module Twirp
48
124
  end
49
125
  end
50
126
 
51
- def rpc_path(rpc_method)
52
- "/#{@service_full_name}/#{rpc_method}"
53
- end
54
-
55
127
  # Make a remote procedure call to a defined rpc_method. The input can be a Proto message instance,
56
128
  # or the attributes (Hash) to instantiate it. Returns a ClientResp instance with an instance of
57
129
  # output_class, or a Twirp::Error. The input and output classes are the ones configued with the rpc DSL.
@@ -66,13 +138,13 @@ module Twirp
66
138
  body = Encoding.encode(input, rpcdef[:input_class], @content_type)
67
139
 
68
140
  resp = @conn.post do |r|
69
- r.url rpc_path(rpc_method)
141
+ r.url "/#{@service_full_name}/#{rpc_method}"
70
142
  r.headers['Content-Type'] = @content_type
71
143
  r.body = body
72
144
  end
73
145
 
74
146
  if resp.status != 200
75
- return ClientResp.new(nil, error_from_response(resp))
147
+ return ClientResp.new(nil, self.class.error_from_response(resp))
76
148
  end
77
149
 
78
150
  if resp.headers['Content-Type'] != @content_type
@@ -91,83 +163,18 @@ module Twirp
91
163
  body = Encoding.encode_json(attrs)
92
164
 
93
165
  resp = @conn.post do |r|
94
- r.url rpc_path(rpc_method)
166
+ r.url "/#{@service_full_name}/#{rpc_method}"
95
167
  r.headers['Content-Type'] = Encoding::JSON
96
168
  r.body = body
97
169
  end
98
170
 
99
171
  if resp.status != 200
100
- return ClientResp.new(nil, error_from_response(resp))
172
+ return ClientResp.new(nil, self.class.error_from_response(resp))
101
173
  end
102
174
 
103
175
  data = Encoding.decode_json(resp.body)
104
176
  return ClientResp.new(data, nil)
105
177
  end
106
-
107
- def error_from_response(resp)
108
- status = resp.status
109
-
110
- if is_http_redirect? status
111
- return twirp_redirect_error(status, resp.headers['Location'])
112
- end
113
-
114
- err_attrs = nil
115
- begin
116
- err_attrs = JSON.parse(resp.body)
117
- rescue JSON::ParserError
118
- return twirp_error_from_intermediary(status, "Response is not JSON", resp.body)
119
- end
120
-
121
- code = err_attrs["code"]
122
- if code.to_s.empty?
123
- return twirp_error_from_intermediary(status, "Response is JSON but it has no \"code\" attribute", resp.body)
124
- end
125
- code = code.to_s.to_sym
126
- if !Twirp::Error.valid_code?(code)
127
- return Twirp::Error.internal("Invalid Twirp error code: #{code}", invalid_code: code.to_s, body: resp.body)
128
- end
129
-
130
- Twirp::Error.new(code, err_attrs["msg"], err_attrs["meta"])
131
- end
132
-
133
- # Error that was caused by an intermediary proxy like a load balancer.
134
- # The HTTP errors code from non-twirp sources is mapped to equivalent twirp errors.
135
- # The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md.
136
- # Returned twirp Errors have some additional metadata for inspection.
137
- def twirp_error_from_intermediary(status, reason, body)
138
- code = case status
139
- when 400 then :internal
140
- when 401 then :unauthenticated
141
- when 403 then :permission_denied
142
- when 404 then :bad_route
143
- when 429, 502, 503, 504 then :unavailable
144
- else :unknown
145
- end
146
-
147
- Twirp::Error.new(code, code.to_s, {
148
- http_error_from_intermediary: "true",
149
- not_a_twirp_error_because: reason,
150
- status_code: status.to_s,
151
- body: body.to_s,
152
- })
153
- end
154
-
155
- # Twirp clients should not follow redirects automatically, Twirp only handles
156
- # POST requests, redirects should only happen on GET and HEAD requests.
157
- def twirp_redirect_error(status, location)
158
- msg = "Unexpected HTTP Redirect from location=#{location}"
159
- Twirp::Error.new(:internal, msg, {
160
- http_error_from_intermediary: "true",
161
- not_a_twirp_error_because: "Redirects not allowed on Twirp requests",
162
- status_code: status.to_s,
163
- location: location.to_s,
164
- })
165
- end
166
-
167
- def is_http_redirect?(status)
168
- status >= 300 && status <= 399
169
- end
170
-
171
178
  end
172
179
 
173
180
  class ClientResp
@@ -1,3 +1,3 @@
1
1
  module Twirp
2
- VERSION = "0.4.0"
2
+ VERSION = "0.4.1"
3
3
  end
@@ -27,6 +27,28 @@ class ClientTest < Minitest::Test
27
27
  end
28
28
  end
29
29
 
30
+ def test_dsl_method_definition_collision
31
+ # To avoid collisions, the Twirp::Client class should have very few methods
32
+ mthds = Twirp::Client.instance_methods(false)
33
+ assert_equal [:json, :rpc], mthds
34
+
35
+ # If one of the methods is being implemented through the DSL, the colision should be avoided
36
+ num_mthds = EmptyClient.instance_methods.size
37
+ EmptyClient.rpc :Json, Example::Empty, Example::Empty, :ruby_method => :json
38
+ assert_equal num_mthds, EmptyClient.instance_methods.size # no new method was added (collision)
39
+
40
+ # Make sure that the previous .json method was not modified
41
+ c = EmptyClient.new(conn_stub("/EmptyClient/Json") {|req|
42
+ [200, {}, json(foo: "bar")]
43
+ })
44
+ resp = c.json(:Json, foo: "bar")
45
+ assert_equal "bar", resp.data["foo"]
46
+
47
+ # Adding any other rpc would work as expected
48
+ EmptyClient.rpc :Other, Example::Empty, Example::Empty, :ruby_method => :other
49
+ assert_equal num_mthds + 1, EmptyClient.instance_methods.size # new method added
50
+ end
51
+
30
52
 
31
53
  # Call .rpc on Protobuf client
32
54
  # ----------------------------
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.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyrus A. Forbes