Module: Cel::Protobuf

Defined in:
lib/cel/ast/elements/protobuf.rb

Defined Under Namespace

Modules: CelComparisonMode Classes: Enum

Constant Summary collapse

PROTOBUF_VALUES =
Set.new(
  ["Any", "google.protobuf.Any",
   "ListValue", "google.protobuf.ListValue",
   "Struct", "google.protobuf.Struct",
   "Value", "google.protobuf.Value",
   "BoolValue", "google.protobuf.BoolValue",
   "BytesValue", "google.protobuf.BytesValue",
   "DoubleValue", "google.protobuf.DoubleValue",
   "FloatValue", "google.protobuf.FloatValue",
   "Int32Value", "google.protobuf.Int32Value",
   "Int64Value", "google.protobuf.Int64Value",
   "Uint32Value", "google.protobuf.Uint32Value",
   "Uint64Value", "google.protobuf.Uint64Value",
   "NullValue", "google.protobuf.NullValue",
   "StringValue", "google.protobuf.StringValue",
   "Timestamp", "google.protobuf.Timestamp",
   "Duration", "google.protobuf.Duration"]
)
PROTO_TO_CEL_TYPE =
{
  Google::Protobuf::Any => TYPES[:any],
  Google::Protobuf::Value => TYPES[:any],
  Google::Protobuf::ListValue => TYPES[:list],
  Google::Protobuf::Struct => TYPES[:map],
  Google::Protobuf::BoolValue => TYPES[:bool],
  Google::Protobuf::BytesValue => TYPES[:bytes],
  Google::Protobuf::DoubleValue => TYPES[:double],
  Google::Protobuf::FloatValue => TYPES[:double],
  Google::Protobuf::Int32Value => TYPES[:int],
  Google::Protobuf::Int64Value => TYPES[:int],
  Google::Protobuf::UInt32Value => TYPES[:uint],
  Google::Protobuf::UInt64Value => TYPES[:uint],
  Google::Protobuf::NullValue => TYPES[:null],
  Google::Protobuf::StringValue => TYPES[:string],
  Google::Protobuf::Timestamp => TYPES[:timestamp],
  Google::Protobuf::Duration => TYPES[:duration],
}.freeze
NILLABLE_WRAPPERS =
(PROTO_TO_CEL_TYPE.keys - [Google::Protobuf::Value, Google::Protobuf::ListValue,
Google::Protobuf::Struct, Google::Protobuf::Duration,
Google::Protobuf::Timestamp]).freeze

Class Method Summary collapse

Class Method Details

.base_classObject



32
33
34
# File 'lib/cel/ast/elements/protobuf.rb', line 32

def base_class
  Google::Protobuf::MessageExts
end

.convert_from_proto_field_type(field, value) ⇒ Object



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/cel/ast/elements/protobuf.rb', line 352

def convert_from_proto_field_type(field, value)
  if value.nil? && (field.type == :message)
    subtype = field.subtype.msgclass

    if NILLABLE_WRAPPERS.include?(subtype) ||
       subtype == Google::Protobuf::Value
      # if a wrapper type, ignore null
    else
      value = subtype.new
    end
  end

  case field.type
  when :message
    value
  when :enum
    enum_type = const_get(Cel.package_to_module_name(field.submsg_name))

    return Enum.new(value, enum_type) if value.is_a?(Integer)

    enum_mod = Cel.package_to_module_name(field.submsg_name)

    Enum.new(const_get(enum_mod).resolve(value), enum_type)
  when :int32, :int64, :sint32, :sint64, :sfixed32, :sfixed64
    return unless value

    Number.new(:int, value)
  when :uint32, :uint64, :fixed32, :fixed64
    return unless value

    Number.new(:uint, value)
  when :float, :double
    return unless value

    Number.new(:double, value)
  when :bool
    return unless value

    Bool.cast(value)
  when :string
    return unless value

    String.new(value)
  when :bytes
    return unless value

    Bytes.new(value)
  end
end

.convert_to_cel_type(proto, package) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/cel/ast/elements/protobuf.rb', line 109

def convert_to_cel_type(proto, package)
  case proto
  when Google::Protobuf::FieldDescriptor

    cel_type =
      case proto.type
      when :int32, :int64, :sint32, :sint64, :sfixed32, :sfixed64, :enum
        TYPES[:int]
      when :uint32, :uint64, :fixed32, :fixed64
        TYPES[:uint]
      when :float, :double
        TYPES[:double]
      when :bool
        TYPES[:bool]
      when :string
        TYPES[:string]
      when :bytes
        TYPES[:bytes]
      when :message
        proto_type = convert_to_proto_type(proto.submsg_name, package)

        proto_type = PROTO_TO_CEL_TYPE[proto_type] if PROTO_TO_CEL_TYPE.key?(proto_type)
        proto_type
      end

    if proto.label == :repeated
      return TYPES[:list, cel_type] unless proto.subtype

      case proto.subtype.options
      when Google::Protobuf::EnumOptions
        return TYPES[:list, cel_type]
      when Google::Protobuf::MessageOptions
        # protobuf doesn't have a way to identify whether this field descriptors is from a map,
        # nor the types for key and values.
        return TYPES[:map, cel_type] if proto.subtype.options.map_entry
      end

      return TYPES[:list, cel_type]
    end

    cel_type
  else
    PROTO_TO_CEL_TYPE[proto]
  end
