module DutyFree::SuggestTemplate
Public Class Methods
_find_belongs_tos(klass, to_klass, errored_assocs)
click to toggle source
Find belongs_tos for this model to one more more other klasses
# File lib/duty_free/suggest_template.rb, line 180 def self._find_belongs_tos(klass, to_klass, errored_assocs) klass.reflect_on_all_associations.each_with_object([]) do |bt_assoc, s| # .is_a?(ActiveRecord::Reflection::BelongsToReflection) next unless bt_assoc.belongs_to? && !errored_assocs.include?(bt_assoc) begin s << bt_assoc if !bt_assoc.options[:polymorphic] && bt_assoc.klass == to_klass rescue NameError errored_assocs << bt_assoc puts "* In the #{bt_assoc.active_record.name} model \"belongs_to :#{bt_assoc.name}\" could not find a model named #{bt_assoc.class_name}." end end end
_find_requireds(klass)
click to toggle source
# File lib/duty_free/suggest_template.rb, line 226 def self._find_requireds(klass) errored_columns = ::DutyFree.instance_variable_get(:@errored_columns) klass.validators.select do |v| v.is_a?(ActiveRecord::Validations::PresenceValidator) end.each_with_object([]) do |v, s| v.attributes.each do |a| attrib = a.to_s klass_col = [klass, attrib] next if errored_columns.include?(klass_col) if klass.columns.map(&:name).include?(attrib) s << attrib else hm_and_bt_names = klass.reflect_on_all_associations.each_with_object([]) do |assoc, names| names << assoc.name.to_s if [:belongs_to, :has_many, :has_one].include?(assoc.macro) names end unless hm_and_bt_names.include?(attrib) puts "* In the #{klass.name} model \"validates_presence_of :#{attrib}\" should be removed as it does not refer to any existing column." errored_columns << klass_col end end end end end
_suggest_template(hops, do_has_many, this_klass, poison_links = [], path = '')
click to toggle source
# File lib/duty_free/suggest_template.rb, line 35 def self._suggest_template(hops, do_has_many, this_klass, poison_links = [], path = '') errored_assocs = ::DutyFree.instance_variable_get(:@errored_assocs) this_primary_key = Array(this_klass.primary_key) # Find all associations, and track all belongs_tos this_belongs_tos = [] assocs = {} this_klass.reflect_on_all_associations.each do |assoc| # PolymorphicReflection AggregateReflection RuntimeReflection is_belongs_to = assoc.belongs_to? # Figure out if it's belongs_to, has_many, or has_one belongs_to_or_has_many = if is_belongs_to 'belongs_to' elsif (is_habtm = assoc.macro == :has_and_belongs_to_many) 'has_and_belongs_to_many' elsif assoc.macro == :has_many 'has_many' else 'has_one' end # Always process belongs_to, and also process has_one and has_many if do_has_many is chosen. # Skip any HMT or HABTM. (Maybe break out HABTM into a combo HM and BT in the future.) if is_habtm puts "* In the #{this_klass.name} model there's a problem with: \"has_and_belongs_to_many :#{assoc.name}\" because join table \"#{assoc.join_table}\" does not exist. You can create it with a create_join_table migration." unless ActiveRecord::Base.connection.table_exists?(assoc.join_table) # %%% Search for other associative candidates to use instead of this HABTM contraption puts "* In the #{this_klass.name} model there's a problem with: \"has_and_belongs_to_many :#{assoc.name}\" because it includes \"through: #{assoc.options[:through].inspect}\" which is pointless and should be removed." if assoc.options.include?(:through) end if (is_through = assoc.is_a?(ActiveRecord::Reflection::ThroughReflection)) && assoc.options.include?(:as) puts "* In the #{this_klass.name} model there's a problem with: \"has_many :#{assoc.name} through: #{assoc.options[:through].inspect}\" because it also includes \"as: #{assoc.options[:as].inspect}\", so please choose either for this line to be a \"has_many :#{assoc.name} through:\" or to be a polymorphic \"has_many :#{assoc.name} as:\". It can't be both." end next if is_through || is_habtm || (!is_belongs_to && !do_has_many) || errored_assocs.include?(assoc) if is_belongs_to && assoc.options[:polymorphic] # Polymorphic belongs_to? # Load all models # %%% Note that this works in Rails 5.x, but may not work in Rails 6.0 and later, which uses the Zeitwerk loader by default: Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!) # Find all current possible polymorphic relations ActiveRecord::Base.descendants.each do |model| # Skip auto-generated HABTM_DestinationModel models next if model.respond_to?(:table_name_resolver) && model.name.start_with?('HABTM_') && model.table_name_resolver.is_a?( ActiveRecord::Associations::Builder::HasAndBelongsToMany::JoinTableResolver::KnownClass ) # Find applicable polymorphic has_many associations from each real model model.reflect_on_all_associations.each do |poly_assoc| next unless poly_assoc.macro == :has_many && poly_assoc.inverse_of == assoc this_belongs_tos += (fkeys = [poly_assoc.type, poly_assoc.foreign_key]) assocs["#{assoc.name}_#{poly_assoc.active_record.name.underscore}".to_sym] = [[fkeys, assoc.active_record], poly_assoc.active_record] end end else # Is it a polymorphic has_many, which is defined using as: :somethingable ? is_polymorphic_hm = assoc.inverse_of&.options&.fetch(:polymorphic) { nil } begin # Standard has_one, or has_many, and belongs_to uses assoc.klass. # Also polymorphic belongs_to uses assoc.klass. assoc_klass = is_polymorphic_hm ? assoc.inverse_of.active_record : assoc.klass rescue NameError # For models which cannot be found by name end new_assoc = if assoc_klass.nil? puts "* In the #{this_klass.name} model there's a problem with: \"#{belongs_to_or_has_many} :#{assoc.name}\" because there is no \"#{assoc.class_name}\" model." nil # Cause this one to be excluded elsif is_belongs_to this_belongs_tos << (fk = assoc.foreign_key.to_s) [[[fk], assoc.active_record], assoc_klass] else # has_many or has_one inverse_foreign_keys = is_polymorphic_hm ? [assoc.type, assoc.foreign_key] : [assoc.inverse_of&.foreign_key&.to_s] missing_key_columns = inverse_foreign_keys - assoc_klass.columns.map(&:name) if missing_key_columns.empty? puts "* Missing inverse foreign key for #{this_klass.name} #{belongs_to_or_has_many} :#{assoc.name}" if inverse_foreign_keys.first.nil? # puts "Has columns #{inverse_foreign_keys.inspect}" [[inverse_foreign_keys, assoc_klass], assoc_klass] else if inverse_foreign_keys.length > 1 puts "* The #{assoc_klass.name} model is missing #{missing_key_columns.join(' and ')} columns to allow it to support polymorphic inheritance." else print "* In the #{this_klass.name} model there's a problem with: \"#{belongs_to_or_has_many} :#{assoc.name}\"." if (inverses = _find_belongs_tos(assoc_klass, this_klass, errored_assocs)).empty? if inverse_foreign_keys.first.nil? puts " Consider adding \"foreign_key: :#{this_klass.name.underscore}_id\" regarding some column in #{assoc_klass.name} to this #{belongs_to_or_has_many} entry." else puts " (Cannot find foreign key \"#{inverse_foreign_keys.first.inspect}\" in #{assoc_klass.name}.)" end else puts " Consider adding \"#{inverses.map { |x| "inverse_of: :#{x.name}" }.join(' or ')}\" to this entry." end end nil end end if new_assoc.nil? errored_assocs << assoc else assocs[assoc.name] = new_assoc end end end # Include all columns except for the primary key, any foreign keys, and excluded_columns # %%% add EXCLUDED_ALL_COLUMNS || ... excluded_columns = %w[created_at updated_at deleted_at] template = (this_klass.columns.map(&:name) - this_primary_key - this_belongs_tos - excluded_columns) template.map!(&:to_sym) requireds = _find_requireds(this_klass).map { |r| "#{path}#{r}".to_sym } # Now add the foreign keys and any has_manys in the form of references to associated models assocs.each do |k, assoc| # assoc.first describes this foreign key and class, and is used for a "reverse poison" # detection so we don't fold back on ourselves next if poison_links.include?(assoc.first) is_has_many = (assoc.first.last == assoc.last) # puts "#{k} #{hops}" unique, new_requireds = if hops.zero? # For has_one or has_many, exclude with priority the foreign key column(s) we rode in here on priority_excluded_columns = assoc.first.first if is_has_many # puts "Excluded: #{priority_excluded_columns.inspect}" _suggest_unique_column(assoc.last, priority_excluded_columns, "#{path}#{k}_") else new_poison_links = if is_has_many # has_many is simple, just exclude how we got here from the foreign table [assoc.first] else # belongs_to is more involved since there may be multiple foreign keys which point # from the foreign table to this primary one, so exclude all these links. _find_belongs_tos(assoc.first.last, assoc.last, errored_assocs).map do |f_assoc| [[f_assoc.foreign_key.to_s], f_assoc.active_record] end end # puts "New Poison: #{new_poison_links.inspect}" _suggest_template(hops - 1, do_has_many, assoc.last, poison_links + new_poison_links, "#{path}#{k}_") end template << { k => unique } requireds += new_requireds end [template, requireds] end
_suggest_unique_column(klass, priority_excluded_columns, path)
click to toggle source
# File lib/duty_free/suggest_template.rb, line 194 def self._suggest_unique_column(klass, priority_excluded_columns, path) # %%% Try to find out if this klass already has an import template, and if so then # bring in its first unique column set as a suggestion # ... # Not available, so grasping at straws, just search for any available column # %%% add EXCLUDED_UNIQUE_COLUMNS || ... klass_columns = klass.columns # Requireds takes its cues from all attributes having a presence validator requireds = _find_requireds(klass) klass_columns = klass_columns.reject { |col| priority_excluded_columns.include?(col.name) } if priority_excluded_columns excluded_columns = %w[created_at updated_at deleted_at] unique = [( # Find the first text field of a required if one exists klass_columns.find { |col| requireds.include?(col.name) && col.type == :string }&.name || # Find the first text field, now of a non-required, if one exists klass_columns.find { |col| col.type == :string }&.name || # If no string then look for the first non-PK that is also not a foreign key or created_at or updated_at klass_columns.find do |col| requireds.include?(col.name) && col.name != klass.primary_key && !excluded_columns.include?(col.name) end&.name || # And now the same but not a required, the first non-PK that is also not a foreign key or created_at or updated_at klass_columns.find do |col| col.name != klass.primary_key && !excluded_columns.include?(col.name) end&.name || # Finally just accept the PK if nothing else klass.primary_key ).to_sym] [unique, requireds.map { |r| "#{path}#{r}".to_sym }] end
_template_pretty_print(template, indent = 0, child_count = 0, is_hash_in_hash = false)
click to toggle source
Show a “pretty” version of IMPORT_TEMPLATE, to be placed in a model
# File lib/duty_free/suggest_template.rb, line 253 def self._template_pretty_print(template, indent = 0, child_count = 0, is_hash_in_hash = false) unless indent.negative? if indent.zero? print 'IMPORT_TEMPLATE = ' else puts unless is_hash_in_hash end print "#{' ' * indent unless is_hash_in_hash}{" if indent.zero? indent = 2 print "\n#{' ' * indent}" else print ' ' unless is_hash_in_hash end end is_first = true template.each do |k, v| # Skip past this when doing a child count child_count = _template_pretty_print(v, -10_000) if indent >= 0 if is_first is_first = false elsif indent == 2 || (indent >= 0 && child_count > 5) print ",\n#{' ' * indent}" # Comma, newline, and indentation end if indent.negative? child_count += 1 else # Fairly good to troubleshoot child_count things with: "#{k}#{child_count}: " print "#{k}: " end if v.is_a?(Array) print '[' unless indent.negative? v.each_with_index do |item, idx| # This is where most of the commas get printed, so you can do "#{child_count}," to diagnose things print ',' if idx.positive? && indent >= 0 case item when Hash # puts '^' unless child_count < 5 || indent.negative? child_count = _template_pretty_print(item, indent + 2, child_count) when Symbol if indent.negative? child_count += 1 else print ' ' if idx.positive? print item.inspect end end end print ']' unless indent.negative? elsif v.is_a?(Hash) # A hash in a hash child_count = _template_pretty_print(v, indent + 2, child_count, true) elsif v.nil? puts 'nil' unless indent.negative? end end if indent == 2 puts indent = 0 puts '}.freeze' elsif indent >= 0 print "#{' ' unless child_count.zero?}}" end child_count end