class Protip::Decorator

Wraps a protobuf message to allow:

Attributes

message[R]
nested_resources[R]
transformer[R]

Public Class Methods

enum_for_field(field) click to toggle source
# File lib/protip/decorator.rb, line 180
def enum_for_field(field)
  return nil if field.label == :repeated
  if field.type == :enum
    field.subtype
  elsif field.type == :message && field.subtype.name == 'protip.messages.EnumValue'
    Protip::Transformers::EnumTransformer.enum_for_field(field)
  else
    nil
  end
end
new(message, transformer, nested_resources={}) click to toggle source
# File lib/protip/decorator.rb, line 15
def initialize(message, transformer, nested_resources={})
  @message = message
  @transformer = transformer
  @nested_resources = nested_resources
end

Public Instance Methods

==(decorator) click to toggle source
# File lib/protip/decorator.rb, line 172
def ==(decorator)
  decorator.class == self.class &&
    decorator.message.class == message.class &&
    message.class.encode(message) == decorator.message.class.encode(decorator.message) &&
    transformer == decorator.transformer
end
as_json() click to toggle source
# File lib/protip/decorator.rb, line 147
def as_json
  to_h.as_json
end
assign_attributes(attributes) click to toggle source

Mass assignment of message attributes. Nested messages will be built if necessary, but not overwritten if they already exist.

@param attributes [Hash] The attributes to set. Keys are field

names. For primitive fields and message fields which are
convertible to/from Ruby objects, values are the same as you'd
pass to the field's setter method. For nested message fields
which can't be converted to/from Ruby objects, values are
attribute hashes which will be passed down to
+assign_attributes+ on the nested field.  @return [NilClass]
# File lib/protip/decorator.rb, line 125
def assign_attributes(attributes)
  attributes.each do |field_name, value|
    field = message.class.descriptor.lookup(field_name.to_s) ||
      (raise ArgumentError.new("Unrecognized field: #{field_name}"))
    # Message fields can be set directly by Hash, which either
    # builds or updates them as appropriate.
    #
    # TODO: This kind of oddly assumes that the built message
    # responds to +assign_attributes+ (as it does when a
    # +DecoratingTransformer+ is used for the transformation). Can
    # be removed if we decide the update behavior is unnecessary,
    # since +DecoratingTransformer+ supports assignment by hash.
    if field.type == :message && value.is_a?(Hash)
      (get(field) || build(field.name)).assign_attributes value
    else
      set(field, value)
    end
  end

  nil # Return nil to match ActiveRecord behavior
end
build(field_name, attributes = {}) click to toggle source

Create a nested field on our message. For example, given the following definitions:

message Inner {
  optional string str = 1;
}
message Outer {
  optional Inner inner = 1;
}

We could create an inner message using:

wrapper = Protip::Wrapper.new(Outer.new, transformer)
wrapper.inner # => nil
wrapper.build(:inner, str: 'example')
wrapper.inner.str # => 'example'

Assigns values by decorating an instance of the inner message, passing in our transformer, and calling assign_attributes on the created decorator object.

Rebuilds the field if it’s already present. Raises an error if the name of a primitive field is given.

TODO: do we still need this or is it enough to just use +decorator.field_name = hash+?

@param field_name [String|Symbol] The field name to build @param attributes [Hash] The initial attributes to set on the

field (as parsed by +assign_attributes+) @return
[Protip::Wrapper] The created field
# File lib/protip/decorator.rb, line 95
def build(field_name, attributes = {})

  field = message.class.descriptor.detect do |f|
    f.name.to_sym == field_name.to_sym
  end

  if !field
    raise "No field named #{field_name}"
  elsif field.type != :message
    raise "Can only build message fields: #{field_name}"
  end

  built = field.subtype.msgclass.new
  built_wrapper = self.class.new(built, transformer)
  built_wrapper.assign_attributes attributes
  message[field_name.to_s] = built_wrapper.message

  get(field)
end
inspect() click to toggle source
# File lib/protip/decorator.rb, line 21
def inspect
  "<#{self.class.name}(#{transformer.class.name}) #{message.inspect}>"
end
method_missing(name, *args) click to toggle source
Calls superclass method
# File lib/protip/decorator.rb, line 38
def method_missing(name, *args)
  descriptor = message.class.descriptor
  name = name.to_s
  last_char = name[-1, 1]

  if last_char == '='
    return method_missing_set(name, *args)
  end

  if last_char == '?'
    return method_missing_query(name, *args)
  end

  field = descriptor.lookup(name)
  if field
    return method_missing_field(field, *args)
  end

  oneof = descriptor.lookup_oneof(name)
  # For calls to a oneof group, return the active oneof field, or nil if there isn't one
  if oneof
    return method_missing_oneof(oneof, *args)
  end

  super
