Class: FlexColumns::Contents::FlexColumnContentsBase

Inherits:
Object
  • Object
show all
Extended by:
Definition::FlexColumnContentsClass
Includes:
ActiveModel::Validations
Defined in:
lib/flex_columns/contents/flex_column_contents_base.rb

Overview

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

Constant Summary

Constants included from Definition::FlexColumnContentsClass

Definition::FlexColumnContentsClass::DEFAULT_MAX_JSON_LENGTH_BEFORE_COMPRESSION

Instance Attribute Summary

Attributes included from Definition::FlexColumnContentsClass

#model_class

Instance Method Summary collapse

Methods included from Definition::FlexColumnContentsClass

_flex_columns_create_column_data, all_field_names, delegation_prefix, delegation_type, field, field_named, field_with_json_storage_name, fields_are_private_by_default?, include_fields_into, is_flex_column_class?, object_for, requires_serialization_on_save?, setup!, sync_methods!

Constructor Details

#initialize(input) ⇒ FlexColumnContentsBase

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.



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

Instance Method Details

#[](field_name) ⇒ Object

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



101
102
103
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 101

def [](field_name)
  column_data[field_name]
end

#[]=(field_name, new_value) ⇒ Object

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



107
108
109
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 107

def []=(field_name, new_value)
  column_data[field_name] = new_value
end

#before_save!Object

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.



159
160
161
162
163
164
165
166
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 159

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!Object

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.



133
134
135
136
137
138
139
140
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 133

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_sourceObject

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.



64
65
66
67
68
69
70
71
72
# 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

#keysObject

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.



170
171
172
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 170

def keys
  column_data.keys
end

#notification_hash_for_flex_column_data_sourceObject

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.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 78

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_jsonObject

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.



145
146
147
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 145

def to_json
  column_data.to_json
end

#to_modelObject

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.



95
96
97
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 95

def to_model
  self
end

#to_stored_dataObject

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.



153
154
155
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 153

def to_stored_data
  column_data.to_stored_data
end

#touch!Object

A flex column has been “touched” if it has had at least one field changed to a different value than it had before, or if someone has called #touch! on it. If a column has not been touched, validations are not run on it, nor is it re-serialized back out to the database on save!. Generally, this is a good thing: it increases performance substantially for times when you haven’t actually changed the flex column’s contents at all. It does mean that invalid data won’t be detected and unknown fields won’t be removed (if you’ve specified :unknown_fields => delete), however.

There may be times, however, when you want to make sure the column is stored back out (including removing any unknown fields, if you selected that option), or to make sure that validations get run, no matter what. In this case, you can call #touch!.



121
122
123
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 121

def touch!
  column_data.touch!
end

#touched?Boolean

Has at least one field in the column been changed, or has someone called #touch! ?

Returns:

  • (Boolean)


126
127
128
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 126

def touched?
  column_data.touched?
end