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:.
-
#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.
253 254 255 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 253 def column @column end |
#model_class ⇒ Object (readonly)
Returns the value of attribute model_class.
253 254 255 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 253 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.
22 23 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 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 22 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?
143 144 145 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 143 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.
137 138 139 140 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 137 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)?
116 117 118 119 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 116 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
)?
123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 123 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.
53 54 55 56 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 53 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.
59 60 61 62 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 59 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.
66 67 68 69 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 66 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?
159 160 161 162 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 159 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.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 85 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.
73 74 75 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 73 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.
110 111 112 113 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 110 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.)
151 152 153 154 155 156 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 151 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 |
#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).
191 192 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 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 191 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) column = model_class.columns.detect { |c| c.name.to_s == column_name.to_s } unless column raise FlexColumns::Errors::NoSuchColumnError, %{You're trying to define a flex column #{column_name.inspect}, but the model you're defining it on, #{model_class.name}, seems to have no column named that. It has columns named: #{model_class.columns.map(&:name).sort_by(&:to_s).join(", ")}.} end unless column.type == :binary || column.text? || column.sql_type == "json" # for PostgreSQL >= 9.2, which has a native JSON data type raise FlexColumns::Errors::InvalidColumnTypeError, %{You're trying to define a flex column #{column_name.inspect}, but that column (on model #{model_class.name}) isn't of a type that accepts text. That column is of type: #{column.type.inspect}.} end () @model_class = model_class @column = column @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.
241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/flex_columns/definition/flex_column_contents_class.rb', line 241 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 |