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 collapse

INSPECT_MAXIMUM_LENGTH_FOR_ANY_ATTRIBUTE_VALUE =
100

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?, reset_column_information, 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.



119
120
121
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 119

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.



125
126
127
# 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 = { }) ⇒ Object

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.



88
89
90
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 88

def as_json(options = { })
  to_hash_for_serialization
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.



191
192
193
194
195
196
197
198
# 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!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.



146
147
148
149
150
151
152
153
# 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_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

#deserialized?Boolean

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

Returns:

  • (Boolean)


139
140
141
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 139

def deserialized?
  column_data.deserialized?
end

#inspectObject

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.



161
162
163
164
165
166
167
168
169
170
171
172
# 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

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



202
203
204
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 202

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.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
# 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_serializationObject

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.



79
80
81
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 79

def to_hash_for_serialization
  @column_data.to_hash
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.



177
178
179
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 177

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.



113
114
115
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 113

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.



185
186
187
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 185

def to_stored_data
  column_data.to_stored_data
end

#touch!Object

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.



133
134
135
# File 'lib/flex_columns/contents/flex_column_contents_base.rb', line 133

def touch!
  column_data.touch!
end