Class: Twirp::Client

Inherits:
Object
  • Object
show all
Extended by:
ServiceDSL
Defined in:
lib/twirp/client.rb

Class Method Summary collapse

Instance Method Summary collapse

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

Returns:

  • (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