Class: Twirp::Client
- Inherits:
-
Object
- Object
- Twirp::Client
- Extended by:
- ServiceDSL
- Defined in:
- lib/twirp/client.rb
Class Method Summary collapse
-
.client_for(svclass) ⇒ Object
DSL (alternative) to define a client from a Service class.
- .error_from_response(resp) ⇒ Object
- .is_http_redirect?(status) ⇒ Boolean
-
.rpc_define_method(rpcdef) ⇒ Object
Hook for ServiceDSL#rpc to define a new method client.<ruby_method>(input, opts).
- .rpc_path(service_full_name, rpc_method) ⇒ Object
-
.twirp_error_from_intermediary(status, reason, body) ⇒ Object
Error that was caused by an intermediary proxy like a load balancer.
-
.twirp_redirect_error(status, location) ⇒ Object
Twirp clients should not follow redirects automatically, Twirp only handles POST requests, redirects should only happen on GET and HEAD requests.
Instance Method Summary collapse
-
#initialize(conn, opts = {}) ⇒ Client
constructor
Init with a Faraday connection, or a base_url that is used in a default connection.
-
#json(rpc_method, attrs = {}) ⇒ Object
Convenience method to call any rpc method with dynamic json attributes.
-
#rpc(rpc_method, input) ⇒ Object
Make a remote procedure call to a defined rpc_method.
Methods included from ServiceDSL
package, package_name, rpcs, service, service_full_name, service_name
Constructor Details
#initialize(conn, opts = {}) ⇒ Client
Init with a Faraday connection, or a base_url that is used in a default connection. Clients use Content-Type=“application/protobuf” by default. For JSON clinets use :content_type => “application/json”.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/twirp/client.rb', line 108 def initialize(conn, opts={}) @conn = case conn when String then Faraday.new(url: conn) # init with hostname when Faraday::Connection then conn # init with connection else raise ArgumentError.new("Invalid conn #{conn.inspect}. Expected String hostname or Faraday::Connection") end @content_type = (opts[:content_type] || Encoding::PROTO) if !Encoding.valid_content_type?(@content_type) raise ArgumentError.new("Invalid content_type #{@content_type.inspect}. Expected one of #{Encoding.valid_content_types.inspect}") end @service_full_name = if opts[:package] || opts[:service] opts[:package].to_s.empty? ? opts[:service].to_s : "#{opts[:package]}.#{opts[:service]}" else self.class.service_full_name # defined through DSL end end |
Class Method Details
.client_for(svclass) ⇒ Object
DSL (alternative) to define a client from a Service class.
17 18 19 20 21 22 23 |
# File 'lib/twirp/client.rb', line 17 def client_for(svclass) package svclass.package_name service svclass.service_name svclass.rpcs.each do |rpc_method, rpcdef| rpc rpc_method, rpcdef[:input_class], rpcdef[:output_class], ruby_method: rpcdef[:ruby_method] end end |
.error_from_response(resp) ⇒ Object
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/twirp/client.rb', line 34 def error_from_response(resp) status = resp.status if is_http_redirect? status return twirp_redirect_error(status, resp.headers['Location']) end err_attrs = nil begin err_attrs = Encoding.decode_json(resp.body) rescue JSON::ParserError return twirp_error_from_intermediary(status, "Response is not JSON", resp.body) end code = err_attrs["code"] if code.to_s.empty? return twirp_error_from_intermediary(status, "Response is JSON but it has no \"code\" attribute", resp.body) end code = code.to_s.to_sym if !Twirp::Error.valid_code?(code) return Twirp::Error.internal("Invalid Twirp error code: #{code}", invalid_code: code.to_s, body: resp.body) end Twirp::Error.new(code, err_attrs["msg"], err_attrs["meta"]) end |
.is_http_redirect?(status) ⇒ Boolean
94 95 96 |
# File 'lib/twirp/client.rb', line 94 def is_http_redirect?(status) status >= 300 && status <= 399 end |
.rpc_define_method(rpcdef) ⇒ Object
Hook for ServiceDSL#rpc to define a new method client.<ruby_method>(input, opts).
26 27 28 29 30 31 32 |
# File 'lib/twirp/client.rb', line 26 def rpc_define_method(rpcdef) unless method_defined? rpcdef[:ruby_method] # collision with existing rpc method define_method rpcdef[:ruby_method] do |input| rpc(rpcdef[:rpc_method], input) end end end |
.rpc_path(service_full_name, rpc_method) ⇒ Object
98 99 100 |
# File 'lib/twirp/client.rb', line 98 def rpc_path(service_full_name, rpc_method) "/#{service_full_name}/#{rpc_method}" end |
.twirp_error_from_intermediary(status, reason, body) ⇒ Object
Error that was caused by an intermediary proxy like a load balancer. The HTTP errors code from non-twirp sources is mapped to equivalent twirp errors. The mapping is similar to gRPC: github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md. Returned twirp Errors have some additional metadata for inspection.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/twirp/client.rb', line 64 def twirp_error_from_intermediary(status, reason, body) code = case status when 400 then :internal when 401 then :unauthenticated when 403 then :permission_denied when 404 then :bad_route when 429, 502, 503, 504 then :unavailable else :unknown end Twirp::Error.new(code, code.to_s, { http_error_from_intermediary: "true", not_a_twirp_error_because: reason, status_code: status.to_s, body: body.to_s, }) end |
.twirp_redirect_error(status, location) ⇒ Object
Twirp clients should not follow redirects automatically, Twirp only handles POST requests, redirects should only happen on GET and HEAD requests.
84 85 86 87 88 89 90 91 92 |
# File 'lib/twirp/client.rb', line 84 def twirp_redirect_error(status, location) msg = "Unexpected HTTP Redirect from location=#{location}" Twirp::Error.new(:internal, msg, { http_error_from_intermediary: "true", not_a_twirp_error_because: "Redirects not allowed on Twirp requests", status_code: status.to_s, location: location.to_s, }) end |
Instance Method Details
#json(rpc_method, attrs = {}) ⇒ Object
Convenience method to call any rpc method with dynamic json attributes. It is like .rpc but does not use the defined Protobuf messages to serialize/deserialize data; the request attrs can be anything and the response data is always a plain Hash of attributes. This is useful to test a service before doing any code-generation.
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/twirp/client.rb', line 162 def json(rpc_method, attrs={}) body = Encoding.encode_json(attrs) resp = @conn.post do |r| r.url "/#{@service_full_name}/#{rpc_method}" r.headers['Content-Type'] = Encoding::JSON r.body = body end if resp.status != 200 return ClientResp.new(nil, self.class.error_from_response(resp)) end data = Encoding.decode_json(resp.body) return ClientResp.new(data, nil) end |
#rpc(rpc_method, input) ⇒ Object
Make a remote procedure call to a defined rpc_method. The input can be a Proto message instance, or the attributes (Hash) to instantiate it. Returns a ClientResp instance with an instance of output_class, or a Twirp::Error. The input and output classes are the ones configued with the rpc DSL. If rpc_method was not defined with the rpc DSL then a response with a bad_route error is returned instead.
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/twirp/client.rb', line 131 def rpc(rpc_method, input) rpcdef = self.class.rpcs[rpc_method.to_s] if !rpcdef return ClientResp.new(nil, Twirp::Error.bad_route("rpc not defined on this client")) end input = rpcdef[:input_class].new(input) if input.is_a? Hash body = Encoding.encode(input, rpcdef[:input_class], @content_type) resp = @conn.post do |r| r.url "/#{@service_full_name}/#{rpc_method}" r.headers['Content-Type'] = @content_type r.body = body end if resp.status != 200 return ClientResp.new(nil, self.class.error_from_response(resp)) end if resp.headers['Content-Type'] != @content_type return ClientResp.new(nil, Twirp::Error.internal("Expected response Content-Type #{@content_type.inspect} but found #{resp.headers['Content-Type'].inspect}")) end data = Encoding.decode(resp.body, rpcdef[:output_class], @content_type) return ClientResp.new(data, nil) end |