lib/poly_belongs_to/core.rb
module PolyBelongsTo
##
# PolyBelongsTo::Core are the core set of methods included on all ActiveModel & ActiveRecord instances.
module Core
extend ActiveSupport::Concern
included do
# @return [Symbol, nil] first belongs_to relation
def self.pbt
reflect_on_all_associations(:belongs_to).first.try(:name)
end
# @return [Array<Symbol>] belongs_to relations
def self.pbts
reflect_on_all_associations(:belongs_to).map(&:name)
end
# @return [Array<Symbol>] has_one relations
def self.has_one_of
reflect_on_all_associations(:has_one).map(&:name)
end
# @return [Array<Symbol>] has_many relations
def self.has_many_of
reflect_on_all_associations(:has_many).map(&:name)
end
# @return [Array<Symbol>] has_many relations
def self.habtm_of
reflect_on_all_associations(:has_and_belongs_to_many).map(&:name)
end
# Boolean reponse of current class being polymorphic
# @return [true, false]
def self.poly?
!!reflect_on_all_associations(:belongs_to).first.try {|i| i.options[:polymorphic] }
end
# Symbol for html form params
# @param allow_as_nested [true, false] Allow parameter name to be nested attribute symbol
# @return [Symbol] The symbol for the form in the view
def self.pbt_params_name(allow_as_nested = true)
if poly? && allow_as_nested
"#{table_name}_attributes".to_sym
else
name.downcase.to_sym
end
end
# The symbol as an id field for the first belongs_to relation
# @return [Symbol, nil] :`belongs_to_object`_id or nil
def self.pbt_id_sym
val = pbt
val ? "#{val}_id".to_sym : nil
end
# The symbol as an sym field for the polymorphic belongs_to relation, or nil
# @return [Symbol, nil] :`belongs_to_object`_sym or nil
def self.pbt_type_sym
poly? ? "#{pbt}_type".to_sym : nil
end
# Returns an unique array of strings of every polymorphic record type
# @return [Array<String>]
def self.pbt_poly_types
return [] unless poly?
uniq.pluck(pbt_type_sym)
end
# Returns strings of the invalid class names stored in polymorphic records
# @return [Array<String>]
def self.pbt_mistypes
pbt_poly_types.select do |i|
begin
!i.constantize.respond_to?(:pluck)
rescue
true
end
end
end
# Returns strings of the valid class names stored in polymorphic records
# @return [Array<String>]
def self.pbt_valid_types
pbt_poly_types.delete_if do |i|
begin
!i.constantize.respond_to?(:pluck)
rescue
true
end
end
end
# Returns records with invalid class names stored in polymorphic records
# @return [Array<Object>, nil] ActiveRecord mistyped objects
def self.pbt_mistyped
return nil unless poly?
where(pbt_type_sym => pbt_mistypes)
end
# Return Array of current Class records that are orphaned from parents
# @return [Array<Object>, nil] ActiveRecord orphan objects
def self.pbt_orphans
return nil unless pbts.present?
poly? ? _pbt_polymorphic_orphans : _pbt_nonpolymorphic_orphans
end
private
# Return Array of current Class polymorphic records that are orphaned from parents
# @return [Array<Object>] ActiveRecord orphan objects
def self._pbt_polymorphic_orphans
accumulative = nil
pbt_valid_types.each do |type|
arel_part = arel_table[pbt_id_sym].not_in(type.constantize.arel_table.project(:id)).and(arel_table[pbt_type_sym].eq(type))
accumulative = accumulative.present? ? accumulative.or(arel_part) : arel_part
end
where(accumulative)
end
# Return Array of current Class nonpolymorphic records that are orphaned from parents
# @return [Array<Object>] ActiveRecord orphan objects
def self._pbt_nonpolymorphic_orphans
where(arel_table[pbt_id_sym].not_in(pbt.to_s.camelize.constantize.arel_table.project(:id)))
end
class << self
private :_pbt_polymorphic_orphans
private :_pbt_nonpolymorphic_orphans
end
end
# @return [Symbol, nil] first belongs_to relation
def pbt
self.class.pbt
end
# @return [Array<Symbol>] belongs_to relations
def pbts
self.class.pbts
end
# Boolean reponse of current class being polymorphic
# @return [true, false]
def poly?
self.class.poly?
end
# Value of parent id. nil if no parent
# @return [Integer, nil]
def pbt_id
val = pbt
val ? send("#{val}_id") : nil
end
# Value of polymorphic relation type. nil if not polymorphic.
# @return [String, nil]
def pbt_type
poly? ? send("#{pbt}_type") : nil
end
# Get the parent relation. Polymorphic relations are prioritized first.
# @return [Object, nil] ActiveRecord object instasnce
def pbt_parent
val = pbt
if val && !pbt_id.nil?
if poly?
"#{pbt_type}".constantize.where(id: pbt_id).first
else
"#{val}".camelize.constantize.where(id: pbt_id).first
end
end
end
# Climb up each parent object in the hierarchy until the top is reached.
# This has a no-repeat safety built in. Polymorphic parents have priority.
# @return [Object, nil] top parent ActiveRecord object instace
def pbt_top_parent
record = self
return nil unless record.pbt_parent
no_repeat = PolyBelongsTo::SingletonSet.new
while !no_repeat.include?(record.pbt_parent) && !record.pbt_parent.nil?
no_repeat.add?(record)
record = record.pbt_parent
end
record
end
# All belongs_to parents as class objects. One if polymorphic.
# @return [Array<Object>] ActiveRecord classes of parent objects.
def pbt_parents
if poly?
Array[pbt_parent].compact
else
self.class.pbts.map do |i|
try{ "#{i}".camelize.constantize.where(id: send("#{i}_id")).first }
end.compact
end
end
# The symbol as an id field for the first belongs_to relation
# @return [Symbol, nil] :`belongs_to_object`_id or nil
def pbt_id_sym
self.class.pbt_id_sym
end
# The symbol as an sym field for the polymorphic belongs_to relation, or nil
# @return [Symbol, nil] :`belongs_to_object`_sym or nil
def pbt_type_sym
self.class.pbt_type_sym
end
# Symbol for html form params
# @param allow_as_nested [true, false] Allow parameter name to be nested attribute symbol
# @return [Symbol] The symbol for the form in the view
def pbt_params_name(allow_as_nested = true)
self.class.pbt_params_name(allow_as_nested)
end
# Return true or false on whether the record is orphaned
# @return [Boolean
def orphan?
pbts.present? && !pbt_parent.present?
end
end
end