Class: Flattener::Base
- Inherits:
-
Object
- Object
- Flattener::Base
- Includes:
- ActiveModel::Model
- Defined in:
- lib/has_helpers/flattener/base.rb
Constant Summary collapse
- EXCLUDE_COLUMNS =
e.g. :dw, :api, :importers
["delta", "express_add", "photo_updated_at", "photo_file_name", "photo_content_type", "photo_file_size"]
- CONSTANT_COLUMNS =
["abbreviation", "id", "name"]
Class Attribute Summary collapse
- .associations ⇒ Object
- .columns ⇒ Object
- .default_for ⇒ Object
-
.is_pivot ⇒ Object
(also: is_pivot?)
Returns the value of attribute is_pivot.
- .model_klass ⇒ Object
-
.pivot_associations ⇒ Object
readonly
Returns the value of attribute pivot_associations.
-
.pivot_columns ⇒ Object
readonly
Returns the value of attribute pivot_columns.
-
.pivot_count ⇒ Object
readonly
Returns the value of attribute pivot_count.
Instance Attribute Summary collapse
Class Method Summary collapse
-
.add(*args, **options, &block) ⇒ Object
Add/update an association to to be included when using a flattener.
-
.add_column(name, sql_type, **options) ⇒ Object
Add a custom column to the flattener.
-
.belongs_to(name, scope = nil, **options) ⇒ Object
HACK Does this really belong inside of the flattener?.
-
.bypass_constant_column_removals?(bypass = false) ⇒ Boolean
When including HasHelpers::Constant to a class this removes all columns that are not from the constants relation.
- .exclude_columns(*column_names) ⇒ Object
- .find_association(name) ⇒ Object
- .find_column_association(foreign_key) ⇒ Object
-
.flatten(trait: nil) ⇒ Object
rubocop:disable Lint/IneffectiveAccessModifier.
-
.has_many(name, scope = nil, **options) ⇒ Object
HACK Does this really belong inside of the flattener?.
- .include_column?(column) ⇒ Boolean
- .is_constant?(klass = model_klass) ⇒ Boolean
-
.join(name, **options, &block) ⇒ Object
Adds/updates an association, usually on a join table, aliases the foreign key of the added association to :id, and aliases the primary key to :link_id.
- .model_associations ⇒ Object
- .model_columns ⇒ Object
-
.pivot!(count: 1, expand_on: nil, expand_over: [], excluded: [/id\z/, "created_at"], included: [], is_lookup: nil, &block) ⇒ Object
Defines the pivot for the current flattener.
- .pivot_expand_on(klass = nil) ⇒ Object
-
.pivot_or_self ⇒ Object
If the current flattener is a pivot then this method returns a class which generates only the pivots attributes and associations.
-
.remove(*args) ⇒ Object
Remove a association(s) from use by the flattener.
-
.trait(trait_name, &block) ⇒ Object
Traits allow you to name groups of flattener settings inside of a block.
- .update_column(name, **options) ⇒ Object
Instance Method Summary collapse
- #add(*names, **options, &block) ⇒ Object
-
#add_column(name, sql_type, **options) ⇒ Object
HACK Does this really belong inside of the flattener?.
- #associations ⇒ Object
- #belongs_to(*args) ⇒ Object
- #columns ⇒ Object
- #exclude_columns(*column_names) ⇒ Object
- #flatten(depth = 1) ⇒ Object
- #include_columns(*column_names) ⇒ Object
- #join(name, **options, &block) ⇒ Object
- #remove(*args) ⇒ Object
- #update_column(name, **options) ⇒ Object
Class Attribute Details
.associations ⇒ Object
107 108 109 |
# File 'lib/has_helpers/flattener/base.rb', line 107 def associations model_associations.reject(&:removed) end |
.columns ⇒ Object
254 255 256 257 258 |
# File 'lib/has_helpers/flattener/base.rb', line 254 def columns @columns ||= model_columns. lazy. select { |sql_column| include_column?(sql_column) } end |
.default_for ⇒ Object
266 267 268 |
# File 'lib/has_helpers/flattener/base.rb', line 266 def default_for @default_for ||= {} end |
.is_pivot ⇒ Object Also known as: is_pivot?
Returns the value of attribute is_pivot.
97 98 99 |
# File 'lib/has_helpers/flattener/base.rb', line 97 def is_pivot @is_pivot end |
.model_klass ⇒ Object
100 101 102 103 104 105 |
# File 'lib/has_helpers/flattener/base.rb', line 100 def model_klass @model_klass ||= begin klass = singleton_class? ? superclass : self # Singleton classes are not named, but in this case we really want the name of its superclass. klass.name.gsub("Flatteners::", "").constantize end end |
.pivot_associations ⇒ Object (readonly)
Returns the value of attribute pivot_associations.
98 99 100 |
# File 'lib/has_helpers/flattener/base.rb', line 98 def pivot_associations @pivot_associations end |
.pivot_columns ⇒ Object (readonly)
Returns the value of attribute pivot_columns.
98 99 100 |
# File 'lib/has_helpers/flattener/base.rb', line 98 def pivot_columns @pivot_columns end |
.pivot_count ⇒ Object (readonly)
Returns the value of attribute pivot_count.
98 99 100 |
# File 'lib/has_helpers/flattener/base.rb', line 98 def pivot_count @pivot_count end |
Instance Attribute Details
#include_for ⇒ Object
55 56 57 |
# File 'lib/has_helpers/flattener/base.rb', line 55 def include_for @include_for ||= Flattener::Base.default_for.keys end |
Class Method Details
.add(*args, **options, &block) ⇒ Object
Add/update an association to to be included when using a flattener.
Examples
Flattener.for(Advisor).add :bank_accounts, for: [:dw, api] the 'for:' argument will default to all of the options available
class Flatteners::Advisor < Flattener::Base add :errors_and_omissions end
324 325 326 327 328 329 330 331 |
# File 'lib/has_helpers/flattener/base.rb', line 324 def self.add(*args, **, &block) args.each do |name| if (association = find_association(name)) association.update(**, for: ::Flattener::Base.default_for.lazy.map { |k| k.first }, removed: false) association.flattener = Flattener.for(association.klass).new.tap { |f| f.instance_eval(&block) } if block.present? end end end |
.add_column(name, sql_type, **options) ⇒ Object
Add a custom column to the flattener
Example
Flattener.for(Advisor).add_column :amount, "numeric(20,5)" Flattener.for(Advisor).add_column :name, "text"
HACK Does this really belong inside of the flattener?
341 342 343 344 |
# File 'lib/has_helpers/flattener/base.rb', line 341 def self.add_column(name, sql_type, **) sql_column = Flattener::Column.build_sql_column(name, sql_type) model_columns << Flattener::Column.new(sql_column: sql_column, klass: model_klass, association: find_column_association(sql_column.name), **) end |
.belongs_to(name, scope = nil, **options) ⇒ Object
HACK Does this really belong inside of the flattener?
112 113 114 115 116 117 118 |
# File 'lib/has_helpers/flattener/base.rb', line 112 def belongs_to(name, scope = nil, **) ar_association = ::ActiveRecord::Associations::Builder::BelongsTo.create_reflection(model_klass, name, scope, ) Flattener::Association.new(ar_association).tap do |association| model_associations << association add_column ar_association.foreign_key, "integer" end end |
.bypass_constant_column_removals?(bypass = false) ⇒ Boolean
When including HasHelpers::Constant to a class this removes all columns that are not from the constants relation. This will bypass that validation if true including other columns
276 277 278 |
# File 'lib/has_helpers/flattener/base.rb', line 276 def bypass_constant_column_removals?(bypass = false) @bypass_constant_column_removals ||= bypass end |
.exclude_columns(*column_names) ⇒ Object
260 261 262 263 264 |
# File 'lib/has_helpers/flattener/base.rb', line 260 def exclude_columns(*column_names) (@exclude_columns ||= []).tap do |exclude_columns| exclude_columns.concat column_names if column_names end end |
.find_association(name) ⇒ Object
413 414 415 |
# File 'lib/has_helpers/flattener/base.rb', line 413 def self.find_association(name) model_associations.detect { |a| a.ar_association_name == name } end |
.find_column_association(foreign_key) ⇒ Object
417 418 419 |
# File 'lib/has_helpers/flattener/base.rb', line 417 def self.find_column_association(foreign_key) model_associations.detect { |a| a.macro != :has_many && a.ar_association.foreign_key.to_s == foreign_key.to_s } end |
.flatten(trait: nil) ⇒ Object
rubocop:disable Lint/IneffectiveAccessModifier
307 308 309 310 311 |
# File 'lib/has_helpers/flattener/base.rb', line 307 def self.flatten(trait: nil) new.tap do |flattener| flattener.instance_eval(&traits[trait]) if traits[trait] end.flatten end |
.has_many(name, scope = nil, **options) ⇒ Object
HACK Does this really belong inside of the flattener?
121 122 123 124 125 126 |
# File 'lib/has_helpers/flattener/base.rb', line 121 def has_many(name, scope = nil, **) ar_association = ::ActiveRecord::Associations::Builder::HasMany.create_reflection(model_klass, name, scope, ) Flattener::Association.new(ar_association).tap do |association| model_associations << association end end |
.include_column?(column) ⇒ Boolean
270 271 272 |
# File 'lib/has_helpers/flattener/base.rb', line 270 def include_column?(column) EXCLUDE_COLUMNS.exclude?(column.name) && exclude_columns.exclude?(column.name.to_sym) && (!is_constant?(model_klass) || bypass_constant_column_removals? || CONSTANT_COLUMNS.include?(column.name)) end |
.is_constant?(klass = model_klass) ⇒ Boolean
280 281 282 |
# File 'lib/has_helpers/flattener/base.rb', line 280 def is_constant?(klass = model_klass) klass.include?(HasHelpers::Constant) end |
.join(name, **options, &block) ⇒ Object
Adds/updates an association, usually on a join table, aliases the foreign key of the added association to :id, and aliases the primary key to :link_id.
This is for the benefit of the end user not having to understand the database relationships
Example
class Flatteners::BankAccountOwner < Flattener::Base alias :bank_account join :bank_account end
In this example, the end result of flattening will produce:
- :bank_account_owner aliased to :bank_account
- an attribute bank_account_id, which is the bank_account_owner's :bank_account_id
- an attribue bank_account_link_id, which is the bank_account_owner's :id
378 379 380 381 382 383 384 385 |
# File 'lib/has_helpers/flattener/base.rb', line 378 def self.join(name, **, &block) if add(name, always_include: true, merge_into_parent: true, **, &block) # rubocop:disable Rails/SkipsModelValidations update_column(name.to_s.foreign_key, alias_name: :id) update_column(model_klass.primary_key, alias_name: :link_id) # rubocop:enable Rails/SkipsModelValidations end end |
.model_associations ⇒ Object
284 285 286 |
# File 'lib/has_helpers/flattener/base.rb', line 284 def model_associations @model_associations ||= model_klass.reflect_on_all_associations.map { |a| Flattener::Association.new(a) } end |
.model_columns ⇒ Object
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'lib/has_helpers/flattener/base.rb', line 288 def model_columns @model_columns ||= begin # Flatteners are loaded before running migrations # Only load columns if the table exists connection = ::ActiveRecord::Base.connection table_name = model_klass.table_name model_klass_columns = if connection.table_exists?(table_name) || connection.view_exists?(table_name) model_klass.columns else [] end model_klass_columns.map { |sql_column| Flattener::Column.new(sql_column: sql_column, klass: model_klass, association: find_column_association(sql_column.name)) } end end |
.pivot!(count: 1, expand_on: nil, expand_over: [], excluded: [/id\z/, "created_at"], included: [], is_lookup: nil, &block) ⇒ Object
Defines the pivot for the current flattener. HACK: See notes for belongs_to *
Options
:count The number of pivots (sets of fields) to generate. Defaults to 1.
:expand_on Collection or proc returning the names on which to expand. A proc may accept one argument which is the class of the parent flattener.
:is_lookup Set this to true if the pivot table requires a look-up.
Examples
pivot!(count: 3) pivot!(count: 5, expand_on: %w[home office other]) pivot!( count: 5, expand_on: -> (klass) { HasContactInfo::AddressLocation.scoped_to(klass)::ALL.map(&:name) } ) pivot!(count: 5, is_lookup: true)
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 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 |
# File 'lib/has_helpers/flattener/base.rb', line 142 def pivot!( count: 1, expand_on: nil, expand_over: [], excluded: [/id\z/, "created_at"], included: [], # Overrides excluded values is_lookup: nil, &block ) self.is_pivot = true @pivot_count = count @pivot_expand_on = @pivot_is_lookup = is_lookup if block # FIXME: This is too hacky. We need to find an alternative way to define pivots * # Get a clone of the current flattener for use in evaluating the # given block. This allow the pivot associations and columns to be # determined while preserving the columns and associtions on the # non-pivoted flattener. instance = clone if @model_associations.present? instance.instance_eval do @model_associations = @model_associations.map(&:clone) end end instance.model_klass = model_klass # model_klass may otherwise fail since the instance class has no name. else instance = self end ## Determine which associations to use for the pivot # Remove associations except for constants (default behavior, perhaps there is something better) instance.remove( *(associations.reject(&:is_constant?)).map(&:name) ) # Eval the block after removing associations so that changes # made in the block remain in effect. instance.instance_eval(&block) if block @pivot_associations = instance.associations.dup # Determine the pivot columns @pivot_columns = instance.columns.lazy. reject { |col| excluded.any? { |ex| ex.match(col.name) } }. to_a # Must force to an array to avoid an infinite loop when adding columns and otherwise incorrect results. # Remove columns exclude_columns( *@pivot_columns.lazy. reject do |col| # Do not exclude any columns which have been explicitly included. included.any? { |inc| inc.match(col.name) } end. map(&:name). map(&:to_sym) ) @pivot_columns.concat( instance.send(:model_columns). select do |col| included.any? { |inc| inc.match(col.name) } end. to_a ) # Add expansions expansions = Array() expansions << nil if expansions.none? # Add a blank entry which provides a value to iterate over. expansions.each do |expansion| expansion = "#{ expansion.underscore }_" if expansion (1..count).each do |idx| # add expanded columns @pivot_columns.each do |col| add_column("#{ expansion }#{ col.name.to_sym }_#{ idx }", col.sql_type) end # add expanded associations @pivot_associations.each do |assoc| add_column("#{ expansion }#{ assoc.foreign_key }_#{ idx }", "integer") add_column("#{ expansion }#{ assoc.foreign_key.to_s.remove(/_id\z/) }_#{ idx }", "character varying") end end end end |
.pivot_expand_on(klass = nil) ⇒ Object
246 247 248 249 250 251 252 |
# File 'lib/has_helpers/flattener/base.rb', line 246 def (klass = nil) if @pivot_expand_on.respond_to?(:call) @pivot_expand_on.call(*klass) else Array(@pivot_expand_on) end end |
.pivot_or_self ⇒ Object
If the current flattener is a pivot then this method returns a class which generates only the pivots attributes and associations. Otherwise, it returns the current flattener class.
231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/has_helpers/flattener/base.rb', line 231 def pivot_or_self if is_pivot # Return a duplicate of the current flattener class with the # pivot additions mixed in. self.dup.tap do |klass| klass.prepend(::Flattener::Pivot) klass.is_lookup = @pivot_is_lookup end else self end end |
.remove(*args) ⇒ Object
Remove a association(s) from use by the flattener
Example
Flattener.for(Advisor).remove :bank_accounts, :errors_and_omissions
352 353 354 355 356 357 358 |
# File 'lib/has_helpers/flattener/base.rb', line 352 def self.remove(*args) args.each do |name| if (association = find_association(name)) association.remove end end end |
.trait(trait_name, &block) ⇒ Object
Traits allow you to name groups of flattener settings inside of a block.
Example
class Flatteners::Advisor < Flattener::Base trait :dw do exclude_columns :organization_id remove :adjustments end end
Flattener.for(Advisor).flatten(trait: :dw)
400 401 402 |
# File 'lib/has_helpers/flattener/base.rb', line 400 def self.trait(trait_name, &block) traits[trait_name] = block end |
.update_column(name, **options) ⇒ Object
404 405 406 407 408 409 410 411 |
# File 'lib/has_helpers/flattener/base.rb', line 404 def self.update_column(name, **) raise ArgumentError, "nil is not a valid column" if name.nil? begin columns.detect { |column| column.name.to_s == name.to_s }.update() rescue NoMethodError raise "No column `#{name}` on #{self.name}" if ::HasUtils::Env.development? end end |
Instance Method Details
#add(*names, **options, &block) ⇒ Object
17 18 19 20 21 22 23 24 |
# File 'lib/has_helpers/flattener/base.rb', line 17 def add(*names, **, &block) names.each do |name| association = associations.detect { |a| a.ar_association.name == name } associations << association = self.class.find_association(name).dup unless association association.update(**, for: Flattener::Base.default_for.keys) association.flattener = Flattener.for(association.klass).new.tap { |f| f.instance_eval(&block) } if block.present? end end |
#add_column(name, sql_type, **options) ⇒ Object
HACK Does this really belong inside of the flattener?
27 28 29 30 |
# File 'lib/has_helpers/flattener/base.rb', line 27 def add_column(name, sql_type, **) sql_column = Flattener::Column.build_sql_column(name, sql_type) model_columns << Flattener::Column.new(sql_column: sql_column, klass: self.class.model_klass, association: self.class.find_column_association(sql_column.name), **) end |
#associations ⇒ Object
13 14 15 |
# File 'lib/has_helpers/flattener/base.rb', line 13 def associations @associations ||= self.class.associations.select { |a| (a.for.to_a & include_for).present? }.map(&:dup) end |
#belongs_to(*args) ⇒ Object
84 85 86 |
# File 'lib/has_helpers/flattener/base.rb', line 84 def belongs_to(*args) singleton_class.belongs_to(*args) end |
#columns ⇒ Object
48 49 50 51 52 53 |
# File 'lib/has_helpers/flattener/base.rb', line 48 def columns @columns ||= model_columns. lazy. select { |c| include_columns.blank? || include_columns.include?(c.name.to_sym) }. reject { |c| exclude_columns.include?(c.name.to_sym) } # FIXME: It seems inconsistent to return string column names (from .columns) yet require symbol names for excluded columns. * end |
#exclude_columns(*column_names) ⇒ Object
65 66 67 68 69 |
# File 'lib/has_helpers/flattener/base.rb', line 65 def exclude_columns(*column_names) (@exclude_columns ||= []).tap do |exclude_columns| exclude_columns.concat column_names if column_names end end |
#flatten(depth = 1) ⇒ Object
80 81 82 |
# File 'lib/has_helpers/flattener/base.rb', line 80 def flatten(depth = 1) Flattener::Resource.new(flattener: self).flatten(depth) end |
#include_columns(*column_names) ⇒ Object
59 60 61 62 63 |
# File 'lib/has_helpers/flattener/base.rb', line 59 def include_columns(*column_names) (@include_columns ||= []).tap do |include_columns| include_columns.concat column_names if column_names end end |
#join(name, **options, &block) ⇒ Object
39 40 41 42 43 44 45 46 |
# File 'lib/has_helpers/flattener/base.rb', line 39 def join(name, **, &block) if add(name, always_include: true, merge_into_parent: true, **, &block) # rubocop:disable Rails/SkipsModelValidations update_column(name.to_s.foreign_key, alias_name: :id) update_column(self.class.model_klass.primary_key, alias_name: :link_id) # rubocop:enable Rails/SkipsModelValidations end end |
#remove(*args) ⇒ Object
32 33 34 35 36 37 |
# File 'lib/has_helpers/flattener/base.rb', line 32 def remove(*args) args.each do |name| association = associations.detect { |a| a.ar_association_name == name } associations.delete(association) if association end end |
#update_column(name, **options) ⇒ Object
71 72 73 74 75 76 77 78 |
# File 'lib/has_helpers/flattener/base.rb', line 71 def update_column(name, **) raise ArgumentError, "nil is not a valid column" if name.nil? begin columns.detect { |column| column.name.to_s == name.to_s }.update() rescue NoMethodError raise "No column `#{name}` on #{self.class.name}" if ::HasUtils::Env.development? end end |