Module: GrpcRest

Defined in:
lib/grpc_rest.rb,
lib/base_interceptor.rb,
lib/generator/method.rb,
lib/generator/service.rb,
lib/grpc_rest/version.rb,
lib/generator/controller.rb

Defined Under Namespace

Classes: BaseInterceptor, Controller, GeneratedMethod, GeneratedService, GrpcRestCall, Railtie

Constant Summary collapse

VERSION =
'0.5.1'

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.servicesObject

Returns the value of attribute services.



14
15
16
# File 'lib/grpc_rest.rb', line 14

def services
  @services
end

.strict_modeObject

Returns the value of attribute strict_mode.



13
14
15
# File 'lib/grpc_rest.rb', line 13

def strict_mode
  @strict_mode
end

Class Method Details

.assign_params(request, param_hash, body_string, params) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/grpc_rest.rb', line 137

def assign_params(request, param_hash, body_string, params)
  parameters = params.to_h.deep_dup
  # each instance of {variable} means that we set the corresponding param variable into the
  # Protobuf request
  # The variable pattern could have dots which indicate we need to drill into the request
  # to set it - e.g. {subrecord.foo} means we need to set the value of `request.subrecord.foo` to `params[:foo].`
  # We can also do simple wildcard replacement if there's a * - for example, {name=something-*}
  # means we should set `request.name` to "something-#{params[:name]}".
  param_hash.each do |entry|
    name_tokens = entry[:split_name]
    value_to_use = parameters.delete(name_tokens.last)
    if entry[:val]
      regex = entry[:val].tr('*', '')
      value_to_use = value_to_use.gsub(regex, '')
    end
    assign_value(request, entry[:name], value_to_use)
  end
  return unless body_string.present? && body_string != '*'

  # we need to "splat" the body parameters into the given sub-record rather than into the top-level.
  sub_field(request, body_string, parameters)
end

.assign_value(proto, path, value) ⇒ Object



39
40
41
42
43
44
45
46
# File 'lib/grpc_rest.rb', line 39

def assign_value(proto, path, value)
  tokens = path.split('.')
  tokens[0...-1].each do |path_seg|
    proto = sub_field(proto, path_seg, {})
    return if proto.nil?
  end
  proto.public_send(:"#{tokens.last}=", value) if proto.respond_to?(:"#{tokens.last}=")
end

.controller(service) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/generator/controller.rb', line 22

def self.controller(service)
  # e.g. GrpcRestControllers::MyServiceController
  klass = Class.new(GrpcRest::Controller) do
    service.methods.each do |method|
      # e.g. def my_service_test_method
    	define_method(method.name) do
        parameters = request.parameters.to_h.deep_transform_keys(&:underscore).
          except('controller', 'action', service.name.underscore)
        grpc_request = GrpcRest.init_request(method.request_type, parameters)
        GrpcRest.assign_params(grpc_request,
                               method.path_info.map(&:to_h),
                               method.option_body,
                               request.parameters)
        render json: GrpcRest.send_request(service.service,
                                           method.name.underscore,
                                           grpc_request,
                                           method.rest_options,
                                           headers: request.headers)
      end
      Rails.application.routes.append do
        service_name = service.name.demodulize.underscore
        path = "grpc_rest_controllers/#{service_name}##{method.name.underscore}"
        # e.g. get '/my_service/test_method' => '/grpc_rest_controllers/my_service#test_method'
        self.send(method.http_method, method.sanitized_path => path)
      end
    end
  end
  GrpcRestControllers.const_set("#{service.name.demodulize}Controller", klass)
end

.error_msg(error) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/grpc_rest.rb', line 192

def error_msg(error)
  error_info = "#{error.message}, backtrace: #{error.backtrace.join("\n")}"
  if error.respond_to?(:cause) && error.cause
    error_info += "\n\nCaused by: " + error.cause.backtrace.join("\n")
  end
  Rails.logger.error(error_info)
  if error.respond_to?(:code)
    {
      code: error.code,
      message: error.message,
      details: [
        {
          backtrace: error_info
        }
      ]
    }
  else
    {
      code: 3,
      message: "InvalidArgument: #{error.message}",
      details: [
        {
          backtrace: error.backtrace
        }
      ]
    }
  end