end

.convert_to_enum(value, proto_type = nil) ⇒ Object



307
308
309
310
311
312
313
# File 'lib/cel/ast/elements/protobuf.rb', line 307

def convert_to_enum(value, proto_type = nil)
  if proto_type
    Enum.new(value, proto_type)
  else
    Enum.new(value)
  end
end

.convert_to_proto(proto_type, values, convert_value_to_proto: false) ⇒ Object



155
156
157
158
159
160
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
191
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/cel/ast/elements/protobuf.rb', line 155

def convert_to_proto(proto_type, values, convert_value_to_proto: false)
  return values.unpack(proto_type) if values.is_a?(Google::Protobuf::Any)

  if proto_type == Google::Protobuf::Struct
    values = values[:fields] if values.key?(:fields)

    raise EvaluateError, "bad key type" unless values.all? { |k, _| k.is_a?(::String) }

    proto_type.from_hash(values)
  elsif proto_type == Google::Protobuf::ListValue
    if values.is_a?(Hash) && values.key?(:values)
      proto_type.from_a(values[:values])
    else
      proto_type.from_a(values)
    end
  elsif proto_type == Google::Protobuf::NullValue
    nil
  elsif proto_type == Google::Protobuf::Value
    case values
    when Google::Protobuf::Duration
      proto_type.new(string_value: "#{values.seconds}#{if values.nanos.positive?
                                                         ".#{values.nanos / 1_000_000_000.0}"
                                                       end}s")
    when Google::Protobuf::Timestamp
      proto_type.new(string_value: values.to_time.utc.iso8601(9))
    when Hash
      if values.key?(:struct_value)

        raise EvaluateError, "bad key type" unless values[:struct_value].all? { |k, _| k.is_a?(::String) }

        values[:struct_value] = Google::Protobuf::Struct.from_hash(values[:struct_value])
      elsif values.key?(:list_value)
        values[:list_value] = Google::Protobuf::ListValue.from_a(values[:list_value])
      elsif values.empty?
        if convert_value_to_proto
          values = { struct_value: Google::Protobuf::Struct.from_hash(values) }
        else
          values[:null_value] = Google::Protobuf::NullValue::NULL_VALUE
        end
      elsif values.any? { |k, _| proto_type.descriptor.lookup(k.to_s).nil? }
        raise EvaluateError, "bad key type" unless values.all? { |k, _| k.is_a?(::String) }

        values = { struct_value: Google::Protobuf::Struct.from_hash(values) }
      end
      proto_type.new(values)
    when Google::Protobuf::BoolValue
      proto_type.new(bool_value: values.value)
    when Google::Protobuf::DoubleValue,
         Google::Protobuf::FloatValue,
         Google::Protobuf::Int32Value,
         Google::Protobuf::UInt32Value
      proto_type.new(number_value: values.value)
    when Google::Protobuf::Int64Value,
         Google::Protobuf::UInt64Value
      if values.value >= MAX_INT - 1
        proto_type.new(string_value: values.value.to_s)
      else
        proto_type.new(number_value: values.value)
      end
    when Google::Protobuf::StringValue
      proto_type.new(string_value: values.value)
    when Google::Protobuf::BytesValue
      proto_type.new(string_value: [values.value].pack("m0"))
    when Google::Protobuf::NullValue
      proto_type.new(null_value: Google::Protobuf::NullValue::NULL_VALUE)
    when Google::Protobuf::Struct
      proto_type.new(struct_value: values.value)
    when Google::Protobuf::ListValue
      proto_type.new(list_value: values.value)
    when Google::Protobuf::Empty
      proto_type.from_ruby({})
    when Google::Protobuf::FieldMask
      proto_type.from_ruby(values.paths.join(","))
    else
      proto_type.from_ruby(values)
    end
  elsif proto_type == Google::Protobuf::Any
    case values
    when Array
      proto_type.pack(Google::Protobuf::ListValue.from_a(values))
    when Hash
      # type_url: "", value: ""
      # TODO: consider pattern matching
      if convert_value_to_proto
        proto_type.pack(Google::Protobuf::Struct.from_hash(values))
      else
        proto_type.new(values)
      end
    when base_class
      proto_type.pack(values)
    else
      proto_type.pack(Google::Protobuf::Value.from_ruby(values))
    end
  elsif proto_type == Google::Protobuf::Timestamp
    case values
    when Time
      proto_type.from_time(values)
    when nil
      nil
    else
      proto_type.new(values)
    end
  elsif proto_type == Google::Protobuf::Duration
    case values
    when Numeric
      seconds, nanos = values.divmod(1)
      nanos *= 1_000_000_000
      proto_type.new(seconds: seconds, nanos: nanos)
    when nil
      nil
    else
      proto_type.new(values)
    end
  elsif values.is_a?(Hash)
    values = values.to_h do |key, value|
      field_descriptor = proto_type.descriptor.lookup(key.to_s)
      if value.nil? && (NILLABLE_WRAPPERS.include?(field_descriptor.subtype.msgclass) ||
               proto_type.descriptor.enum_for(:each_oneof).one? do |oneof|
                 oneof.enum_for(:each).include?(field_descriptor)
               end)

        next([key, value])
      end

      next([key, value]) unless field_descriptor && field_descriptor.type != :enum

      field_subtype = field_descriptor.subtype

      next([key, value]) unless field_subtype

      [
        key,
        # discard protobuf enums as well
        value.is_a?(field_subtype.msgclass) ?
        value :
        convert_to_proto(field_subtype.msgclass, value, convert_value_to_proto: true),
      ]
    end

    # protobuf sets maps as abstract classes, and there's apparently no way to identify them
    # regardless, since their type is "message". Checking for ruby class name is the best
    # proxy I could come up with.
    return values unless proto_type.name

    proto_type.new(values)
  elsif values.nil?
    Google::Protobuf::Value.from_ruby(values)
  else
    proto_type.new(value: values)
  end