end
respond_to_missing?(name, *) click to toggle source
Calls superclass method
# File lib/protip/decorator.rb, line 25
def respond_to_missing?(name, *)
  return true if super

  # Responds to calls to oneof groups by name
  return true if message.class.descriptor.lookup_oneof(name.to_s)

  # Responds to field getters, setters, and query methods for all fieldsfa
  field = message.class.descriptor.lookup(name.to_s.gsub(/[=?]$/, ''))
  return true if field

  false
end
to_h() click to toggle source

@return [Hash] A hash whose keys are the fields of our message,

and whose values are the Ruby representations (either nested
hashes or transformed messages) of the field values.
# File lib/protip/decorator.rb, line 154
def to_h
  hash = {}

  # Use nested +to_h+ on fields which are also decorated messages
  transform = ->(v) { v.is_a?(self.class) ? v.to_h : v }

  message.class.descriptor.each do |field|
    value = get(field)
    if field.label == :repeated
      value.map!{|v| transform[v]}
    else
      value = transform[value]
    end
    hash[field.name.to_sym] = value
  end
  hash
end

Private Instance Methods

get(field) click to toggle source

Get the transformed value of the given field on our message.

@param field [::Google::Protobuf::FieldDescriptor]

# File lib/protip/decorator.rb, line 197
def get(field)
  if field.label == :repeated
    message[field.name].map{|value| to_ruby_value field, value}
  else
    to_ruby_value field, message[field.name]
  end
end
matches?(field, value) click to toggle source
# File lib/protip/decorator.rb, line 263
def matches?(field, value)
  enum = Protip::Decorator.enum_for_field(field)
  if value.is_a?(1.class)
    sym = enum.lookup_value(value)
  else
    sym = value.to_sym
    sym = nil if (nil == enum.lookup_name(sym))
  end
  raise RangeError.new("#{field} has no value #{value}") if nil == sym
  get(field) == sym
end
method_missing_field(field, *args) click to toggle source
# File lib/protip/decorator.rb, line 282
def method_missing_field(field, *args)
  raise ArgumentError unless args.length == 0
  get field
end
method_missing_oneof(oneof, *args) click to toggle source
# File lib/protip/decorator.rb, line 275
def method_missing_oneof(oneof, *args)
  raise ArgumentError unless args.length == 0
  field_name = message.public_send(oneof.name)

  field_name ? get(message.class.descriptor.lookup(field_name.to_s)) : nil
end
method_missing_query(name, *args) click to toggle source
# File lib/protip/decorator.rb, line 287
def method_missing_query(name, *args)
  field = message.class.descriptor.lookup(name[0, name.length - 1])
  raise NoMethodError.new("No query method found for: #{name}") unless field

  if nil != Protip::Decorator.enum_for_field(field) && args.length == 1
    matches?(field, args[0])
  elsif args.length == 0
    value = get(field)

    # Copied in from ActiveSupport +.blank?+
    blank = (value.respond_to?(:empty?) ? !!value.empty? : !value)
    !blank
  else
    raise ArgumentError
  end
end
method_missing_set(name, *args) click to toggle source
# File lib/protip/decorator.rb, line 304
def method_missing_set(name, *args)
  raise ArgumentError unless args.length == 1
  field = message.class.descriptor.lookup(name[0, name.length - 1])
  raise(NoMethodError.new) unless field
  set(field, args[0])
end
set(field, value) click to toggle source
# File lib/protip/decorator.rb, line 228
def set(field, value)
  return if field.label == :optional && value.nil? && get(field).nil?

  if field.label == :repeated
    new_values = value.map {|v| to_protobuf_value(field, v) }.compact

    message[field.name].replace(new_values)
  else
    message[field.name] = to_protobuf_value(field, value)
  end

end
to_protobuf_value(field, value) click to toggle source

Helper for setting values - converts the value for the given field to one that we can set directly

# File lib/protip/decorator.rb, line 243
def to_protobuf_value(field, value)
  if field.type == :message
    if nil == value
      field&.default || field&.subtype&.msgclass&.new
    # This check must happen before the nested_resources check to
    # ensure nested messages are set properly
    elsif value.is_a?(field.subtype.msgclass)
      value
    elsif nested_resources.has_key?(field.name.to_sym)
      value.message
    else
      transformer.to_message(value, field)
    end
  elsif field.type == :enum
    value.is_a?(1.class) ? value : value.to_sym
  else
    value
  end
end
to_ruby_value(field, value) click to toggle source

Helper for getting values - converts the value for the given field to one that we can return to the user

@param field [::Google::Protobuf::FieldDescriptor] The

descriptor for the field we're fetching.

@param value [Object] The message or primitive value of the

field.
# File lib/protip/decorator.rb, line 212
def to_ruby_value(field, value)
  if field.type == :message
    field_name_sym = field.name.to_sym
    if nil == value
      nil
    elsif nested_resources.has_key?(field_name_sym)
      resource_klass = nested_resources[field_name_sym]
      resource_klass.new value
    else
      transformer.to_object value, field
    end
  else
    value
  end
end