end

.get_response(service, method, request, headers: {}) ⇒ Object



257
258
259
260
261
262
263
264
265
# File 'lib/grpc_rest.rb', line 257

def get_response(service, method, request, headers: {})
  if defined?(Gruf)
    klass = ::Gruf::Controllers::Base.subclasses.find do |k|
      k.bound_service == service
    end
    return send_gruf_request(klass, service, method, request, headers: headers) if klass
  end
  send_grpc_request(service, method, request)
end

.grpc_http_status(code) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/grpc_rest.rb', line 161

def grpc_http_status(code)
  case code
  when GRPC::Core::StatusCodes::OK
    :ok
  when GRPC::Core::StatusCodes::CANCELLED
    499
  when GRPC::Core::StatusCodes::INVALID_ARGUMENT,
    GRPC::Core::StatusCodes::FAILED_PRECONDITION,
    GRPC::Core::StatusCodes::OUT_OF_RANGE
    :bad_request
  when GRPC::Core::StatusCodes::DEADLINE_EXCEEDED
    :gateway_timeout
  when GRPC::Core::StatusCodes::NOT_FOUND
    :not_found
  when GRPC::Core::StatusCodes::ALREADY_EXISTS, GRPC::Core::StatusCodes::ABORTED
    :conflict
  when GRPC::Core::StatusCodes::PERMISSION_DENIED
    :forbidden
  when GRPC::Core::StatusCodes::UNAUTHENTICATED
    :unauthorized
  when GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED
    :too_many_requests
  when GRPC::Core::StatusCodes::UNIMPLEMENTED
    :not_implemented
  when GRPC::Core::StatusCodes::UNAVAILABLE
    :service_unavailable
  else
    :internal_server_error
  end
end

.gruf_interceptors(request) ⇒ Array<Gruf::Interceptors::Base>

Parameters:

  • request (Gruf::Controllers::Request)

Returns:

  • (Array<Gruf::Interceptors::Base>)


245
246
247
248
249
250
# File 'lib/grpc_rest.rb', line 245

def gruf_interceptors(request)
  error = Gruf::Error.new
  interceptors = Gruf.interceptors.prepare(request, error)
  interceptors.delete_if { |k| k.class.name.split('::').first == 'Gruf' }
  interceptors
end

.handle_enum_values(descriptor, value) ⇒ Object



65
66
67
68
69
70
71
72
73
# File 'lib/grpc_rest.rb', line 65

def handle_enum_values(descriptor, value)
  names = descriptor.subtype.to_h.keys.map(&:to_s)
  prefix = longest_common_substring(names)
  if prefix.present? && !value.starts_with?(prefix)
    "#{prefix}#{value}"
  else
    value
  end
end

.init_request(request_class, params) ⇒ Object



128
129
130
131
132
133
134
135
# File 'lib/grpc_rest.rb', line 128

def init_request(request_class, params)
  map_proto_record(request_class.descriptor, params)
  if GrpcRest.strict_mode
    request_class.decode_json(JSON.generate(params))
  else
    request_class.decode_json(JSON.generate(params), ignore_unknown_fields: true)
  end
end

.longest_common_substring(arr) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/grpc_rest.rb', line 49

def longest_common_substring(arr)
  return '' if arr.empty?

  result = 0
  first_val = arr[0]
  (0...first_val.length).each do |k|
    all_matched = true
    character = first_val[k]
    arr.each { |str| all_matched &= (character == str[k]) }
    break unless all_matched

    result += 1
  end
  first_val.slice(0, result)
end

.map_proto_record(proto, params) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/grpc_rest.rb', line 109

