class FlexColumns::Contents::FlexColumnContentsBase

When you declare a flex column, we actually generate a brand-new Class for that column; instances of that flex column are instances of this new Class. This class acquires functionality from two places: FlexColumnContentsBase, which defines its instance methods, and FlexColumnContentsClass, which defines its class methods. (While FlexColumnContentsBase is an actual Class, FlexColumnContentsClass is a Module that FlexColumnContentsBase +extend+s. Both could be combined, but, simply for readability and maintainability, it was better to make them separate.)

This Class therefore defines the methods that are available on an instance of a flex-column class – on the object returned by my_user.user_attributes, for example.

Constants

INSPECT_MAXIMUM_LENGTH_FOR_ANY_ATTRIBUTE_VALUE

Attributes

column_data[R]
model_instance[R]

Public Class Methods

new(input) click to toggle source

Creates a new instance. input is the source of data we should use: normally this is an instance of the enclosing model class (e.g., User), but it can also be a simple String (if you’re creating an instance using the bulk API – +HasFlexColumns#create_flex_objects_from+, for example) containing the stored JSON for this object, or nil (if you’re doing the same, but have no source data).

The reason this class hangs onto the whole model instance, instead of just the string, is twofold:

  • It needs to be able to add validation errors back onto the model instance;

  • It wants to be able to pass a description of the model instance into generated exceptions and the ActiveSupport::Notifications calls made, so that when things go wrong or you’re doing performance work, you can understand what row in what table contains incorrect data or data that is making things slow.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 38
    def initialize(input)
      storage_string = nil

      if input.kind_of?(String)
        @model_instance = nil
        storage_string = input
        @source_string = input
      elsif (! input)
        @model_instance = nil
        storage_string = nil
      elsif input.class.equal?(self.class.model_class)
        @model_instance = input
        storage_string = @model_instance[self.class.column_name]
      else
        raise ArgumentError, %{You can create a #{self.class.name} from a String, nil, or #{self.class.model_class.name} (#{self.class.model_class.object_id}),
not #{input.inspect} (#{input.object_id}).}
      end

      # Creates an instance of FlexColumns::Contents::ColumnData, which is the thing that does most of the actual
      # work with the underlying data for us.
      @column_data = self.class._flex_columns_create_column_data(storage_string, self)
    end

Public Instance Methods

[](field_name) click to toggle source

Provides Hash-style read access to fields in the flex column. This delegates to the ColumnData object, which does most of the actual work.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 119
def [](field_name)
  column_data[field_name]
end
[]=(field_name, new_value) click to toggle source

Provides Hash-style write access to fields in the flex column. This delegates to the ColumnData object, which does most of the actual work.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 125
def []=(field_name, new_value)
  column_data[field_name] = new_value
end
as_json(options = { }) click to toggle source

Make sure this flex-column object itself is smart enough to turn itself into JSON correctly.

Most importantly, this method has NOTHING to do with our internal ‘serialize a column as JSON’ mechanisms. It is ONLY called if you try to serialize something that in turn points directly to (i.e., not via the enclosing ActiveRecord object) this flex-column object.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 88
def as_json(options = { })
  to_hash_for_serialization
end
before_save!() click to toggle source

Called via the ActiveRecord::Base#before_save hook that gets installed on the enclosing model instance. This is what actually serializes the column data and sets it on the ActiveRecord model when it’s being saved.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 191
def before_save!
  return unless model_instance

  # Make sure we only save if we need to -- otherwise, save the CPU cycles.
  if self.class.requires_serialization_on_save?(model_instance)
    model_instance[column_name] = column_data.to_stored_data
  end
end
before_validation!() click to toggle source

Called via the ActiveRecord::Base#before_validation hook that gets installed on the enclosing model instance. This runs any validations that are present on this flex-column object, and then propagates any errors back to the enclosing model instance, so that errors show up there, as well.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 146
def before_validation!
  return unless model_instance
  unless valid?
    errors.each do |name, message|
      model_instance.errors.add("#{column_name}.#{name}", message)
    end
  end
end
describe_flex_column_data_source() click to toggle source

Returns a String, appropriate for human consumption, that describes the model instance we’re created from (or raw String, if that’s the case). This is used solely by the errors in FlexColumns::Errors, and is used to give good, actionable diagnostic messages when something goes wrong.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 64
def describe_flex_column_data_source
  if model_instance
    out = self.class.model_class.name.dup
    out << " ID #{model_instance.id}" if model_instance.id
    out << ", column #{self.class.column_name.inspect}"
  else
    "(data passed in by client, for #{self.class.model_class.name}, column #{self.class.column_name.inspect})"
  end
end
deserialized?() click to toggle source

Has the column been deserialized? A column is deserialized if someone has tried to read from or write to it, or if someone has called touch!.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 139
def deserialized?
  column_data.deserialized?
end
inspect() click to toggle source

NOTE: This method WILL deserialize the contents of the column, if it hasn’t already been deserialized. This is extremely useful for debugging, and almost certainly what you want, but if, for some reason, you call inspect on every single instance of a flex-column you get back from the database, you’ll incur a needless performance penalty. You have been warned.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 161
def inspect
  string_hash = { }
  column_data.to_hash.each do |k,v|
    v_string = v.to_s
    if v_string.length > INSPECT_MAXIMUM_LENGTH_FOR_ANY_ATTRIBUTE_VALUE
      v_string = "#{v_string[0..(INSPECT_MAXIMUM_LENGTH_FOR_ANY_ATTRIBUTE_VALUE - 1)]}..."
    end
    string_hash[k] = v_string
  end

  "<#{self.class.name}: #{string_hash.inspect}>"
end
keys() click to toggle source

Returns an Array containing the names (as Symbols) of all fields on this flex-column object that currently have any data set for them &mdash; i.e., that are not nil.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 202
def keys
  column_data.keys
end
notification_hash_for_flex_column_data_source() click to toggle source

Returns a Hash, appropriate for integration into the payload of an ActiveSupport::Notification call, that describes the model instance we’re created from (or raw String, if that’s the case). This is used by the calls made to ActiveSupport::Notifications when a flex-column object is serialized or deserialized, and is used to give good, actionable content when monitoring system performance.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 96
def notification_hash_for_flex_column_data_source
  out = {
    :model_class => self.class.model_class,
    :column_name => self.class.column_name
  }

  if model_instance
    out[:model] = model_instance
  else
    out[:source] = @source_string
  end

  out
end
to_hash_for_serialization() click to toggle source

See the comment above FlexColumns::HasFlexColumns#read_attribute_for_serialization – this is responsible for correctly turning a flex-column object into a hash for serializing *the entire enclosing ActiveRecord model*.

Most importantly, this method has NOTHING to do with our internal ‘serialize a column as JSON’ mechanisms. It is ONLY called if you try to serialize the enclosing ActiveRecord instance.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 79
def to_hash_for_serialization
  @column_data.to_hash
end
to_json() click to toggle source

Returns a JSON string representing the current contents of this flex column. Note that this is not always exactly what gets stored in the database, because of binary columns and compression; for that, use to_stored_data, below.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 177
def to_json
  column_data.to_json
end
to_model() click to toggle source

This is required by ActiveModel::Validations; it’s asking, “what’s the ActiveModel object I should use for validation purposes?”. And, here, it’s this same object.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 113
def to_model
  self
end
to_stored_data() click to toggle source

Returns a String representing exactly the data that will get stored in the database, for this flex column. This will be a UTF-8-encoded String containing pure JSON if this is a textual column, or, if it’s a binary column, either a UTF-8-encoded JSON String prefixed by a small header, or a BINARY-encoded String containing GZip’ed JSON, prefixed by a small header.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 185
def to_stored_data
  column_data.to_stored_data
end
touch!() click to toggle source

Sometimes you want to deserialize a flex column explicitly, without actually changing anything in it. (For example, if you set :unknown_fields => :delete, then unknown fields are removed from a column only if it has been deserialized before you save it.) While you could accomplish this by simply accessing any field of the column, it’s cleaner and more clear what you’re doing to just call this method.

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 133
def touch!
  column_data.touch!
end

Private Instance Methods

column() click to toggle source

What’s the ActiveRecord ColumnDefinition object for this flex column?

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 215
def column
  self.class.column
end
column_name() click to toggle source

What’s the name of the flex column itself?

# File lib/flex_columns/contents/flex_column_contents_base.rb, line 210
def column_name
  self.class.column_name
end