786 lines
39 KiB
Ruby
Executable File
786 lines
39 KiB
Ruby
Executable File
# Protocol Buffers - Google's data interchange format
|
|
# Copyright 2023 Google Inc. All rights reserved.
|
|
#
|
|
# Use of this source code is governed by a BSD-style
|
|
# license that can be found in the LICENSE file or at
|
|
# https://developers.google.com/open-source/licenses/bsd
|
|
|
|
|
|
# Decorates Descriptor with the `build_message_class` method that defines
|
|
# Message classes.
|
|
module Google
|
|
module Protobuf
|
|
class FFI
|
|
# Message
|
|
attach_function :clear_message_field, :upb_Message_ClearFieldByDef, [:Message, FieldDescriptor], :void
|
|
attach_function :get_message_value, :upb_Message_GetFieldByDef, [:Message, FieldDescriptor], MessageValue.by_value
|
|
attach_function :get_message_has, :upb_Message_HasFieldByDef, [:Message, FieldDescriptor], :bool
|
|
attach_function :set_message_field, :upb_Message_SetFieldByDef, [:Message, FieldDescriptor, MessageValue.by_value, Internal::Arena], :bool
|
|
attach_function :encode_message, :upb_Encode, [:Message, MiniTable.by_ref, :size_t, Internal::Arena, :pointer, :pointer], EncodeStatus
|
|
attach_function :json_decode_message_detecting_nonconformance, :upb_JsonDecodeDetectingNonconformance, [:binary_string, :size_t, :Message, Descriptor, :DefPool, :int, Internal::Arena, Status.by_ref], :int
|
|
attach_function :json_encode_message, :upb_JsonEncode, [:Message, Descriptor, :DefPool, :int, :binary_string, :size_t, Status.by_ref], :size_t
|
|
attach_function :decode_message, :upb_Decode, [:binary_string, :size_t, :Message, MiniTable.by_ref, :ExtensionRegistry, :int, Internal::Arena], DecodeStatus
|
|
attach_function :get_mutable_message, :upb_Message_Mutable, [:Message, FieldDescriptor, Internal::Arena], MutableMessageValue.by_value
|
|
attach_function :get_message_which_oneof, :upb_Message_WhichOneofByDef, [:Message, OneofDescriptor], FieldDescriptor
|
|
attach_function :message_discard_unknown, :upb_Message_DiscardUnknown, [:Message, Descriptor, :DefPool, :int], :bool
|
|
attach_function :message_next, :upb_Message_Next, [:Message, Descriptor, :DefPool, :FieldDefPointer, MessageValue.by_ref, :pointer], :bool
|
|
attach_function :message_freeze, :upb_Message_Freeze, [:Message, MiniTable.by_ref], :void
|
|
attach_function :message_frozen?, :upb_Message_IsFrozen, [:Message], :bool
|
|
attach_function :message_shallow_copy, :upb_Message_ShallowCopy, [:Message, :Message, MiniTable.by_ref, Internal::Arena], :bool
|
|
|
|
# MessageValue
|
|
attach_function :message_value_equal, :shared_Msgval_IsEqual, [MessageValue.by_value, MessageValue.by_value, CType, Descriptor], :bool
|
|
attach_function :message_value_hash, :shared_Msgval_GetHash, [MessageValue.by_value, CType, Descriptor, :uint64_t], :uint64_t
|
|
end
|
|
|
|
class Descriptor
|
|
def build_message_class
|
|
descriptor = self
|
|
Class.new(Google::Protobuf::const_get(:AbstractMessage)) do
|
|
@descriptor = descriptor
|
|
class << self
|
|
attr_accessor :descriptor
|
|
private
|
|
attr_accessor :oneof_field_names
|
|
include ::Google::Protobuf::Internal::Convert
|
|
end
|
|
|
|
alias original_method_missing method_missing
|
|
def method_missing(method_name, *args)
|
|
method_missing_internal method_name, *args, mode: :method_missing
|
|
end
|
|
|
|
def respond_to_missing?(method_name, include_private = false)
|
|
method_missing_internal(method_name, mode: :respond_to_missing?) || super
|
|
end
|
|
|
|
##
|
|
# Public constructor. Automatically allocates from a new Arena.
|
|
def self.new(initial_value = nil)
|
|
instance = allocate
|
|
instance.send(:initialize, initial_value)
|
|
instance
|
|
end
|
|
|
|
##
|
|
# Is this object frozen?
|
|
# Returns true if either this Ruby wrapper or the underlying
|
|
# representation are frozen. Freezes the wrapper if the underlying
|
|
# representation is already frozen but this wrapper isn't.
|
|
def frozen?
|
|
unless Google::Protobuf::FFI.message_frozen? @msg
|
|
raise RuntimeError.new "Ruby frozen Message with mutable representation" if super
|
|
return false
|
|
end
|
|
method(:freeze).super_method.call unless super
|
|
true
|
|
end
|
|
|
|
##
|
|
# Freezes the map object. We have to intercept this so we can freeze the
|
|
# underlying representation, not just the Ruby wrapper. Returns self.
|
|
def freeze
|
|
if method(:frozen?).super_method.call
|
|
unless Google::Protobuf::FFI.message_frozen? @msg
|
|
raise RuntimeError.new "Underlying representation of message still mutable despite frozen wrapper"
|
|
end
|
|
return self
|
|
end
|
|
unless Google::Protobuf::FFI.message_frozen? @msg
|
|
Google::Protobuf::FFI.message_freeze(@msg, Google::Protobuf::FFI.get_mini_table(self.class.descriptor))
|
|
end
|
|
super
|
|
end
|
|
|
|
def dup
|
|
mini_table = Google::Protobuf::FFI.get_mini_table(self.class.descriptor)
|
|
new_msg = Google::Protobuf::FFI.new_message_from_def(mini_table, @arena)
|
|
unless Google::Protobuf::FFI.message_shallow_copy(new_msg, @msg, mini_table, @arena)
|
|
raise RuntimeError.new "Failed to shallow copy message."
|
|
end
|
|
self.class.private_constructor(@arena, msg: new_msg)
|
|
end
|
|
alias clone dup
|
|
|
|
def eql?(other)
|
|
return false unless self.class === other
|
|
encoding_options = Google::Protobuf::FFI::Upb_Encode_Deterministic | Google::Protobuf::FFI::Upb_Encode_SkipUnknown
|
|
temporary_arena = Google::Protobuf::FFI.create_arena
|
|
mini_table = Google::Protobuf::FFI.get_mini_table(self.class.descriptor)
|
|
size_one = ::FFI::MemoryPointer.new(:size_t, 1)
|
|
encoding_one = ::FFI::MemoryPointer.new(:pointer, 1)
|
|
encoding_status = Google::Protobuf::FFI.encode_message(@msg, mini_table, encoding_options, temporary_arena, encoding_one.to_ptr, size_one)
|
|
raise ParseError.new "Error comparing messages due to #{encoding_status} while encoding LHS of `eql?()`" unless encoding_status == :Ok
|
|
|
|
size_two = ::FFI::MemoryPointer.new(:size_t, 1)
|
|
encoding_two = ::FFI::MemoryPointer.new(:pointer, 1)
|
|
encoding_status = Google::Protobuf::FFI.encode_message(other.instance_variable_get(:@msg), mini_table, encoding_options, temporary_arena, encoding_two.to_ptr, size_two)
|
|
raise ParseError.new "Error comparing messages due to #{encoding_status} while encoding RHS of `eql?()`" unless encoding_status == :Ok
|
|
|
|
if encoding_one.null? or encoding_two.null?
|
|
raise ParseError.new "Error comparing messages"
|
|
end
|
|
size_one.read(:size_t) == size_two.read(:size_t) and Google::Protobuf::FFI.memcmp(encoding_one.read(:pointer), encoding_two.read(:pointer), size_one.read(:size_t)).zero?
|
|
end
|
|
alias == eql?
|
|
|
|
def hash
|
|
encoding_options = Google::Protobuf::FFI::Upb_Encode_Deterministic | Google::Protobuf::FFI::Upb_Encode_SkipUnknown
|
|
temporary_arena = Google::Protobuf::FFI.create_arena
|
|
mini_table_ptr = Google::Protobuf::FFI.get_mini_table(self.class.descriptor)
|
|
size_ptr = ::FFI::MemoryPointer.new(:size_t, 1)
|
|
encoding = ::FFI::MemoryPointer.new(:pointer, 1)
|
|
encoding_status = Google::Protobuf::FFI.encode_message(@msg, mini_table_ptr, encoding_options, temporary_arena, encoding.to_ptr, size_ptr)
|
|
if encoding_status != :Ok or encoding.null?
|
|
raise ParseError.new "Error calculating hash"
|
|
end
|
|
encoding.read(:pointer).read_string(size_ptr.read(:size_t)).hash
|
|
end
|
|
|
|
def to_h
|
|
to_h_internal @msg, self.class.descriptor
|
|
end
|
|
|
|
##
|
|
# call-seq:
|
|
# Message.inspect => string
|
|
#
|
|
# Returns a human-readable string representing this message. It will be
|
|
# formatted as "<MessageType: field1: value1, field2: value2, ...>". Each
|
|
# field's value is represented according to its own #inspect method.
|
|
def inspect
|
|
self.class.inspect_internal @msg
|
|
end
|
|
|
|
def to_s
|
|
self.inspect
|
|
end
|
|
|
|
##
|
|
# call-seq:
|
|
# Message.[](index) => value
|
|
# Accesses a field's value by field name. The provided field name
|
|
# should be a string.
|
|
def [](name)
|
|
raise TypeError.new "Expected String for name but got #{name.class}" unless name.is_a? String
|
|
index_internal name
|
|
end
|
|
|
|
##
|
|
# call-seq:
|
|
# Message.[]=(index, value)
|
|
# Sets a field's value by field name. The provided field name should
|
|
# be a string.
|
|
# @param name [String] Name of the field to be set
|
|
# @param value [Object] Value to set the field to
|
|
def []=(name, value)
|
|
raise TypeError.new "Expected String for name but got #{name.class}" unless name.is_a? String
|
|
index_assign_internal(value, name: name)
|
|
end
|
|
|
|
##
|
|
# call-seq:
|
|
# MessageClass.decode(data, options) => message
|
|
#
|
|
# Decodes the given data (as a string containing bytes in protocol buffers wire
|
|
# format) under the interpretation given by this message class's definition
|
|
# and returns a message object with the corresponding field values.
|
|
# @param data [String] Binary string in Protobuf wire format to decode
|
|
# @param options [Hash] options for the decoder
|
|
# @option options [Integer] :recursion_limit Set to maximum decoding depth for message (default is 64)
|
|
def self.decode(data, options = {})
|
|
raise ArgumentError.new "Expected hash arguments." unless options.is_a? Hash
|
|
raise ArgumentError.new "Expected string for binary protobuf data." unless data.is_a? String
|
|
decoding_options = 0
|
|
depth = options[:recursion_limit]
|
|
|
|
if depth.is_a? Numeric
|
|
decoding_options |= Google::Protobuf::FFI.decode_max_depth(depth.to_i)
|
|
end
|
|
|
|
message = new
|
|
mini_table_ptr = Google::Protobuf::FFI.get_mini_table(message.class.descriptor)
|
|
status = Google::Protobuf::FFI.decode_message(
|
|
data,
|
|
data.bytesize,
|
|
message.instance_variable_get(:@msg),
|
|
mini_table_ptr,
|
|
Google::Protobuf::FFI.get_extension_registry(message.class.descriptor.send(:pool).descriptor_pool),
|
|
decoding_options,
|
|
message.instance_variable_get(:@arena)
|
|
)
|
|
raise ParseError.new "Error occurred during parsing" unless status == :Ok
|
|
message
|
|
end
|
|
|
|
##
|
|
# call-seq:
|
|
# MessageClass.encode(msg, options) => bytes
|
|
#
|
|
# Encodes the given message object to its serialized form in protocol buffers
|
|
# wire format.
|
|
# @param options [Hash] options for the encoder
|
|
# @option options [Integer] :recursion_limit Set to maximum encoding depth for message (default is 64)
|
|
def self.encode(message, options = {})
|
|
raise ArgumentError.new "Message of wrong type." unless message.is_a? self
|
|
raise ArgumentError.new "Expected hash arguments." unless options.is_a? Hash
|
|
|
|
encoding_options = 0
|
|
depth = options[:recursion_limit]
|
|
|
|
if depth.is_a? Numeric
|
|
encoding_options |= Google::Protobuf::FFI.decode_max_depth(depth.to_i)
|
|
end
|
|
|
|
encode_internal(message.instance_variable_get(:@msg), encoding_options) do |encoding, size, _|
|
|
if encoding.nil? or encoding.null?
|
|
raise RuntimeError.new "Exceeded maximum depth (possibly cycle)"
|
|
else
|
|
encoding.read_string_length(size).force_encoding("ASCII-8BIT").freeze
|
|
end
|
|
end
|
|
end
|
|
|
|
##
|
|
# all-seq:
|
|
# MessageClass.decode_json(data, options = {}) => message
|
|
#
|
|
# Decodes the given data (as a string containing bytes in protocol buffers wire
|
|
# format) under the interpretation given by this message class's definition
|
|
# and returns a message object with the corresponding field values.
|
|
#
|
|
# @param options [Hash] options for the decoder
|
|
# @option options [Boolean] :ignore_unknown_fields Set true to ignore unknown fields (default is to raise an error)
|
|
# @return [Message]
|
|
def self.decode_json(data, options = {})
|
|
decoding_options = 0
|
|
unless options.is_a? Hash
|
|
if options.respond_to? :to_h
|
|
options = options.to_h
|
|
else
|
|
#TODO can this error message be improve to include what was received?
|
|
raise ArgumentError.new "Expected hash arguments"
|
|
end
|
|
end
|
|
raise ArgumentError.new "Expected string for JSON data." unless data.is_a? String
|
|
raise RuntimeError.new "Cannot parse a wrapper directly" if descriptor.send(:wrapper?)
|
|
|
|
if options[:ignore_unknown_fields]
|
|
decoding_options |= Google::Protobuf::FFI::Upb_JsonDecode_IgnoreUnknown
|
|
end
|
|
|
|
message = new
|
|
pool_def = message.class.descriptor.instance_variable_get(:@descriptor_pool).descriptor_pool
|
|
status = Google::Protobuf::FFI::Status.new
|
|
result = Google::Protobuf::FFI.json_decode_message_detecting_nonconformance(data, data.bytesize, message.instance_variable_get(:@msg), message.class.descriptor, pool_def, decoding_options, message.instance_variable_get(:@arena), status)
|
|
case result
|
|
when Google::Protobuf::FFI::Upb_JsonDecodeResult_Error
|
|
raise ParseError.new "Error occurred during parsing: #{Google::Protobuf::FFI.error_message(status)}"
|
|
end
|
|
message
|
|
end
|
|
|
|
def self.encode_json(message, options = {})
|
|
encoding_options = 0
|
|
unless options.is_a? Hash
|
|
if options.respond_to? :to_h
|
|
options = options.to_h
|
|
else
|
|
#TODO can this error message be improve to include what was received?
|
|
raise ArgumentError.new "Expected hash arguments"
|
|
end
|
|
end
|
|
|
|
if options[:preserve_proto_fieldnames]
|
|
encoding_options |= Google::Protobuf::FFI::Upb_JsonEncode_UseProtoNames
|
|
end
|
|
if options[:emit_defaults]
|
|
encoding_options |= Google::Protobuf::FFI::Upb_JsonEncode_EmitDefaults
|
|
end
|
|
if options[:format_enums_as_integers]
|
|
encoding_options |= Google::Protobuf::FFI::Upb_JsonEncode_FormatEnumsAsIntegers
|
|
end
|
|
|
|
buffer_size = 1024
|
|
buffer = ::FFI::MemoryPointer.new(:char, buffer_size)
|
|
status = Google::Protobuf::FFI::Status.new
|
|
msg = message.instance_variable_get(:@msg)
|
|
pool_def = message.class.descriptor.instance_variable_get(:@descriptor_pool).descriptor_pool
|
|
size = Google::Protobuf::FFI::json_encode_message(msg, message.class.descriptor, pool_def, encoding_options, buffer, buffer_size, status)
|
|
unless status[:ok]
|
|
raise ParseError.new "Error occurred during encoding: #{Google::Protobuf::FFI.error_message(status)}"
|
|
end
|
|
|
|
if size >= buffer_size
|
|
buffer_size = size + 1
|
|
buffer = ::FFI::MemoryPointer.new(:char, buffer_size)
|
|
status.clear
|
|
size = Google::Protobuf::FFI::json_encode_message(msg, message.class.descriptor, pool_def, encoding_options, buffer, buffer_size, status)
|
|
unless status[:ok]
|
|
raise ParseError.new "Error occurred during encoding: #{Google::Protobuf::FFI.error_message(status)}"
|
|
end
|
|
if size >= buffer_size
|
|
raise ParseError.new "Inconsistent JSON encoding sizes - was #{buffer_size - 1}, now #{size}"
|
|
end
|
|
end
|
|
|
|
buffer.read_string_length(size).force_encoding("UTF-8").freeze
|
|
end
|
|
|
|
private
|
|
# Implementation details below are subject to breaking changes without
|
|
# warning and are intended for use only within the gem.
|
|
|
|
include Google::Protobuf::Internal::Convert
|
|
|
|
##
|
|
# Checks ObjectCache for a sentinel empty frozen Map of the key and
|
|
# value types matching the field descriptor's MessageDef and returns
|
|
# the cache entry. If an entry is not found, one is created and added
|
|
# to the cache keyed by the MessageDef pointer first.
|
|
# @param field_descriptor [FieldDescriptor] Field to retrieve.
|
|
def empty_frozen_map(field_descriptor)
|
|
sub_message_def = Google::Protobuf::FFI.get_subtype_as_message field_descriptor
|
|
frozen_map = OBJECT_CACHE.get sub_message_def
|
|
if frozen_map.nil?
|
|
frozen_map = Google::Protobuf::Map.send(:construct_for_field, field_descriptor)
|
|
OBJECT_CACHE.try_add(sub_message_def, frozen_map.freeze)
|
|
end
|
|
raise "Empty Frozen Map is not frozen" unless frozen_map.frozen?
|
|
frozen_map
|
|
end
|
|
|
|
##
|
|
# Returns a frozen Map instance for the given field. If the message
|
|
# already has a value for that field, it is used. If not, a sentinel
|
|
# (per FieldDescriptor) empty frozen Map is returned instead.
|
|
# @param field_descriptor [FieldDescriptor] Field to retrieve.
|
|
def frozen_map_from_field_descriptor(field_descriptor)
|
|
message_value = Google::Protobuf::FFI.get_message_value @msg, field_descriptor
|
|
return empty_frozen_map field_descriptor if message_value[:map_val].null?
|
|
get_map_field(message_value[:map_val], field_descriptor).freeze
|
|
end
|
|
|
|
##
|
|
# Returns a Map instance for the given field. If the message is frozen
|
|
# the return value is also frozen. If not, a mutable instance is
|
|
# returned instead.
|
|
# @param field_descriptor [FieldDescriptor] Field to retrieve.
|
|
def map_from_field_descriptor(field_descriptor)
|
|
return frozen_map_from_field_descriptor field_descriptor if frozen?
|
|
mutable_message_value = Google::Protobuf::FFI.get_mutable_message @msg, field_descriptor, @arena
|
|
get_map_field(mutable_message_value[:map], field_descriptor)
|
|
end
|
|
|
|
##
|
|
# Checks ObjectCache for a sentinel empty frozen RepeatedField of the
|
|
# value type matching the field descriptor's MessageDef and returns
|
|
# the cache entry. If an entry is not found, one is created and added
|
|
# to the cache keyed by the MessageDef pointer first.
|
|
# @param field_descriptor [FieldDescriptor] Field to retrieve.
|
|
def empty_frozen_repeated_field(field_descriptor)
|
|
sub_message_def = Google::Protobuf::FFI.get_subtype_as_message field_descriptor
|
|
frozen_repeated_field = OBJECT_CACHE.get sub_message_def
|
|
if frozen_repeated_field.nil?
|
|
frozen_repeated_field = Google::Protobuf::RepeatedField.send(:construct_for_field, field_descriptor)
|
|
OBJECT_CACHE.try_add(sub_message_def, frozen_repeated_field.freeze)
|
|
end
|
|
raise "Empty frozen RepeatedField is not frozen" unless frozen_repeated_field.frozen?
|
|
frozen_repeated_field
|
|
end
|
|
|
|
##
|
|
# Returns a frozen RepeatedField instance for the given field. If the
|
|
# message already has a value for that field, it is used. If not, a
|
|
# sentinel (per FieldDescriptor) empty frozen RepeatedField is
|
|
# returned instead.
|
|
# @param field_descriptor [FieldDescriptor] Field to retrieve.
|
|
def frozen_repeated_field_from_field_descriptor(field_descriptor)
|
|
message_value = Google::Protobuf::FFI.get_message_value @msg, field_descriptor
|
|
return empty_frozen_repeated_field field_descriptor if message_value[:array_val].null?
|
|
get_repeated_field(message_value[:array_val], field_descriptor).freeze
|
|
end
|
|
|
|
##
|
|
# Returns a RepeatedField instance for the given field. If the message
|
|
# is frozen the return value is also frozen. If not, a mutable
|
|
# instance is returned instead.
|
|
# @param field_descriptor [FieldDescriptor] Field to retrieve.
|
|
def repeated_field_from_field_descriptor(field_descriptor)
|
|
return frozen_repeated_field_from_field_descriptor field_descriptor if frozen?
|
|
mutable_message_value = Google::Protobuf::FFI.get_mutable_message @msg, field_descriptor, @arena
|
|
get_repeated_field(mutable_message_value[:array], field_descriptor)
|
|
end
|
|
|
|
##
|
|
# Returns a Message instance for the given field. If the message
|
|
# is frozen nil is always returned. Otherwise, a mutable instance is
|
|
# returned instead.
|
|
# @param field_descriptor [FieldDescriptor] Field to retrieve.
|
|
def message_from_field_descriptor(field_descriptor)
|
|
return nil if frozen?
|
|
return nil unless Google::Protobuf::FFI.get_message_has @msg, field_descriptor
|
|
mutable_message = Google::Protobuf::FFI.get_mutable_message @msg, field_descriptor, @arena
|
|
sub_message = mutable_message[:msg]
|
|
sub_message_def = Google::Protobuf::FFI.get_subtype_as_message field_descriptor
|
|
Descriptor.send(:get_message, sub_message, sub_message_def, @arena)
|
|
end
|
|
|
|
##
|
|
# Returns a scalar value for the given field. If the message
|
|
# is frozen the return value is also frozen.
|
|
# @param field_descriptor [FieldDescriptor] Field to retrieve.
|
|
def scalar_from_field_descriptor(field_descriptor)
|
|
c_type = field_descriptor.send(:c_type)
|
|
message_value = Google::Protobuf::FFI.get_message_value @msg, field_descriptor
|
|
msg_or_enum_def = c_type == :enum ? Google::Protobuf::FFI.get_subtype_as_enum(field_descriptor) : nil
|
|
return_value = convert_upb_to_ruby message_value, c_type, msg_or_enum_def
|
|
frozen? ? return_value.freeze : return_value
|
|
end
|
|
|
|
##
|
|
# Dynamically define accessors methods for every field of @descriptor.
|
|
# Methods with names that conflict with existing methods are skipped.
|
|
def self.setup_accessors!
|
|
@descriptor.each do |field_descriptor|
|
|
field_name = field_descriptor.name
|
|
unless instance_methods(true).include?(field_name.to_sym)
|
|
# Dispatching to either index_internal or get_field is logically
|
|
# correct, but slightly slower due to having to perform extra
|
|
# lookups on each invocation rather than doing it once here.
|
|
if field_descriptor.map?
|
|
define_method(field_name) do
|
|
map_from_field_descriptor field_descriptor
|
|
end
|
|
elsif field_descriptor.repeated?
|
|
define_method(field_name) do
|
|
repeated_field_from_field_descriptor field_descriptor
|
|
end
|
|
elsif field_descriptor.sub_message?
|
|
define_method(field_name) do
|
|
message_from_field_descriptor field_descriptor
|
|
end
|
|
else
|
|
define_method(field_name) do
|
|
scalar_from_field_descriptor field_descriptor
|
|
end
|
|
end
|
|
define_method("#{field_name}=") do |value|
|
|
index_assign_internal(value, field_descriptor: field_descriptor)
|
|
end
|
|
define_method("clear_#{field_name}") do
|
|
clear_internal(field_descriptor)
|
|
end
|
|
if field_descriptor.type == :enum
|
|
if field_descriptor.repeated?
|
|
define_method("#{field_name}_const") do
|
|
return_value = []
|
|
repeated_field_from_field_descriptor(field_descriptor).send(:each_msg_val) do |msg_val|
|
|
return_value << msg_val[:int32_val]
|
|
end
|
|
return_value
|
|
end
|
|
else
|
|
define_method("#{field_name}_const") do
|
|
message_value = Google::Protobuf::FFI.get_message_value @msg, field_descriptor
|
|
message_value[:int32_val]
|
|
end
|
|
end
|
|
end
|
|
if !field_descriptor.repeated? and field_descriptor.wrapper?
|
|
define_method("#{field_name}_as_value") do
|
|
get_field(field_descriptor, unwrap: true)
|
|
end
|
|
define_method("#{field_name}_as_value=") do |value|
|
|
if value.nil?
|
|
clear_internal(field_descriptor)
|
|
else
|
|
index_assign_internal(value, field_descriptor: field_descriptor, wrap: true)
|
|
end
|
|
end
|
|
end
|
|
if field_descriptor.has_presence?
|
|
define_method("has_#{field_name}?") do
|
|
Google::Protobuf::FFI.get_message_has(@msg, field_descriptor)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
##
|
|
# Dynamically define accessors methods for every OneOf field of
|
|
# @descriptor.
|
|
def self.setup_oneof_accessors!
|
|
@oneof_field_names = []
|
|
@descriptor.each_oneof do |oneof_descriptor|
|
|
self.add_oneof_accessors_for! oneof_descriptor
|
|
end
|
|
end
|
|
|
|
##
|
|
# Dynamically define accessors methods for the given OneOf field.
|
|
# Methods with names that conflict with existing methods are skipped.
|
|
# @param oneof_descriptor [OneofDescriptor] Field to create accessors
|
|
# for.
|
|
def self.add_oneof_accessors_for!(oneof_descriptor)
|
|
field_name = oneof_descriptor.name.to_sym
|
|
@oneof_field_names << field_name
|
|
unless instance_methods(true).include?(field_name)
|
|
define_method(field_name) do
|
|
field_descriptor = Google::Protobuf::FFI.get_message_which_oneof(@msg, oneof_descriptor)
|
|
if field_descriptor.nil?
|
|
return
|
|
else
|
|
return field_descriptor.name.to_sym
|
|
end
|
|
end
|
|
define_method("clear_#{field_name}") do
|
|
field_descriptor = Google::Protobuf::FFI.get_message_which_oneof(@msg, oneof_descriptor)
|
|
unless field_descriptor.nil?
|
|
clear_internal(field_descriptor)
|
|
end
|
|
end
|
|
define_method("has_#{field_name}?") do
|
|
!Google::Protobuf::FFI.get_message_which_oneof(@msg, oneof_descriptor).nil?
|
|
end
|
|
end
|
|
end
|
|
|
|
setup_accessors!
|
|
setup_oneof_accessors!
|
|
|
|
def self.private_constructor(arena, msg: nil, initial_value: nil)
|
|
instance = allocate
|
|
instance.send(:initialize, initial_value, arena, msg)
|
|
instance
|
|
end
|
|
|
|
def self.inspect_field(field_descriptor, c_type, message_value)
|
|
if field_descriptor.sub_message?
|
|
sub_msg_descriptor = Google::Protobuf::FFI.get_subtype_as_message(field_descriptor)
|
|
sub_msg_descriptor.msgclass.send(:inspect_internal, message_value[:msg_val])
|
|
else
|
|
convert_upb_to_ruby(message_value, c_type, field_descriptor.subtype).inspect
|
|
end
|
|
end
|
|
|
|
# @param msg [::FFI::Pointer] Pointer to the Message
|
|
def self.inspect_internal(msg)
|
|
field_output = []
|
|
descriptor.each do |field_descriptor|
|
|
next if field_descriptor.has_presence? && !Google::Protobuf::FFI.get_message_has(msg, field_descriptor)
|
|
if field_descriptor.map?
|
|
# TODO Adapted - from map#each_msg_val and map#inspect- can this be refactored to reduce echo without introducing a arena allocation?
|
|
message_descriptor = field_descriptor.subtype
|
|
key_field_def = Google::Protobuf::FFI.get_field_by_number(message_descriptor, 1)
|
|
key_field_type = Google::Protobuf::FFI.get_type(key_field_def)
|
|
|
|
value_field_def = Google::Protobuf::FFI.get_field_by_number(message_descriptor, 2)
|
|
value_field_type = Google::Protobuf::FFI.get_type(value_field_def)
|
|
|
|
message_value = Google::Protobuf::FFI.get_message_value(msg, field_descriptor)
|
|
iter = ::FFI::MemoryPointer.new(:size_t, 1)
|
|
iter.write(:size_t, Google::Protobuf::FFI::Upb_Map_Begin)
|
|
key_value_pairs = []
|
|
while Google::Protobuf::FFI.map_next(message_value[:map_val], iter) do
|
|
iter_size_t = iter.read(:size_t)
|
|
key_message_value = Google::Protobuf::FFI.map_key(message_value[:map_val], iter_size_t)
|
|
value_message_value = Google::Protobuf::FFI.map_value(message_value[:map_val], iter_size_t)
|
|
key_string = convert_upb_to_ruby(key_message_value, key_field_type).inspect
|
|
value_string = inspect_field(value_field_def, value_field_type, value_message_value)
|
|
key_value_pairs << "#{key_string}=>#{value_string}"
|
|
end
|
|
field_output << "#{field_descriptor.name}: {#{key_value_pairs.join(", ")}}"
|
|
elsif field_descriptor.repeated?
|
|
# TODO Adapted - from repeated_field#each - can this be refactored to reduce echo?
|
|
repeated_field_output = []
|
|
message_value = Google::Protobuf::FFI.get_message_value(msg, field_descriptor)
|
|
array = message_value[:array_val]
|
|
n = array.null? ? 0 : Google::Protobuf::FFI.array_size(array)
|
|
0.upto(n - 1) do |i|
|
|
element = Google::Protobuf::FFI.get_msgval_at(array, i)
|
|
repeated_field_output << inspect_field(field_descriptor, field_descriptor.send(:c_type), element)
|
|
end
|
|
field_output << "#{field_descriptor.name}: [#{repeated_field_output.join(", ")}]"
|
|
else
|
|
message_value = Google::Protobuf::FFI.get_message_value msg, field_descriptor
|
|
rendered_value = inspect_field(field_descriptor, field_descriptor.send(:c_type), message_value)
|
|
field_output << "#{field_descriptor.name}: #{rendered_value}"
|
|
end
|
|
end
|
|
"<#{name}: #{field_output.join(', ')}>"
|
|
end
|
|
|
|
def self.deep_copy(msg, arena = nil)
|
|
arena ||= Google::Protobuf::FFI.create_arena
|
|
encode_internal(msg) do |encoding, size, mini_table_ptr|
|
|
message = private_constructor(arena)
|
|
if encoding.nil? or encoding.null? or Google::Protobuf::FFI.decode_message(encoding, size, message.instance_variable_get(:@msg), mini_table_ptr, nil, 0, arena) != :Ok
|
|
raise ParseError.new "Error occurred copying proto"
|
|
end
|
|
message
|
|
end
|
|
end
|
|
|
|
def self.encode_internal(msg, encoding_options = 0)
|
|
temporary_arena = Google::Protobuf::FFI.create_arena
|
|
|
|
mini_table_ptr = Google::Protobuf::FFI.get_mini_table(descriptor)
|
|
size_ptr = ::FFI::MemoryPointer.new(:size_t, 1)
|
|
pointer_ptr = ::FFI::MemoryPointer.new(:pointer, 1)
|
|
encoding_status = Google::Protobuf::FFI.encode_message(msg, mini_table_ptr, encoding_options, temporary_arena, pointer_ptr.to_ptr, size_ptr)
|
|
raise "Encoding failed due to #{encoding_status}" unless encoding_status == :Ok
|
|
yield pointer_ptr.read(:pointer), size_ptr.read(:size_t), mini_table_ptr
|
|
end
|
|
|
|
def method_missing_internal(method_name, *args, mode: nil)
|
|
raise ArgumentError.new "method_missing_internal called with invalid mode #{mode.inspect}" unless [:respond_to_missing?, :method_missing].include? mode
|
|
|
|
#TODO not being allowed is not the same thing as not responding, but this is needed to pass tests
|
|
if method_name.to_s.end_with? '='
|
|
if self.class.send(:oneof_field_names).include? method_name.to_s[0..-2].to_sym
|
|
return false if mode == :respond_to_missing?
|
|
raise RuntimeError.new "Oneof accessors are read-only."
|
|
end
|
|
end
|
|
|
|
original_method_missing(method_name, *args) if mode == :method_missing
|
|
end
|
|
|
|
def clear_internal(field_def)
|
|
raise FrozenError.new "can't modify frozen #{self.class}" if frozen?
|
|
Google::Protobuf::FFI.clear_message_field(@msg, field_def)
|
|
end
|
|
|
|
# Accessor for field by name. Does not delegate to methods setup by
|
|
# self.setup_accessors! in order to avoid conflicts with bad field
|
|
# names e.g. `dup` or `class` which are perfectly valid for proto
|
|
# fields.
|
|
def index_internal(name)
|
|
field_descriptor = self.class.descriptor.lookup(name)
|
|
get_field field_descriptor unless field_descriptor.nil?
|
|
end
|
|
|
|
#TODO - well known types keeps us on our toes by overloading methods.
|
|
# How much of the public API needs to be defended?
|
|
def index_assign_internal(value, name: nil, field_descriptor: nil, wrap: false)
|
|
raise FrozenError.new "can't modify frozen #{self.class}" if frozen?
|
|
if field_descriptor.nil?
|
|
field_descriptor = self.class.descriptor.lookup(name)
|
|
if field_descriptor.nil?
|
|
raise ArgumentError.new "Unknown field: #{name}"
|
|
end
|
|
end
|
|
unless field_descriptor.send :set_value_on_message, value, @msg, @arena, wrap: wrap
|
|
raise RuntimeError.new "allocation failed"
|
|
end
|
|
end
|
|
|
|
##
|
|
# @param initial_value [Object] initial value of this Message
|
|
# @param arena [Arena] Optional; Arena where this message will be allocated
|
|
# @param msg [::FFI::Pointer] Optional; Message to initialize; creates
|
|
# one if omitted or nil.
|
|
def initialize(initial_value = nil, arena = nil, msg = nil)
|
|
@arena = arena || Google::Protobuf::FFI.create_arena
|
|
@msg = msg || Google::Protobuf::FFI.new_message_from_def(Google::Protobuf::FFI.get_mini_table(self.class.descriptor), @arena)
|
|
|
|
unless initial_value.nil?
|
|
raise ArgumentError.new "Expected hash arguments or message, not #{initial_value.class}" unless initial_value.respond_to? :each
|
|
|
|
field_def_ptr = ::FFI::MemoryPointer.new :pointer
|
|
oneof_def_ptr = ::FFI::MemoryPointer.new :pointer
|
|
|
|
initial_value.each do |key, value|
|
|
raise ArgumentError.new "Expected string or symbols as hash keys when initializing proto from hash." unless [String, Symbol].include? key.class
|
|
|
|
unless Google::Protobuf::FFI.find_msg_def_by_name self.class.descriptor, key.to_s, key.to_s.bytesize, field_def_ptr, oneof_def_ptr
|
|
raise ArgumentError.new "Unknown field name '#{key}' in initialization map entry."
|
|
end
|
|
raise NotImplementedError.new "Haven't added oneofsupport yet" unless oneof_def_ptr.get_pointer(0).null?
|
|
raise NotImplementedError.new "Expected a field def" if field_def_ptr.get_pointer(0).null?
|
|
|
|
field_descriptor = FieldDescriptor.from_native field_def_ptr.get_pointer(0)
|
|
|
|
next if value.nil?
|
|
if field_descriptor.map?
|
|
index_assign_internal(Google::Protobuf::Map.send(:construct_for_field, field_descriptor, arena: @arena, value: value), name: key.to_s)
|
|
elsif field_descriptor.repeated?
|
|
index_assign_internal(RepeatedField.send(:construct_for_field, field_descriptor, arena: @arena, values: value), name: key.to_s)
|
|
else
|
|
index_assign_internal(value, name: key.to_s)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Should always be the last expression of the initializer to avoid
|
|
# leaking references to this object before construction is complete.
|
|
Google::Protobuf::OBJECT_CACHE.try_add @msg.address, self
|
|
end
|
|
|
|
##
|
|
# Gets a field of this message identified by FieldDescriptor.
|
|
#
|
|
# @param field [FieldDescriptor] Field to retrieve.
|
|
# @param unwrap [Boolean](false) If true, unwraps wrappers.
|
|
def get_field(field, unwrap: false)
|
|
if field.map?
|
|
map_from_field_descriptor field
|
|
elsif field.repeated?
|
|
repeated_field_from_field_descriptor field
|
|
elsif field.sub_message?
|
|
return nil unless Google::Protobuf::FFI.get_message_has @msg, field
|
|
if unwrap
|
|
if field.has?(self)
|
|
sub_message_def = Google::Protobuf::FFI.get_subtype_as_message field
|
|
wrapper_message_value = Google::Protobuf::FFI.get_message_value @msg, field
|
|
fields = Google::Protobuf::FFI.field_count(sub_message_def)
|
|
raise "Sub message has #{fields} fields! Expected exactly 1." unless fields == 1
|
|
value_field_def = Google::Protobuf::FFI.get_field_by_number sub_message_def, 1
|
|
message_value = Google::Protobuf::FFI.get_message_value wrapper_message_value[:msg_val], value_field_def
|
|
convert_upb_to_ruby message_value, Google::Protobuf::FFI.get_c_type(value_field_def)
|
|
else
|
|
nil
|
|
end
|
|
else
|
|
message_from_field_descriptor field
|
|
end
|
|
else
|
|
scalar_from_field_descriptor field
|
|
end
|
|
end
|
|
|
|
##
|
|
# Gets a RepeatedField from the ObjectCache or creates a new one.
|
|
# @param array [::FFI::Pointer] Pointer to the upb_Array
|
|
# @param field [Google::Protobuf::FieldDescriptor] Type of the repeated field
|
|
def get_repeated_field(array, field)
|
|
return nil if array.nil? or array.null?
|
|
repeated_field = OBJECT_CACHE.get(array.address)
|
|
if repeated_field.nil?
|
|
repeated_field = RepeatedField.send(:construct_for_field, field, arena: @arena, array: array)
|
|
repeated_field.freeze if frozen?
|
|
end
|
|
repeated_field
|
|
end
|
|
|
|
##
|
|
# Gets a Map from the ObjectCache or creates a new one.
|
|
# @param map [::FFI::Pointer] Pointer to the upb_Map
|
|
# @param field [Google::Protobuf::FieldDescriptor] Type of the map field
|
|
def get_map_field(map, field)
|
|
return nil if map.nil? or map.null?
|
|
map_field = OBJECT_CACHE.get(map.address)
|
|
if map_field.nil?
|
|
map_field = Google::Protobuf::Map.send(:construct_for_field, field, arena: @arena, map: map)
|
|
map_field.freeze if frozen?
|
|
end
|
|
map_field
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|