def map_proto_record(proto, params)
  proto.to_a.each do |descriptor|
    field = descriptor.name
    val = params[field]
    next if val.nil?

    if descriptor.label == :repeated
      # leave map entries as key => value
      unless descriptor.subtype&.options&.to_h&.dig(:map_entry)
        params[field] = Array.wrap(val).map { |v| map_proto_type(descriptor, v) }
      end
    else
      params[field] = map_proto_type(descriptor, val)
    end
  end

  params
end

.map_proto_type(descriptor, val) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/grpc_rest.rb', line 75

def map_proto_type(descriptor, val)
  return handle_enum_values(descriptor, val) if descriptor.subtype.is_a?(Google::Protobuf::EnumDescriptor)

  case descriptor.type
  when :int32, :int64, :uint32, :uint64, :sint32, :sint64, :fixed32, :fixed64, :sfixed32, :sfixed64
    return val.to_i
  when :float, :double
    return val.to_f
  when :bool
    return !!val
  end

  case descriptor.subtype&.name
  when 'google.protobuf.Struct'
    Google::Protobuf::Struct.from_hash(val)
  when 'google.protobuf.Timestamp'
    return Google::Protobuf::Timestamp.from_time(Time.at(val)) if val.is_a?(Numeric)

    begin
      parsed_time = Time.new(val)
      Google::Protobuf::Timestamp.from_time(parsed_time)
    rescue ArgumentError => e
      raise ArgumentError, "Could not parse time value '#{val}': #{e.message}"
    end

  when 'google.protobuf.Value'
    Google::Protobuf::Value.from_ruby(val)
  when 'google.protobuf.ListValue'
    Google::Protobuf::ListValue.from_a(val)
  else
    map_proto_record(descriptor.subtype, val)
  end
end

.send_grpc_request(service, method, request) ⇒ Object



252
253
254
255
# File 'lib/grpc_rest.rb', line 252

def send_grpc_request(service, method, request)
  klass = service.subclasses.first
  klass.new.public_send(method, request)
end

.send_gruf_request(klass, service_obj, method, request, headers: {}) ⇒ Object



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/grpc_rest.rb', line 221

def send_gruf_request(klass, service_obj, method, request, headers: {})
  ref = service_obj.rpc_descs[method.classify.to_sym]
  call = GrpcRestCall.new(headers)
  handler = klass.new(
    method_key: method.to_sym,
    service: service_obj,
    rpc_desc: ref,
    active_call: GrpcRestCall.new(headers),
    message: request
  )
  controller_request = Gruf::Controllers::Request.new(
    method_key: method.to_sym,
    service: service_obj,
    rpc_desc: ref,
    active_call: call,
    message: request
  )
  Gruf::Interceptors::Context.new(gruf_interceptors(controller_request)).intercept! do
    handler.send(method.to_sym)
  end
end

.send_request(service, method, request, options = {}, headers: {}) ⇒ Object



267
268
269
270
271
272
273
274
# File 'lib/grpc_rest.rb', line 267

def send_request(service, method, request, options = {}, headers: {})
  response = get_response(service, method, request, headers: headers)
  if options[:emit_defaults]
    response.to_json(emit_defaults: true)
  else
    response
  end
end

.sub_field(proto, name, parameters) ⇒ Object

Gets a sub record from a proto. If it doesn’t exist, initialize it and set it to the proto, then return it.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/grpc_rest.rb', line 22

def sub_field(proto, name, parameters)
  existing = proto.public_send(name.to_sym)
  return existing if existing

  descriptor = proto.class.descriptor.to_a.find { |a| a.name == name }
  return nil if descriptor.nil?

  klass = descriptor.submsg_name.split('.').map(&:camelize).join('::').constantize
  params = klass.descriptor.to_a.map(&:name).to_h do |key|
    [key, parameters.delete(key)]
  end

  sub_record = klass.new(params)
  proto.public_send(:"#{name}=", sub_record)
  sub_record
end

.underscore(s) ⇒ Object



16
17
18
# File 'lib/grpc_rest.rb', line 16

def underscore(s)
  GRPC::GenericService.underscore(s)
end