Module: FlexColumns::Definition::FlexColumnContentsClass

Included in:
Contents::FlexColumnContentsBase
Defined in:
lib/flex_columns/definition/flex_column_contents_class.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 Module therefore defines the methods that are available on a flex-column class – directly from inside the block passed to flex_column, for example.

Constant Summary collapse

DEFAULT_MAX_JSON_LENGTH_BEFORE_COMPRESSION =

By default, how long does the generated JSON have to be before we’ll try compressing it?

200

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#columnObject (readonly)

Returns the value of attribute column.



249
250
251
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 249

def column
  @column
end

#model_classObject (readonly)

Returns the value of attribute model_class.



249
250
251
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 249

def model_class
  @model_class
end

Instance Method Details

#_flex_columns_create_column_data(storage_string, data_source) ⇒ Object

Given a string from storage in storage_string, and an object that responds to ColumnData’s data_source protocol for describing where data came from, create the appropriate ColumnData object to represent that data. (storage_string can absolutely be nil, in case there is no data yet.)

This is used by instances of the generated Class to create the ColumnData object that does most of the work of actually serializing/deserializing JSON and storing data for that instance.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 24

def _flex_columns_create_column_data(storage_string, data_source)
  ensure_setup!

  storage = case column.type
  when :binary, :text, :json then column.type
  when :string then :text
  else raise "Unknown storage type: #{column.type.inspect}"
  end

  create_options = {
    :storage_string => storage_string,
    :data_source    => data_source,
    :unknown_fields => options[:unknown_fields] || :preserve,
    :length_limit   => column.limit,
    :storage        => storage,
    :binary_header  => true,
    :null           => column.null
  }

  create_options[:binary_header] = false if options.has_key?(:header) && (! options[:header])

  if (! options.has_key?(:compress))
    create_options[:compress_if_over_length] = DEFAULT_MAX_JSON_LENGTH_BEFORE_COMPRESSION
  elsif options[:compress]
    create_options[:compress_if_over_length] = options[:compress]
  end

  FlexColumns::Contents::ColumnData.new(field_set, create_options)
end

#all_field_namesObject

What are the names of all fields defined on this flex column?



145
146
147
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 145

def all_field_names
  field_set.all_field_names
end

#column_nameObject

What’s the name of the actual model column this flex-column uses? Returns a Symbol.



139
140
141
142
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 139

def column_name
  ensure_setup!
  column.name.to_sym
end

#delegation_prefixObject

When we delegate methods, what should we prefix them with (if anything)?



118
119
120
121
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 118

def delegation_prefix
  ensure_setup!
  options[:prefix].try(:to_s)
end

#delegation_typeObject

When we delegate methods, should we delegate them at all (returns nil), publicly (:public), or privately (:private)?



125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 125

def delegation_type
  ensure_setup!
  return :public if (! options.has_key?(:delegate))

  case options[:delegate]
  when nil, false then nil
  when true, :public then :public
  when :private then :private
  # OK to raise an untyped error here -- we should've caught this in #validate_options.
  else raise "Impossible value for :delegate: #{options[:delegate]}"
  end
end

#field(name, *args) ⇒ Object

This is what gets called when you declare a field inside a flex column.



55
56
57
58
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 55

def field(name, *args)
  ensure_setup!
  field_set.field(name, *args)
end

#field_named(name) ⇒ Object

Returns the field with the given name, or nil if there is no such field.



61
62
63
64
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 61

def field_named(name)
  ensure_setup!
  field_set.field_named(name)
end

#field_with_json_storage_name(json_storage_name) ⇒ Object

Returns the field that stores its JSON under the given key (json_storage_name), or nil if there is no such field.



68
69
70
71
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 68

def field_with_json_storage_name(json_storage_name)
  ensure_setup!
  field_set.field_with_json_storage_name(json_storage_name)
end

#fields_are_private_by_default?Boolean

Are fields in this flex column private by default?

Returns:

  • (Boolean)


161
162
163
164
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 161

def fields_are_private_by_default?
  ensure_setup!
  options[:visibility] == :private
end

#include_fields_into(dynamic_methods_module, association_name, target_class, options) ⇒ Object

Tells this flex column that you want to include its methods into the given dynamic_methods_module, which is included in the given target_class. (We only use target_class to make sure we don’t define methods that are already present on the given target_class.) association_name is the name of the association that, from the given target_class, will return a model instance that contains this flex column.

options specifies options for the inclusion; it can specify :visibility to change whether methods are public or private, :delegate to turn off delegation of anything other than the flex column itself, or :prefix to set a prefix for the delegated method names.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 87

def include_fields_into(dynamic_methods_module, association_name, target_class, options)
  ensure_setup!

  cn = column_name
  mn = column_name.to_s
  mn = "#{options[:prefix]}_#{mn}" if options[:prefix]

  # Make sure we don't overwrite some #method_missing magic that defines a column accessor, or something
  # similar.
  if target_class._flex_columns_safe_to_define_method?(mn)
    dynamic_methods_module.define_method(mn) do
      associated_object = send(association_name) || send("build_#{association_name}")
      associated_object.send(cn)
    end
    dynamic_methods_module.private(mn) if options[:visibility] == :private
  end

  unless options.has_key?(:delegate) && (! options[:delegate])
    add_custom_methods!(dynamic_methods_module, target_class, options)
    field_set.include_fields_into(dynamic_methods_module, association_name, target_class, options)
  end
