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
-
#column ⇒ Object
readonly
Returns the value of attribute column.
-
#model_class ⇒ Object
readonly
Returns the value of attribute model_class.
Instance Method Summary collapse
-
#_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’sdata_source
protocol for describing where data came from, create the appropriate ColumnData object to represent that data. -
#all_field_names ⇒ Object
What are the names of all fields defined on this flex column?.
-
#column_name ⇒ Object
What’s the name of the actual model column this flex-column uses? Returns a Symbol.
-
#delegation_prefix ⇒ Object
When we delegate methods, what should we prefix them with (if anything)?.
-
#delegation_type ⇒ Object
When we delegate methods, should we delegate them at all (returns
nil
), publicly (:public
), or privately (:private
)?. -
#field(name, *args) ⇒ Object
This is what gets called when you declare a field inside a flex column.
-
#field_named(name) ⇒ Object
Returns the field with the given name, or nil if there is no such field.
-
#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. -
#fields_are_private_by_default? ⇒ Boolean
Are fields in this flex column private by default?.
-
#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 giventarget_class
. -
#is_flex_column_class? ⇒ Boolean
Is this a flex-column class? Of course it is, by definition.
-
#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.
-
#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:.
-
#reset_column_information ⇒ Object
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.
-
#setup!(model_class, column_name, options = { }, &block) ⇒ Object
This is, for all intents and purposes, the initializer (constructor) for this module.
-
#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.
Instance Attribute Details
#column ⇒ Object (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_class ⇒ Object (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 = { :storage_string => storage_string, :data_source => data_source, :unknown_fields => [:unknown_fields] || :preserve, :length_limit => column.limit, :storage => storage, :binary_header => true, :null => column.null } [:binary_header] = false if .has_key?(:header) && (! [:header]) if (! .has_key?(:compress)) [:compress_if_over_length] = DEFAULT_MAX_JSON_LENGTH_BEFORE_COMPRESSION elsif [:compress] [:compress_if_over_length] = [:compress] end FlexColumns::Contents::ColumnData.new(field_set, ) end |
#all_field_names ⇒ Object
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_name ⇒ Object
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_prefix ⇒ Object
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! [:prefix].try(:to_s) end |
#delegation_type ⇒ Object
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 (! .has_key?(:delegate)) case [: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: #{[: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?
161 162 163 164 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 161 def fields_are_private_by_default? ensure_setup! [: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, ) ensure_setup! cn = column_name mn = column_name.to_s mn = "#{[:prefix]}_#{mn}" if [: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 [:visibility] == :private end unless .has_key?(:delegate) && (! [:delegate]) add_custom_methods!(dynamic_methods_module, target_class, ) field_set.include_fields_into(dynamic_methods_module, association_name, target_class, ) 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.
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.)
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_information ⇒ Object
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
ornil
, then field accessors and custom methods defined in this class will not be automatically delegated to from themodel_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).
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, = { }, &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) () @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 |