end

.convert_to_proto_type(type, package) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/cel/ast/elements/protobuf.rb', line 95

def convert_to_proto_type(type, package)
  proto_type = Cel.package_to_module_name(type)

  return unless proto_type

  return Object.const_get(proto_type) if Object.const_defined?(proto_type)

  return unless package

  return unless package.const_defined?(proto_type)

  package.const_get(proto_type)
end

.duration_classObject



44
45
46
# File 'lib/cel/ast/elements/protobuf.rb', line 44

def duration_class
  Google::Protobuf::Duration
end

.enum_classObject



48
49
50
# File 'lib/cel/ast/elements/protobuf.rb', line 48

def enum_class
  Enum
end

.lookup(proto, attribute) ⇒ Object

finds value for attribute declared in the proto.

if none is found, and if attribute is another proto message, then return an instance of the type. if field is an enum,



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/cel/ast/elements/protobuf.rb', line 334

def lookup(proto, attribute)
  value = proto.public_send(attribute)

  field = proto.class.descriptor.lookup(attribute.to_s)

  case field.label
  when :repeated
    case value
    when Google::Protobuf::Map
      value.to_h { |k, v| [k, convert_from_proto_field_type(field, v)] } # rubocop:disable Style/HashTransformValues
    else
      value.map { |v| convert_from_proto_field_type(field, v) }
    end
  else
    convert_from_proto_field_type(field, value)
  end
end

.map_classObject



36
37
38
# File 'lib/cel/ast/elements/protobuf.rb', line 36

def map_class
  Google::Protobuf::Map
end

.timestamp_classObject



40
41
42
# File 'lib/cel/ast/elements/protobuf.rb', line 40

def timestamp_class
  Google::Protobuf::Timestamp
end

.try_convert_from_wrapper(msg) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/cel/ast/elements/protobuf.rb', line 52

def try_convert_from_wrapper(msg)
  case msg
  when Google::Protobuf::Any
    raise EvaluateError, "error conversion for empty Any" unless msg.type_name

    type_msg = Object.const_get(Cel.package_to_module_name(msg.type_name))
    try_convert_from_wrapper(msg.unpack(type_msg))
  when Google::Protobuf::ListValue
    msg.to_a
  when Google::Protobuf::Struct
    msg.to_h
  when Google::Protobuf::Value
    msg.to_ruby(true)
  when Google::Protobuf::BytesValue
    Cel::Bytes.new(msg.value.bytes)
  when Google::Protobuf::Int32Value,
      Google::Protobuf::Int64Value
    Cel::Number.new(:int, msg.value)
  when Google::Protobuf::UInt32Value,
    Google::Protobuf::UInt64Value
    Cel::Number.new(:uint, msg.value)
  when Google::Protobuf::DoubleValue,
        Google::Protobuf::FloatValue
    Cel::Number.new(:double, msg.value)
  when Google::Protobuf::BoolValue,
       Google::Protobuf::NullValue,
       Google::Protobuf::StringValue
    # conversion to cel type won't be ambiguous here
    msg.value
  when Google::Protobuf::Timestamp
    msg.to_time
  when Google::Protobuf::Duration
    Cel::Duration.new(seconds: msg.seconds, nanos: msg.nanos)
  else
    msg.extend(CelComparisonMode)
    msg
  end
end

.try_convert_from_wrapper_typeObject



91
92
93
# File 'lib/cel/ast/elements/protobuf.rb', line 91

def try_convert_from_wrapper_type(*)
  TYPES[:map]
end

.try_invoke_from(var, func, args) ⇒ Object



315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/cel/ast/elements/protobuf.rb', line 315

def try_invoke_from(var, func, args)
  return unless var.respond_to?(:to_str) && PROTOBUF_VALUES.include?(var.to_str)

  protoclass = var.split(".").last
  protoclass = Google::Protobuf.const_get(protoclass)

  value = if args.nil? && protoclass.constants.include?(func.to_sym)
    protoclass.const_get(func)
  else
    protoclass.__send__(func, *args)
  end

  Literal.to_cel_type(value)
end