end

#is_flex_column_class?Boolean

Is this a flex-column class? Of course it is, by definition. We just use this for argument validation in some places.

Returns:

  • (Boolean)


75
76
77
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 75

def is_flex_column_class?
  true
end

#object_for(model_instance) ⇒ Object

Given an instance of the model that this flex column is defined on, return the appropriate flex-column object for that instance. This simply delegates to #_flex_column_object_for on that model instance.



112
113
114
115
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 112

def object_for(model_instance)
  ensure_setup!
  model_instance._flex_column_object_for(column.name)
end

#requires_serialization_on_save?(model) ⇒ Boolean

Given a model instance, do we need to save this column? This is true under one of two cases:

  • Someone has deserialized the column by accessing it (or calling #touch! on it);

  • The column is non-NULL, and there’s no data in it right now. (Saving it will populate it with an empty string.)

Returns:

  • (Boolean)


153
154
155
156
157
158
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 153

def requires_serialization_on_save?(model)
  maybe_flex_object = model._flex_column_object_for(column_name, false)
  out = true if maybe_flex_object && maybe_flex_object.deserialized?
  out ||= true if ((! column.null) && (! model[column_name]))
  out
end

#reset_column_informationObject

This method gets called when ActiveRecord::Base.reset_column_information is called on the underlying model; this simply updates our notion of what column is present. Most importantly, this will correctly switch us from a table-does-not-exist state to a table-exists state (if you migrate the table in), but it also will correctly switch from one column type to another, etc.



227
228
229
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 227

def reset_column_information
  @column = find_column(column_name)
end

#setup!(model_class, column_name, options = { }, &block) ⇒ Object

This is, for all intents and purposes, the initializer (constructor) for this module. But because it’s a module (and has to be), this can’t actually be #initialize. (Another way of saying it: objects have initializers; classes do not.)

You must call this method exactly once for each class that extends this module, and before you call any other method.

model_class must be the ActiveRecord model class for this flex column. column_name must be the name of the column that you’re using as a flex column. options can contain any of:

:visibility

If :private, then all field accessors (readers and writers) will be private by default, unless overridden in their field declaration.

:delegate

If specified and false or nil, then field accessors and custom methods defined in this class will not be automatically delegated to from the model_class.

:prefix

If specified (as a Symbol or String), then field accessors and custom methods delegated from the model_class will be prefixed with this string, followed by an underscore.

:unknown_fields

If specified and :delete, then, if the JSON string for an instance contains fields that aren’t declared in this class, they will be removed from the JSON when saving back out to the database. This is dangerous, but powerful, if you want to keep your data clean.

:compress

If specified and false, this column will never be compressed. If specified as a number, then, when serializing data, we’ll try to compress it if the uncompressed version is at least that many bytes long; we’ll store the compressed version if it’s no more than 95% as long as the uncompressed version. The default is 200. Also note that compression requires a binary storage type for the underlying column.

:header

If the underlying column is of binary storage type, then, by default, we use a tiny header to indicate what kind of data is stored there and whether it’s compressed or not. If this is set to false, disables this header (and therefore also disables compression).

Raises:

  • (ArgumentError)


193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 193

def setup!(model_class, column_name, options = { }, &block)
  raise ArgumentError, "You can't call setup! twice!" if @model_class || @column

  # Make really sure we're being declared in the right kind of class.
  unless model_class.kind_of?(Class) && model_class.respond_to?(:has_any_flex_columns?) && model_class.has_any_flex_columns?
    raise ArgumentError, "Invalid model class: #{model_class.inspect}"
  end

  raise ArgumentError, "Invalid column name: #{column_name.inspect}" unless column_name.kind_of?(Symbol)

  @model_class = model_class
  @column = find_column(column_name)

  validate_options(options)

  @options = options
  @field_set = FlexColumns::Definition::FieldSet.new(self)

  class_name = "#{column_name.to_s.camelize}FlexContents".to_sym
  @model_class.send(:remove_const, class_name) if @model_class.const_defined?(class_name)
  @model_class.const_set(class_name, self)

  # Keep track of which methods were present before and after calling the block that was passed in; this is how
  # we know which methods were declared custom, so we know which ones to add delegation for.
  methods_before = instance_methods
  block_result = class_eval(&block) if block
  @custom_methods = (instance_methods - methods_before).map(&:to_sym)
  block_result
end

#sync_methods!Object

Tells this class to re-publish all its methods to the DynamicMethodsModule it uses internally, and to the model class it’s a part of.

Because Rails in development mode is constantly redefining classes, and we don’t want old cruft that you’ve removed to hang around, we use a “remove absolutely all methods, then add back only what’s defined now” strategy.



237
238
239
240
241
242
243
244
245
246
247
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 237

def sync_methods!
  @dynamic_methods_module ||= FlexColumns::Util::DynamicMethodsModule.new(self, :FlexFieldsDynamicMethods)
  @dynamic_methods_module.remove_all_methods!

  field_set.add_delegated_methods!(@dynamic_methods_module, model_class._flex_column_dynamic_methods_module, model_class)

  if delegation_type
    add_custom_methods!(model_class._flex_column_dynamic_methods_module, model_class,
      :visibility => (delegation_type == :private ? :private : :public))
  end
end