Class: Protip::Decorator

Inherits:
Object
  • Object
show all
Defined in:
lib/protip/decorator.rb

Overview

Wraps a protobuf message to allow:

  • getting/setting of message fields as if they were more complex Ruby objects

  • mass assignment of attributes

  • standardized creation of nested messages that can’t be converted to/from Ruby objects

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(message, transformer, nested_resources = {}) ⇒ Decorator

Returns a new instance of Decorator.



15
16
17
18
19
# File 'lib/protip/decorator.rb', line 15

def initialize(message, transformer, nested_resources={})
  @message = message
  @transformer = transformer
  @nested_resources = nested_resources
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 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

Instance Attribute Details

#messageObject (readonly)

Returns the value of attribute message.



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

def message
  @message
end

#nested_resourcesObject (readonly)

Returns the value of attribute nested_resources.



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

def nested_resources
  @nested_resources
end

#transformerObject (readonly)

Returns the value of attribute transformer.



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

def transformer
  @transformer
end

Class Method Details

.enum_for_field(field) ⇒ Object



180
181
182
183
184
185
186
187
188
189
# 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

Instance Method Details

#==(decorator) ⇒ Object



172
173
174
175
176
177
# 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_jsonObject



147
148
149
# File 'lib/protip/decorator.rb', line 147

def as_json
  to_h.as_json
end

#assign_attributes(attributes) ⇒ Object

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

Parameters:

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



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# 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 = {}) ⇒ Object

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?

Parameters:

  • field_name (String|Symbol)

    The field name to build

  • attributes (Hash) (defaults to: {})

    The initial attributes to set on the field (as parsed by assign_attributes) @return

    Protip::Wrapper

    The created field



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# 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

#inspectObject



21
22
23
# File 'lib/protip/decorator.rb', line 21

def inspect
  "<#{self.class.name}(#{transformer.class.name}) #{message.inspect}>"
end

#respond_to_missing?(name) ⇒ Boolean

Returns:

  • (Boolean)


25
26
27
28
29
30
31
32
33
34
35
36
# 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_hHash

Returns 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.

Returns:

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



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# 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