Class: Protip::Wrapper

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

Overview

Wraps a protobuf message to allow:

  • getting/setting of certain 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, converter, nested_resources = {}) ⇒ Wrapper

Returns a new instance of Wrapper.



13
14
15
16
17
# File 'lib/protip/wrapper.rb', line 13

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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/protip/wrapper.rb', line 34

def method_missing(name, *args)
  descriptor = message.class.descriptor

  is_setter_method = name =~ /=$/
  return method_missing_setter(name, *args) if is_setter_method

  is_query_method = name =~ /\?$/
  return method_missing_query(name, *args) if is_query_method

  field = descriptor.detect{|field| field.name.to_sym == name}
  return method_missing_field(field, *args) if field

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

  super
end

Instance Attribute Details

#converterObject (readonly)

Returns the value of attribute converter.



11
12
13
# File 'lib/protip/wrapper.rb', line 11

def converter
  @converter
end

#messageObject (readonly)

Returns the value of attribute message.



11
12
13
# File 'lib/protip/wrapper.rb', line 11

def message
  @message
end

#nested_resourcesObject (readonly)

Returns the value of attribute nested_resources.



11
12
13
# File 'lib/protip/wrapper.rb', line 11

def nested_resources
  @nested_resources
end

Class Method Details

.matchable?(field) ⇒ Boolean

Semi-private check for whether a field should have an associated query method (e.g. field_name?).

Returns:

  • (Boolean)

    Whether the field should have an associated query method on wrappers.



156
157
158
159
160
161
162
# File 'lib/protip/wrapper.rb', line 156

def matchable?(field)
  return false if field.label == :repeated

  field.type == :enum ||
      field.type == :bool ||
      field.type == :message && (field.subtype.name == "google.protobuf.BoolValue")
end

Instance Method Details

#==(wrapper) ⇒ Object



146
147
148
149
150
151
# File 'lib/protip/wrapper.rb', line 146

def ==(wrapper)
  wrapper.class == self.class &&
    wrapper.message.class == message.class &&
    message.class.encode(message) == wrapper.message.class.encode(wrapper.message) &&
    converter == wrapper.converter
end

#as_jsonObject



126
127
128
# File 'lib/protip/wrapper.rb', line 126

def as_json
  to_h.as_json
end

#assign_attributes(attributes) ⇒ NilClass

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.

Returns:

  • (NilClass)


100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/protip/wrapper.rb', line 100

def assign_attributes(attributes)
  attributes.each do |field_name, value|
    field = message.class.descriptor.detect{|field| field.name == field_name.to_s}
    if !field
      raise ArgumentError.new("Unrecognized field: #{field_name}")
    end

    # For inconvertible nested messages, the value should be either a hash or a message
    if field.type == :message && !converter.convertible?(field.subtype.msgclass)
      if value.is_a?(field.subtype.msgclass) # If a message, set it directly
        set(field, value)
      elsif value.is_a?(Hash) # If a hash, pass it through to the nested message
        wrapper = get(field) || build(field.name) # Create the field if it doesn't already exist
        wrapper.assign_attributes value
      else # If value is a simple type (e.g. nil), set the value directly
        set(field, value)
      end
    # Otherwise, if the field is a convertible message or a simple type, we set the value directly
    else
      set(field, value)
    end
  end

  nil # Return nil to match ActiveRecord behavior
end

#build(field_name, attributes = {}) ⇒ Protip::Wrapper

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, converter)
wrapper.inner # => nil
wrapper.build(:inner, str: 'example')
wrapper.inner.str # => 'example'

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

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)

Returns:



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/protip/wrapper.rb', line 75

def build(field_name, attributes = {})

  field = message.class.descriptor.detect{|field| field.name.to_sym == field_name.to_sym}
  if !field
    raise "No field named #{field_name}"
  elsif field.type != :message
    raise "Can only build message fields: #{field_name}"
  elsif converter.convertible?(field.subtype.msgclass)
    raise "Cannot build a convertible field: #{field.name}"
  end

  message[field_name.to_s] = field.subtype.msgclass.new
  wrapper = get(field)
  wrapper.assign_attributes attributes
  wrapper
end

#respond_to?(name) ⇒ Boolean

Returns:

  • (Boolean)


19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/protip/wrapper.rb', line 19

def respond_to?(name)
  if super
    true
  else
    # 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 in the scalar enum case, query methods
    message.class.descriptor.any? do |field|
      regex = /^#{field.name}[=#{self.class.matchable?(field) ? '\\?' : ''}]?$/
      name.to_s =~ regex
    end
  end
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 converted 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 converted messages) of the field values.



132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/protip/wrapper.rb', line 132

def to_h
  hash = {}
  message.class.descriptor.each do |field|
    value = public_send(field.name)
    if field.label == :repeated
      value.map!{|v| v.is_a?(self.class) ? v.to_h : v}
    else
      value = (value.is_a?(self.class) ? value.to_h : value)
    end
    hash[field.name.to_sym] = value
  end
  hash
end