Module: Brick

Defined in:
lib/brick/util.rb,
lib/brick.rb,
lib/brick/config.rb,
lib/brick/join_array.rb,
lib/brick/version_number.rb,
lib/brick/serializers/json.rb,
lib/brick/serializers/yaml.rb,
lib/brick/frameworks/cucumber.rb,
lib/brick/frameworks/rails/engine.rb,
lib/generators/brick/seeds_generator.rb,
lib/generators/brick/models_generator.rb,
lib/generators/brick/install_generator.rb,
lib/generators/brick/migration_builder.rb,
lib/generators/brick/migrations_generator.rb,
lib/brick/extensions.rb

Overview

:nodoc:

Defined Under Namespace

Modules: Cucumber, Extensions, MigrationBuilder, Rails, RouteMapper, RouteSet, Serializers, Util, VERSION Classes: Config, InstallGenerator, JoinArray, JoinHash, MigrationsGenerator, ModelsGenerator, SeedsGenerator

Constant Summary collapse

ALL_API_ACTIONS =
[:index, :show, :create, :update, :destroy]
CURRENCY_SYMBOLS =
'$£¢₵€₠ƒ¥₿₩₪₹₫₴₱₲₳₸₺₼₽៛₡₢₣₤₥₦₧₨₭₮₯₰₶₷₻₾'
ADD_CONST_MISSING =
lambda do
  return if @_brick_const_missing_done

  @_brick_const_missing_done = true
  alias _brick_const_missing const_missing
  def const_missing(*args)
    requested = args.first.to_s
    is_controller = requested.end_with?('Controller')
    # self.name is nil when a model name is requested in an .erb file
    if self.name && ::Brick.config.path_prefix
      split_self_name.shift if (split_self_name = self.name.split('::')).first.blank?
      # Asking for the prefix module?
      camelize_prefix = ::Brick.config.path_prefix.camelize
      if self == Object && requested == camelize_prefix
        Object.const_set(args.first, (built_module = Module.new))
        puts "module #{camelize_prefix}; end\n"
        return built_module
      elsif module_parent == Object && self.name == camelize_prefix ||
            module_parent.name == camelize_prefix && module_parent.module_parent == Object
        split_self_name.shift # Remove the identified path prefix from the split name
        is_brick_prefix = true
        if is_controller
          brick_root = split_self_name.empty? ? self : camelize_prefix.constantize
        end
      end
    end
    base_module = if self < ActiveRecord::Migration || !self.name
                    brick_root || Object
                  elsif split_self_name&.length&.> 1 # Classic mode
                    begin
                      base = self
                      unless (base_goal = requested.split('::')[0..-2].join('::')).empty?
                        base = base.parent while base.name != base_goal && base != Object
                      end
                      return base._brick_const_missing(*args)

                    rescue NameError # %%% Avoid the error "____ cannot be autoloaded from an anonymous class or module"
                      return self.const_get(args.first) if self.const_defined?(args.first)

                      # unless self == (prnt = (respond_to?(:parent) ? parent : module_parent))
                      unless self == Object
                        begin
                          return Object._brick_const_missing(*args)

                        rescue NameError
                          return Object.const_get(args.first) if Object.const_defined?(args.first)

                        end
                      end
                    end
                    Object
                  else
                    sti_base = (::Brick.config.sti_namespace_prefixes&.fetch("::#{name}::#{requested}", nil) ||
                                ::Brick.config.sti_namespace_prefixes&.fetch("::#{name}::", nil))&.constantize
                    self
                  end
    # puts "#{self.name} - #{args.first}"
    # Unless it's a Brick prefix looking for a TNP that should create a module ...
    relations = ::Brick.relations
    unless (is_tnp_module = (is_brick_prefix && !is_controller && ::Brick.config.table_name_prefixes.values.include?(requested)))
      # ... first look around for an existing module or class.
      desired_classname = (self == Object || !name) ? requested : "#{name}::#{requested}"
      if ((is_defined = self.const_defined?(args.first)) && (possible = self.const_get(args.first)) &&
          # Reset `possible` if it's a controller request that's not a perfect match
          # Was:  (possible = nil)  but changed to #local_variable_set in order to suppress the "= should be ==" warning
          (possible&.name == desired_classname || (is_controller && binding.local_variable_set(:possible, nil)))) ||
         # Try to require the respective Ruby file
         ((filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore) ||
                      (self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = requested).underscore))
          ) && (require_dependency(filename) || true) &&
          ((possible = self.const_get(args.first)) && possible.name == desired_classname)
         ) ||
         # If any class has turned up so far (and we're not in the middle of eager loading)
         # then return what we've found.
         (is_defined && !::Brick.is_eager_loading) # Used to also have:   && possible != self
        if ((!brick_root && (filename || possible.instance_of?(Class))) ||
            (possible.instance_of?(Module) && possible&.module_parent == self) ||
            (possible.instance_of?(Class) && possible == self)) && # Are we simply searching for ourselves?
           # Skip when what we found as `possible` is not related to the base class of an STI model
           (!sti_base || possible.is_a?(sti_base))
          # if possible.is_a?(ActiveRecord::Base) && !possible.abstract_class? && (pk = possible.primary_key) &&
          #    !(relation = relations.fetch(possible.table_name, nil))&.fetch(:pks, nil)
          #   binding.pry
          #   x = 5
          # end
          return possible
        end
      end
    end
    class_name = ::Brick.namify(requested)
    #        CONTROLLER
    result = if ::Brick.enable_controllers? &&
                is_controller && (plural_class_name = class_name[0..-11]).length.positive?
               # Otherwise now it's up to us to fill in the gaps
               controller_class_name = +''
               full_class_name = +''
               unless self == Object
                 controller_class_name << ((split_self_name&.first && split_self_name.join('::')) || self.name)
                 full_class_name << "::#{controller_class_name}"
                 controller_class_name << '::'
               end
               # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
               # Vabc instead of VABC)
               singular_class_name = ::Brick.namify(plural_class_name, :underscore).singularize.camelize
               full_class_name << "::#{singular_class_name}"
               skip_controller = nil
               if plural_class_name == 'BrickOpenapi' ||
                  (
                    (::Brick.config.add_status || ::Brick.config.add_orphans) &&
                    plural_class_name == 'BrickGem'
                  ) ||
                 begin
                   model = self.const_get(full_class_name)

                   # In the very rare case that we've picked up a MODULE which has the same name as what would be the
                   # resource's MODEL name, just build out an appropriate auto-model on-the-fly. (RailsDevs code has this in PayCustomer.)
                   # %%% We don't yet display the code for this new model
                   if model && !model.is_a?(Class)
                     model, _code = Object.send(:build_model, relations, model.module_parent, model.module_parent.name, singular_class_name)
                   end
                 rescue NameError # If the const_get for the model has failed...
                   skip_controller = true
                   # ... then just fall through and allow it to fail when trying to loading the ____Controller class normally.
                 end
               end
               unless skip_controller
                 Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
               end

             # MODULE
             elsif (::Brick.enable_models? || ::Brick.enable_controllers?) && # Schema match?
                   # %%% This works for Person::Person -- but also limits us to not being able to allow more than one level of namespacing
                   (base_module == Object || (camelize_prefix && base_module == Object.const_get(camelize_prefix))) &&
                   (schema_name = [(singular_table_name = class_name.underscore),
                                   (table_name = singular_table_name.pluralize),
                                   ::Brick.is_oracle ? class_name.upcase : class_name,
                                   (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas&.include?(s) }&.camelize ||
                                  (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name) ||
                                  (::Brick.config.table_name_prefixes&.values&.include?(class_name) && class_name))
               return self.const_get(schema_name) if self.const_defined?(schema_name) &&
                                                     (!is_tnp_module || self.const_get(schema_name).is_a?(Class))

               # Build out a module for the schema if it's namespaced
               # schema_name = schema_name.camelize
               base_module.const_set(schema_name.to_sym, (built_module = Module.new))
               [built_module, "module #{schema_name}; end\n"]
               # %%% Perhaps an option to use the first module just as schema, and additional modules as namespace with a table name prefix applied

             # MODULE (overrides from "treat_as_module")
             elsif (::Brick.enable_models? || ::Brick.enable_controllers?) &&
                   (possible_module = (base_module == Object ? '' : "#{base_module.name}::") + class_name) &&
                   ::Brick.config.treat_as_module.include?(possible_module)
               base_module.const_set(class_name.to_sym, (built_module = Module.new))
               [built_module, "module #{possible_module}; end\n"]

             # AVO Resource
             elsif base_module == Object && Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace) && requested.end_with?('Resource') &&
                   # Expect that anything called MotorResource or SpinaResource could be from those administrative gems
                   requested.length > 8 && ['MotorResource', 'SpinaResource'].exclude?(requested)
               if (model = Object.const_get(requested[0..-9])) && model < ActiveRecord::Base
                 require 'generators/avo/resource_generator'
                 field_generator = Generators::Avo::ResourceGenerator.new([''])
                 field_generator.instance_variable_set(:@model, model)
                 fields = field_generator.send(:generate_fields)&.split("\n")
                                         &.each_with_object([]) do |f, s|
                                           if (f = f.strip).start_with?('field ')
                                             f = f[6..-1].split(',')
                                             s << [f.first[1..-1].to_sym, [f[1][1..-1].split(': :').map(&:to_sym)].to_h]
                                           end
                                         end || []
                 built_resource = Class.new(Avo::BaseResource) do |new_resource_class|
                   self.model_class = model
                   self.title = :brick_descrip
                   self.includes = []
                   if (!model.is_view? && mod_pk = model.primary_key)
                     field((mod_pk.is_a?(Array) ? mod_pk.first : mod_pk).to_sym, { as: :id })
                   end
                   # Create a call such as:  field :name, as: :text
                   fields.each do |f|
                     # Add proper types if this is a polymorphic belongs_to
                     if f.last == { as: :belongs_to } &&
                        (fk = ::Brick.relations[model.table_name][:fks].find { |k, v| v[:assoc_name] == f.first.to_s }) &&
                        fk.last.fetch(:polymorphic, nil)
                       poly_types = fk.last.fetch(:inverse_table, nil)&.each_with_object([]) do |poly_table, s|
                         s << Object.const_get(::Brick.relations[poly_table][:class_name])
                       end
                       if poly_types.present?
                         f.last[:polymorphic_as] = f.first
                         f.last[:types] = poly_types
                       end
                     end
                     self.send(:field, *f)
                   end
                 end
                 Object.const_set(requested.to_sym, built_resource)
                 [built_resource, nil]
               end

             # MODEL
             elsif ::Brick.enable_models?
               # Custom inheritable Brick base model?
               class_name = (inheritable_name = class_name)[5..-1] if class_name.start_with?('Brick')
               Object.send(:build_model, relations, base_module, name, class_name, inheritable_name)
             end
    if result
      built_class, code = result
      puts "\n#{code}\n"
      built_class
    elsif !schema_name && ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}")
#         module_prefixes = type_name.split('::')
#         path = base_module.name.split('::')[0..-2] + []
#         module_prefixes.unshift('') unless module_prefixes.first.blank?
#         candidate_file = ::Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
      base_module._brick_const_missing(*args)
    # elsif base_module != Object
    #   module_parent.const_missing(*args)
    elsif Rails.respond_to?(:autoloaders) && # After finding nothing else, if Zeitwerk is enabled ...
          (Rails::Autoloaders.respond_to?(:zeitwerk_enabled?) ? Rails::Autoloaders.zeitwerk_enabled? : true)
      self._brick_const_missing(*args) # ... rely solely on Zeitwerk.
    else # Classic mode
      unless (found = base_module._brick_const_missing(*args))
        puts "MISSING! #{base_module.name} #{args.inspect} #{table_name}"
      end
      found
    end
  end

  # Support Rails < 6.0 which adds #parent instead of #module_parent
  unless respond_to?(:module_parent)
    def module_parent # Weirdly for Grape::API does NOT come in with the proper class, but some anonymous Class thing
      parent
    end
  end
end

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.auto_modelsObject

Returns the value of attribute auto_models.



109
110
111
# File 'lib/brick.rb', line 109

def auto_models
  @auto_models
end

.db_schemasObject

Returns the value of attribute db_schemas.



109
110
111
# File 'lib/brick.rb', line 109

def db_schemas
  @db_schemas
end

.default_schemaObject

Returns the value of attribute default_schema.



109
110
111
# File 'lib/brick.rb', line 109

def default_schema
  @default_schema
end

.established_drfObject

Returns the value of attribute established_drf.



109
110
111
# File 'lib/brick.rb', line 109

def established_drf
  @established_drf
end

.initializer_loadedObject

Returns the value of attribute initializer_loaded.



109
110
111
# File 'lib/brick.rb', line 109

def initializer_loaded
  @initializer_loaded
end

.is_eager_loadingObject

Returns the value of attribute is_eager_loading.



109
110
111
# File 'lib/brick.rb', line 109

def is_eager_loading
  @is_eager_loading
end

.is_oracleObject

Returns the value of attribute is_oracle.



109
110
111
# File 'lib/brick.rb', line 109

def is_oracle
  @is_oracle
end

.routes_doneObject

Returns the value of attribute routes_done.



109
110
111
# File 'lib/brick.rb', line 109

def routes_done
  @routes_done
end

.test_schemaObject

Returns the value of attribute test_schema.



109
110
111
# File 'lib/brick.rb', line 109

def test_schema
  @test_schema
end

Class Method Details

._add_bt_and_hm(fk, relations, polymorphic_class = nil, is_optional = false) ⇒ Object



3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
# File 'lib/brick/extensions.rb', line 3065

def _add_bt_and_hm(fk, relations, polymorphic_class = nil, is_optional = false)
  bt_assoc_name = ::Brick.namify(fk[2].dup, :downcase)
  unless polymorphic_class
    bt_assoc_name = if bt_assoc_name.underscore.end_with?('_id')
                      bt_assoc_name[-3] == '_' ? bt_assoc_name[0..-4] : bt_assoc_name[0..-3]
                    elsif bt_assoc_name.downcase.end_with?('id') && bt_assoc_name.exclude?('_')
                      bt_assoc_name[0..-3] # Make the bold assumption that we can just peel off any final ID part
                    else
                      "#{bt_assoc_name}_bt"
                    end
  end
  bt_assoc_name = "#{bt_assoc_name}_" if bt_assoc_name == 'attribute'

  # %%% Temporary schema patch
  for_tbl = fk[1]
  fk_namified = ::Brick.namify(fk[1])
  fk[0] = ::Brick.apartment_default_tenant if ::Brick.is_apartment_excluded_table(fk_namified)
  fk[1] = "#{fk[0]}.#{fk[1]}" if fk[0] # && fk[0] != ::Brick.default_schema
  bts = (relation = relations.fetch(fk[1], nil))&.fetch(:fks) { relation[:fks] = {} }

  # %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
  # Maybe it's already gotten this info because we got as far as to say there was a unique class
  primary_table = if (is_class = fk[4].is_a?(Hash) && fk[4].key?(:class))
                    pri_tbl = (primary_class = fk[4][:class].constantize).table_name
                    if (pri_tbl_parts = pri_tbl.split('.')).length > 1
                      fk[3] = pri_tbl_parts.first
                    end
                  else
                    is_schema = if ::Brick.config.schema_behavior[:multitenant]
                                  # If Apartment gem lists the primary table as being associated with a non-tenanted model
                                  # then use 'public' schema for the primary table
                                  if ::Brick.is_apartment_excluded_table(fk[4])
                                    fk[3] = ::Brick.apartment_default_tenant
                                    true
                                  end
                                else
                                  fk[3] && fk[3] != ::Brick.default_schema && fk[3] != 'public'
                                end
                    pri_tbl = fk[4]
                    is_schema ? "#{fk[3]}.#{pri_tbl}" : pri_tbl
                  end
  hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class

  unless (cnstr_name = fk[5])
    # For any appended references (those that come from config), arrive upon a definitely unique constraint name
    pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
    pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl&.singularize != bt_assoc_name
    cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
    missing = []
    missing << fk[1] unless relations.key?(fk[1])
    missing << primary_table unless is_class || relations.key?(primary_table)
    unless missing.empty?
      tables = relations.reject { |_k, v| v.is_a?(Hash) && v.fetch(:isView, nil) }.keys
                        .select { |table_name| table_name.is_a?(String) }.sort
      puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
      return
    end
    unless (cols = relations[fk[1]][:cols]).key?(fk[2]) || (polymorphic_class && cols.key?("#{fk[2]}_id") && cols.key?("#{fk[2]}_type"))
      columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
      puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[2]}. (Columns present in #{fk[1]} are #{columns.join(', ')}.)"
      return
    end
    if (redundant = bts.find { |_k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[1] && v[:fk] == fk[2] && v[:inverse_table] == primary_table })
      if is_class && !redundant.last.key?(:class)
        redundant.last[:primary_class] = primary_class # Round out this BT so it can find the proper :source for a HMT association that references an STI subclass
      else
        puts "Brick: Additional reference #{fk.inspect} is redundant and can be removed.  (Already established by #{redundant.first}.)"
      end
      return
    end
  end
  return unless bts # Rails 5.0 and older can have bts end up being nil

  if (assoc_bt = bts[cnstr_name])
    if polymorphic_class
      # Assuming same fk (don't yet support composite keys for polymorphics)
      assoc_bt[:inverse_table] << fk[4]
      assoc_bt[:polymorphic] << polymorphic_class
    else # Expect we could have a composite key going
      if assoc_bt[:fk].is_a?(String)
        assoc_bt[:fk] = [assoc_bt[:fk], fk[2]] unless fk[2] == assoc_bt[:fk]
      elsif assoc_bt[:fk].exclude?(fk[2])
        assoc_bt[:fk] << fk[2]
      end
      assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[2]}"
    end
  else
    inverse_table = [primary_table] if polymorphic_class
    assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[2], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
    assoc_bt[:optional] = true if is_optional
    assoc_bt[:polymorphic] = [polymorphic_class] if polymorphic_class
  end
  if is_class
    # For use in finding the proper :source for a HMT association that references an STI subclass
    assoc_bt[:primary_class] = primary_class
    # For use in finding the proper :inverse_of for a BT association that references an STI subclass
    # assoc_bt[:inverse_of] = primary_class.reflect_on_all_associations.find { |a| a.foreign_key == bt[1] }
  end

  return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[1] == exclusion[0] && fk[2] == exclusion[1] && primary_table == exclusion[2] } || hms.nil?

  if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
    if assoc_hm[:fk].is_a?(String)
      assoc_hm[:fk] = [assoc_hm[:fk], fk[2]] unless fk[2] == assoc_hm[:fk]
    elsif assoc_hm[:fk].exclude?(fk[2])
      assoc_hm[:fk] << fk[2]
    end
    assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
  else
    inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment') && fk[0] == ::Brick.apartment_default_tenant
                for_tbl
              else
                fk[1]
              end
    assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[2], assoc_name: fk_namified.pluralize, alternate_name: bt_assoc_name,
                                      inverse_table: inv_tbl, inverse: assoc_bt }
    assoc_hm[:polymorphic] = true if polymorphic_class
    hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
    this_hm_count = hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
  end
  assoc_bt[:inverse] = assoc_hm
end

._class_pk(dotted_name, multitenant) ⇒ Object



3339
3340
3341
# File 'lib/brick/extensions.rb', line 3339

def _class_pk(dotted_name, multitenant)
  Object.const_get((multitenant ? [dotted_name.split('.').last] : dotted_name.split('.')).map { |nm| "::#{nm.singularize.camelize}" }.join).primary_key
end

._find_csv_separator(stream, likely_separator) ⇒ Object



809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
# File 'lib/brick.rb', line 809

def _find_csv_separator(stream, likely_separator)
  line = nil
  last_char = nil
  in_quotes = false
  ret = +''
  while true
    (line = stream.readline).each_char do |ch|
      likely_separator[ch] += 2 if !in_quotes && last_char == '"' && ch != "\n"
      if ch == '"'
        in_quotes = !in_quotes
        likely_separator[last_char] += 2 if in_quotes && last_char != "\n"
      end
      last_char = ch
    end
    ret << line
    break if !in_quotes || stream.eof?
  end
  likely_separator[line[0]] += 1
  likely_separator[line[-2]] += 1
  ret
end

.additional_references=(ars) ⇒ Object

Additional table associations to use (Think of these as virtual foreign keys perhaps)



466
467
468
469
470
471
472
473
# File 'lib/brick.rb', line 466

def additional_references=(ars)
  if ars
    ars = ars.call if ars.is_a?(Proc)
    ars = ars.to_a unless ars.is_a?(Array)
    ars = [ars] unless ars.empty? || ars.first.is_a?(Array)
    Brick.config.additional_references = ars
  end
end

.always_load_fields=(field_set) ⇒ Object



585
586
587
# File 'lib/brick.rb', line 585

def always_load_fields=(field_set)
  Brick.config.always_load_fields = field_set
end

.apartment_default_tenantObject



174
175
176
# File 'lib/brick.rb', line 174

def apartment_default_tenant
  Apartment.default_tenant || 'public'
end

.apartment_multitenantObject



167
168
169
170
171
172
# File 'lib/brick.rb', line 167

def apartment_multitenant
  if @apartment_multitenant.nil?
    @apartment_multitenant = ::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment')
  end
  @apartment_multitenant
end

.api_filterObject



419
420
421
# File 'lib/brick.rb', line 419

def api_filter
  Brick.config.api_filter
end

.api_filter=(proc) ⇒ Object



414
415
416
# File 'lib/brick.rb', line 414

def api_filter=(proc)
  Brick.config.api_filter = proc
end

.api_root=(path) ⇒ Object



399
400
401
# File 'lib/brick.rb', line 399

def api_root=(path)
  Brick.config.api_roots = [path]
end

.api_rootsObject



409
410
411
# File 'lib/brick.rb', line 409

def api_roots
  Brick.config.api_roots
end

.api_roots=(paths) ⇒ Object



404
405
406
# File 'lib/brick.rb', line 404

def api_roots=(paths)
  Brick.config.api_roots = paths
end

.apply_double_underscore_patchObject



771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
# File 'lib/brick.rb', line 771

def apply_double_underscore_patch
  unless @double_underscore_applied
    # Same as normal #camelize and #underscore, just that double-underscores turn into a single underscore
    ActiveSupport::Inflector.class_eval do
      def camelize(term, uppercase_first_letter = true)
        strings = term.to_s.split('__').map do |string|
          # String#camelize takes a symbol (:upper or :lower), so here we also support :lower to keep the methods consistent.
          if !uppercase_first_letter || uppercase_first_letter == :lower
            string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase! || match }
          else
            string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize! || match }
          end
          string.gsub!(/(?:_|(\/))([a-z\d]*)/i) do
            word = $2
            substituted = inflections.acronyms[word] || word.capitalize! || word
            $1 ? "::#{substituted}" : substituted
          end
          string
        end
        strings.join('_')
      end

      def underscore(camel_cased_word)
        return camel_cased_word.to_s unless /[A-Z-]|::/.match?(camel_cased_word)
        regex = inflections.respond_to?(:acronyms_underscore_regex) ? inflections.acronyms_underscore_regex : inflections.acronym_regex
        camel_cased_word.to_s.gsub('::', '/').split('_').map do |word|
          word.gsub!(regex) { "#{$1 && '_' }#{$2.downcase}" }
          word.gsub!(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << '_' }
          word.tr!('-', '_')
          word.downcase!
          word
        end.join('__')
      end
    end
    @double_underscore_applied = true
  end
end

.ar_baseObject



3188
3189
3190
# File 'lib/brick/extensions.rb', line 3188

def ar_base
  @ar_base ||= Object.const_defined?(:ApplicationRecord) ? ApplicationRecord : Class.new(ActiveRecord::Base)
end

.config {|@config| ... } ⇒ Object Also known as: configure

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns Brick’s global configuration object, a singleton. These settings affect all threads.

Yields:



676
677
678
679
680
# File 'lib/brick.rb', line 676

def config
  @config ||= Brick::Config.instance
  yield @config if block_given?
  @config
end

.controllers_inherit_from=(value) ⇒ Object



439
440
441
# File 'lib/brick.rb', line 439

def controllers_inherit_from=(value)
  Brick.config.controllers_inherit_from = value
end

.ctrl_to_klass(ctrl_path, res_names = {}) ⇒ Object

Attempt to determine an ActiveRecord::Base class and additional STI information when given a controller’s path



730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
# File 'lib/brick.rb', line 730

def ctrl_to_klass(ctrl_path, res_names = {})
  klass = nil
  sti_type = nil

  if res_names.empty?
    ::Brick.relations.each_with_object({}) do |v, s|
      next if v.first.is_a?(Symbol)

      v_parts = v.first.split('.')
      v_parts.shift if v_parts.first == 'public'
      res_names[v_parts.join('.')] = v.first
    end
  end

  c_path_parts = ctrl_path.split('/')
  found = nil
  while c_path_parts.present?
    possible_c_path = c_path_parts.join('.')
    possible_c_path_singular = c_path_parts[0..-2] + [c_path_parts.last.singularize]
    possible_sti = possible_c_path_singular.join('/').camelize
    break if (
               res_name = res_names[found = possible_c_path] ||
                          ((klass = Brick.config.sti_namespace_prefixes.key?("::#{possible_sti}") && possible_sti.constantize) &&
                           (sti_type = possible_sti)) ||
                          # %%% Used to have the more flexible:  (DidYouMean::SpellChecker.new(dictionary: res_names.keys).correct(possible_c_path)).first
                          res_names[found = possible_c_path] || res_names[found = possible_c_path_singular.join('.')] ||
                          ((::Brick.config.table_name_prefixes.key?(tn_prefix = c_path_parts.first) ||
                            ::Brick.config.table_name_prefixes.key?(tn_prefix = "#{c_path_parts.first}_")) &&
                           res_names[found = tn_prefix + c_path_parts.last]
                          )
             ) &&
             (
               klass ||
               ((rel = ::Brick.relations.fetch(res_name, nil)) &&
               (klass ||= rel[:class_name]&.constantize))
             )
    c_path_parts.shift
  end
  [klass, sti_type, found]
end

.custom_columns=(cust_cols) ⇒ Object

Custom columns to add to a table, minimally defined with a name and DSL string.



477
478
479
480
481
482
# File 'lib/brick.rb', line 477

def custom_columns=(cust_cols)
  if cust_cols
    cust_cols = cust_cols.call if cust_cols.is_a?(Proc)
    Brick.config.custom_columns = cust_cols
  end
end

.default_route_fallback=(resource_name) ⇒ Object



577
578
579
# File 'lib/brick.rb', line 577

def default_route_fallback=(resource_name)
  Brick.config.default_route_fallback = resource_name
end

.display_classes(prefix, rels, max_length) ⇒ Object



721
722
723
724
725
726
727
# File 'lib/brick.rb', line 721

def display_classes(prefix, rels, max_length)
  rels.sort.each do |rel|
    ::Brick.auto_models << rel.first
    puts "#{rel.first}#{' ' * (max_length - rel.first.length)}  /#{prefix}#{"#{rel[1]}/" if rel[1]}#{rel.last}"
  end
  puts "\n"
end

.eager_load_classes(do_ar_abstract_bases = false) ⇒ Object



687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
# File 'lib/brick.rb', line 687

def eager_load_classes(do_ar_abstract_bases = false)
  ::Brick.is_eager_loading = true
  if ::ActiveSupport.version < ::Gem::Version.new('6') ||
     ::Rails.configuration.instance_variable_get(:@autoloader) == :classic
    if ::ActiveSupport.version < ::Gem::Version.new('4')
      ::Rails.application.eager_load!
    else
      ::Rails.configuration.eager_load_namespaces.select { |ns| ns < ::Rails::Application }.each(&:eager_load!)
    end
  else
    # Same as:  Zeitwerk::Loader.eager_load_all -- plus retry when something skips a beat
    Zeitwerk::Registry.loaders.each { |loader| load_with_retry(loader) }
  end
  abstract_ar_bases = if do_ar_abstract_bases
                        ActiveRecord::Base.descendants.select { |ar| ar.abstract_class? }.map(&:name)
                      end
  ::Brick.is_eager_loading = false
  abstract_ar_bases
end

.enable_apiObject



394
395
396
# File 'lib/brick.rb', line 394

def enable_api
  Brick.config.enable_api
end

.enable_api=(path) ⇒ Object



389
390
391
# File 'lib/brick.rb', line 389

def enable_api=(path)
  Brick.config.enable_api = path
end

.enable_controllers=(value) ⇒ Object

Switches Brick auto-controllers on or off, for all threads



351
352
353
# File 'lib/brick.rb', line 351

def enable_controllers=(value)
  Brick.config.enable_controllers = value
end

.enable_controllers?Boolean

Returns ‘true` if Brick controllers are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


358
359
360
# File 'lib/brick.rb', line 358

def enable_controllers?
  !!Brick.config.enable_controllers
end

.enable_models=(value) ⇒ Object

Switches Brick auto-models on or off, for all threads



338
339
340
# File 'lib/brick.rb', line 338

def enable_models=(value)
  Brick.config.enable_models = value
end

.enable_models?Boolean

Returns ‘true` if Brick models are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


345
346
347
# File 'lib/brick.rb', line 345

def enable_models?
  !!Brick.config.enable_models
end

.enable_routes=(value) ⇒ Object

Switches Brick auto-routes on or off, for all threads



377
378
379
# File 'lib/brick.rb', line 377

def enable_routes=(value)
  Brick.config.enable_routes = value
end

.enable_routes?Boolean

Returns ‘true` if Brick routes are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


384
385
386
# File 'lib/brick.rb', line 384

def enable_routes?
  !!Brick.config.enable_routes
end

.enable_views=(value) ⇒ Object

Switches Brick auto-views on or off, for all threads



364
365
366
# File 'lib/brick.rb', line 364

def enable_views=(value)
  Brick.config.enable_views = value
end

.enable_views?Boolean

Returns ‘true` if Brick views are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


371
372
373
# File 'lib/brick.rb', line 371

def enable_views?
  !!Brick.config.enable_views
end

.ensure_unique(name, *sources) ⇒ Object



3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
# File 'lib/brick/extensions.rb', line 3246

def ensure_unique(name, *sources)
  base = name
  if (added_num = name.slice!(/_(\d+)$/))
    added_num = added_num[1..-1].to_i
  else
    added_num = 1
  end
  while (
    name = "#{base}_#{added_num += 1}"
    sources.each_with_object(nil) do |v, s|
      s || case v
           when Hash
             v.key?(name)
           when Array
             v.include?(name)
           end
    end
  )
  end
  name
end

.exclude_column(table, col) ⇒ Object



312
313
314
315
# File 'lib/brick.rb', line 312

def exclude_column(table, col)
  puts "Excluding #{table}.#{col}"
  true
end

.exclude_hms=(skips) ⇒ Object

Skip creating a has_many association for these (Uses the same exact three-part format as would define an additional_reference)



492
493
494
495
496
497
498
499
# File 'lib/brick.rb', line 492

def exclude_hms=(skips)
  if skips
    skips = skips.call if skips.is_a?(Proc)
    skips = skips.to_a unless skips.is_a?(Array)
    skips = [skips] unless skips.empty? || skips.first.is_a?(Array)
    Brick.config.exclude_hms = skips
  end
end

.exclude_tables=(value) ⇒ Object



429
430
431
# File 'lib/brick.rb', line 429

def exclude_tables=(value)
  Brick.config.exclude_tables = value
end

.existing_stisObject



105
106
107
# File 'lib/brick.rb', line 105

def existing_stis
  @existing_stis ||= Brick.config.sti_namespace_prefixes.each_with_object({}) { |snp, s| s[snp.first[2..-1]] = snp.last unless snp.first.end_with?('::') }
end

.find_col_renaming(api_ver_path, relation_name) ⇒ Object



3332
3333
3334
3335
3336
3337
# File 'lib/brick/extensions.rb', line 3332

def find_col_renaming(api_ver_path, relation_name)
  ::Brick.config.api_column_renaming&.fetch(
    api_ver_path,
    ::Brick.config.api_column_renaming&.fetch(relation_name, nil)
  )
end

.find_orphans(multi_schema) ⇒ Object

Locate orphaned records



3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
# File 'lib/brick/extensions.rb', line 3269

def find_orphans(multi_schema)
  is_default_schema = multi_schema&.==(::Brick.apartment_default_tenant)
  relations.each_with_object([]) do |v, s|
    frn_tbl = v.first
    next if frn_tbl.is_a?(Symbol) || # Skip internal metadata entries
            (relation = v.last).key?(:isView) || config.exclude_tables.include?(frn_tbl) ||
            !(for_pk = (relation[:pkey].values.first&.first))

    is_default_frn_schema = !is_default_schema && multi_schema &&
                            ((frn_parts = frn_tbl.split('.')).length > 1 && frn_parts.first)&.==(::Brick.apartment_default_tenant)
    relation[:fks].select { |_k, assoc| assoc[:is_bt] }.each do |_k, bt|
      begin
        if bt.key?(:polymorphic)
          pri_pk = for_pk
          pri_tables = Brick.config.polymorphics["#{frn_tbl}.#{bt[:fk]}"]
                            .each_with_object(Hash.new { |h, k| h[k] = [] }) do |pri_class, s|
            s[Object.const_get(pri_class).table_name] << pri_class
          end
          fk_id_col = "#{bt[:fk]}_id"
          fk_type_col = "#{bt[:fk]}_type"
          selects = []
          pri_tables.each do |pri_tbl, pri_types|
            # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
            # are both in the "public" schema
            next if is_default_frn_schema &&
                    ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(::Brick.apartment_default_tenant)

            selects << "SELECT '#{pri_tbl}' AS pri_tbl, frn.#{fk_type_col} AS pri_type, frn.#{fk_id_col} AS pri_id, frn.#{for_pk} AS frn_id
            FROM #{frn_tbl} AS frn
              LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{fk_id_col}
            WHERE frn.#{fk_type_col} IN (#{
              pri_types.map { |pri_type| "'#{pri_type}'" }.join(', ')
            }) AND frn.#{bt[:fk]}_id IS NOT NULL AND pri.#{pri_pk} IS NULL\n"
          end
          ActiveRecord::Base.execute_sql(selects.join("UNION ALL\n")).each do |o|
            entry = [frn_tbl, o['frn_id'], o['pri_type'], o['pri_id'], fk_id_col]
            entry << o['pri_tbl'] if (pri_class = Object.const_get(o['pri_type'])) != pri_class.base_class
            s << entry
          end
        else
          # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
          # are both in the "public" schema
          pri_tbl = bt.key?(:inverse_table) && bt[:inverse_table]
          next if is_default_frn_schema &&
                  ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(::Brick.apartment_default_tenant)

          pri_pk = relations[pri_tbl].fetch(:pkey, nil)&.values&.first&.first ||
                   _class_pk(pri_tbl, multi_schema)
          ActiveRecord::Base.execute_sql(
            "SELECT frn.#{bt[:fk]} AS pri_id, frn.#{for_pk} AS frn_id
            FROM #{frn_tbl} AS frn
              LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{bt[:fk]}
            WHERE frn.#{bt[:fk]} IS NOT NULL AND pri.#{pri_pk} IS NULL
            ORDER BY 1, 2"
          ).each { |o| s << [frn_tbl, o['frn_id'], pri_tbl, o['pri_id'], bt[:fk]] }
        end
      rescue StandardError => err
        puts "Strange -- #{err.inspect}"
      end
    end
  end
end

.gem_versionObject

Returns Brick’s ‘::Gem::Version`, convenient for comparisons. This is recommended over `::Brick::VERSION::STRING`.



657
658
659
# File 'lib/brick.rb', line 657

def gem_version
  ::Gem::Version.new(VERSION::STRING)
end

.get_bts_and_hms(model, recalculate = nil) ⇒ Object



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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/brick.rb', line 200

def get_bts_and_hms(model, recalculate = nil)
  if !recalculate && (ret = model.instance_variable_get(:@_brick_bts_and_hms))
    return ret
  end

  model_cols = model.columns_hash
  pk_type = if (mpk = model.primary_key).is_a?(Array)
              # Composite keys should really use:  model.primary_key.map { |pk_part| model_cols[pk_part].type }
              model_cols[mpk.first].type
            else
              mpk && model_cols[mpk].type
            end
  bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
    # %%% The time will come when we will support type checking of composite foreign keys!
    # binding.pry if a.foreign_key.is_a?(Array)
    if a.belongs_to? && !a.polymorphic? && ::Brick.config.polymorphics.fetch(full_assoc_name = "#{model.table_name}.#{a.name}", nil)
      puts "Based on inclusion in ::Brick.polymorphics, marking association #{full_assoc_name} as being polymorphic."
      a.options[:polymorphic] = true
    end
    unless a.polymorphic? || (!a.belongs_to? && (through = a.options[:through])) ||
           (a.klass && ::Brick.config.exclude_tables.exclude?(a.klass.table_name) &&
             (!a.belongs_to? || (same_type = (fk_type = model_cols[a.foreign_key]&.type) == pk_type))
           )
      if same_type == false # We really do want to test specifically for false here, and not nil!
        puts "WARNING:
  Foreign key column #{a.klass.table_name}.#{a.foreign_key} is #{fk_type}, but the primary key it relates to, #{a.active_record.table_name}.#{a.active_record.primary_key}, is #{pk_type}.
  These columns should both be of the same type."
      end
      next
    end

    if a.belongs_to?
      if a.polymorphic?
        rel_poly_bt = relations[model.table_name][:fks].find { |_k, fk| fk[:assoc_name] == a.name.to_s }
        if (primary_tables = rel_poly_bt&.last&.fetch(:inverse_table, [])).is_a?(Array)
          models = rel_poly_bt[1][:polymorphic]&.map { |table| table.singularize.camelize.constantize }
          s.first[a.foreign_key.to_s] = [a.name, models, true]
        else
          # This will come up when using Devise invitable when invited_by_class_name is not
          # specified because in that circumstance it adds a polymorphic :invited_by association,
          # along with appropriate invited_by_type and invited_by_id columns.

          # See if any currently-loaded models have a has_many association over to this polymorphic belongs_to
          hm_models = ActiveRecord::Base.descendants.select do |m|
            m.reflect_on_all_associations.any? { |assoc| !assoc.belongs_to? && assoc.options[:as]&.to_sym == a.name }
          end
          # No need to include models with no table, or subclassed models if their parent is already in the list
          hm_models.reject! { |m| !m.table_exists? || hm_models.any? { |parent| parent != m && m < parent } }
          if hm_models.empty?
            puts "Missing any real indication as to which models \"has_many\" this polymorphic BT in model #{a.active_record.name}:"
            puts "  belongs_to :#{a.name}, polymorphic: true"
          else
            puts "Having analysed all currently-loaded models to infer the various polymorphic has_many associations for #{model.name}, here are the current results:"
            puts "::Brick.polymorphics = { \"#{model.table_name}.#{a.name}\" =>
                     #{hm_models.map(&:name).inspect}
                   }"
            puts 'If you add the above to your brick.rb, it will "cement" these options into place, and avoid this lookup process.'
            s.first[a.foreign_key.to_s] = [a.name, hm_models, true]
          end
        end
      else
        bt_key = a.foreign_key.is_a?(Array) ? a.foreign_key : a.foreign_key.to_s
        s.first[bt_key] = [a.name, a.klass]
      end
    else # This gets all forms of has_many and has_one
      if through # has_many :through or has_one :through
        is_invalid_source = nil
        begin
          if a.through_reflection.macro != :has_many # This HM goes through either a belongs_to or a has_one, so essentially a HOT?
            # Treat it like a belongs_to - just keyed on the association name instead of a foreign_key
            s.first[a.name] = [a.name, a.klass]
            next
          elsif !a.source_reflection # Had considered:  a.active_record.reflect_on_association(a.source_reflection_name).nil?
            is_invalid_source = true
          end
        rescue
          is_invalid_source = true
        end
        if is_invalid_source
          puts "WARNING:  HMT relationship :#{a.name} in model #{model.name} has invalid source :#{a.source_reflection_name}."
          next
        end
      else
        if !a.options.key?(:as) && a.klass.column_names.exclude?(a.foreign_key.to_s)
          options = ", #{a.options.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}" if a.options.present?
          puts "WARNING:  Model #{model.name} has this association:
        has_many :#{a.name}#{options}
      which expects column #{a.foreign_key} to exist in table #{a.klass.table_name}.  This column is missing."
          next

        end
      end
      s.last[a.name] = a
    end
  end
  # Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
  # as well as any possible polymorphic associations
  skip_hms = {}
  hms.each do |hmt|
    if (through = hmt.last.options[:through])
      # ::Brick.relations[hmt.last.through_reflection.table_name]
      skip_hms[through] = nil if hms[through] && model.is_brick? &&
                                 hmt.last.klass != hmt.last.active_record && # Don't pull HMs for HMTs that point back to the same table
                                 hmt[1].source_reflection.belongs_to? # Don't pull HMs when the HMT is built from HM -> HM
      # End up with a hash of HMT names pointing to join-table associations
      model._br_associatives[hmt.first] = hms[through] # || hms["#{(opt = hmt.last.options)[:through].to_s.singularize}_#{opt[:source].to_s.pluralize}".to_sym]
    end
  end
  skip_hms.each { |k, _v| hms.delete(k) }
  model.instance_variable_set(:@_brick_bts_and_hms, [bts, hms]) # Cache and return this result
end

.get_possible_schemasObject



114
115
116
117
118
119
120
121
# File 'lib/brick.rb', line 114

def get_possible_schemas
  if (possible_schemas = (multitenancy = ::Brick.config.schema_behavior&.[](:multitenant)) &&
                          multitenancy&.[](:schema_to_analyse))
    possible_schemas = [possible_schemas] unless possible_schemas.is_a?(Array)
    possible_schema = possible_schemas.find { |ps| ::Brick.db_schemas.key?(ps) }
  end
  [possible_schema, possible_schemas, multitenancy]
end

.get_status_of_resourcesObject

Identify built out routes, migrations, models, (and also soon controllers and views!) for each resource



3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
# File 'lib/brick/extensions.rb', line 3195

def get_status_of_resources
  rails_root = ::Rails.root.to_s
  migrations = if Dir.exist?(mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{rails_root}/db/migrate")
                 Dir["#{mig_path}/**/*.rb"].each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
                   File.read(v).split("\n").each_with_index do |line, line_idx|
                     # For all non-commented lines, look for any that have "create_table", "alter_table", or "drop_table"
                     if !line.lstrip.start_with?('#') &&
                        (idx = (line.index('create_table ') || line.index('create_table('))&.+(13)) ||
                        (idx = (line.index('alter_table ') || line.index('alter_table('))&.+(12)) ||
                        (idx = (line.index('drop_table ') || line.index('drop_table('))&.+(11))
                       tbl = line[idx..-1].match(/([:'"\w\.]+)/)&.captures&.first
                       if tbl
                         v = v[(rails_root.length)..-1] if v.start_with?(rails_root)
                         v = v[1..-1] if v.start_with?('/')
                         s[tbl.tr(':\'"', '').pluralize] << [v, line_idx + 1]
                       end
                     end
                   end
                 end
               end
  abstract_activerecord_bases = ::Brick.eager_load_classes(true)
  rails_root = ::Rails.root.to_s
  models = ::Brick.relations.each_with_object({}) do |rel, s|
             next if rel.first.is_a?(Symbol)

             begin
               if (model = rel.last[:class_name]&.constantize) &&
                  (inh = ActiveRecord::Base._brick_inheriteds[model]&.join(':'))
                 inh = inh[rails_root.length + 1..-1] if inh.start_with?(rails_root)
                 s[rel.first] = [inh, model]
               end
             rescue
             end
           end
  ::Brick.relations.each_with_object([]) do |v, s|
    next if v.first.is_a?(Symbol) # ||
    #       Brick.config.exclude_tables.include?(v.first)

    tbl_parts = v.first.split('.')
    tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
    res = tbl_parts.join('.')
    table_name = (model = models[res])&.last&.table_name
    table_name ||= begin
                     v.last[:class_name].constantize.table_name
                   rescue
                   end
    model = model.first if model.is_a?(Array)
    s << [v.first, table_name || v.first, migrations&.fetch(res, nil), model]
  end
end

.has_ones=(hos) ⇒ Object

Associations to treat as a has_one



509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/brick.rb', line 509

def has_ones=(hos)
  if hos
    hos = hos.call if hos.is_a?(Proc)
    hos = hos.to_a unless hos.is_a?(Array)
    hos = [hos] unless hos.empty? || hos.first.is_a?(Array)
    # Translate to being nested hashes
    Brick.config.has_ones = hos&.each_with_object(Hash.new { |h, k| h[k] = {} }) do |v, s|
      s[v.first][v[1]] = v[2] if v[1]
      s
    end
  end
end

.hmts=(assocs) ⇒ Object

Associations for which to auto-create a has_many _, through: _



530
531
532
# File 'lib/brick.rb', line 530

def hmts=(assocs)
  Brick.config.hmts = assocs
end

.is_apartment_excluded_table(tbl) ⇒ Object



3343
3344
3345
3346
3347
3348
3349
# File 'lib/brick/extensions.rb', line 3343

def is_apartment_excluded_table(tbl)
  if Object.const_defined?('Apartment')
    tbl_klass = (tnp = ::Brick.config.table_name_prefixes&.find { |k, _v| tbl.start_with?(k) }) ? +"#{tnp.last}::" : +''
    tbl_klass << tbl[tnp&.first&.length || 0..-1].singularize.camelize
    Apartment.excluded_models&.include?(tbl_klass)
  end
end

.is_geography?(val) ⇒ Boolean

Returns:

  • (Boolean)


321
322
323
# File 'lib/brick.rb', line 321

def is_geography?(val)
  val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12]
end

.json_columns=(cols) ⇒ Object



541
542
543
# File 'lib/brick.rb', line 541

def json_columns=(cols)
  Brick.config.json_columns = cols
end

.license=(key) ⇒ Object



581
582
583
# File 'lib/brick.rb', line 581

def license=(key)
  Brick.config.license = key
end

.load_additional_referencesObject

Load additional references (virtual foreign keys) This is attempted early if a brick initialiser file is found, and then again as a failsafe at the end of our engine’s initialisation %%% Maybe look for differences the second time ‘round and just add new stuff instead of entirely deferring



592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
# File 'lib/brick.rb', line 592

def load_additional_references
  return if @_additional_references_loaded || ::Brick.config.mode != :on

  relations = ::Brick.relations
  if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
    is_optional = ActiveRecord.version >= ::Gem::Version.new('5.0')
    if ars
      ars.each do |ar|
        fk = ar.length < 5 ? [nil, +ar[0], ar[1], nil, +ar[2]] : [ar[0], +ar[1], ar[2], ar[3], +ar[4], ar[5]]
        ::Brick._add_bt_and_hm(fk, relations, false, is_optional)
      end
    end
    if (polys = ::Brick.config.polymorphics)
      if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.key?(schema)
        ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
      end
      missing_stis = {}
      polys.each do |k, v|
        table_name, poly = k.split('.')
        v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").each_with_object([]) { |result, s| s << result['typ'] if result['typ'] }
        v.each do |type|
          # Allow polymorphic BT to relate to an STI subclass
          base_type = ::Brick.config.sti_namespace_prefixes["::#{type}"] ||
                      ::Brick.config.sti_namespace_prefixes.find { |k, _v| k.end_with?('::') && type.start_with?(k[2..-1]) }&.last&.[](2..-1)
          if relations.key?(primary_table = (base_type || type).underscore.pluralize)
            ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations,
                                   type, # Polymorphic class
                                   is_optional)
          elsif relations.present?
            missing_stis[primary_table] = type unless ::Brick.existing_stis.key?(type)
          end
        end
      end
      unless missing_stis.empty?
        print "
You might be missing an STI namespace prefix entry for these tables:  #{missing_stis.keys.join(', ')}.
In config/initializers/brick.rb appropriate entries would look something like:
  Brick.sti_namespace_prefixes = {"
        puts missing_stis.map { |_k, missing_sti| "\n    '#{missing_sti}' => 'SomeParentModel'" }.join(',')
        puts "  }
(Just trade out SomeParentModel with some more appropriate one.)"
      end
    end
    @_additional_references_loaded = true
  end

  # Find associative tables that can be set up for has_many :through
  ::Brick.relations.each do |key, tbl|
    next if key.is_a?(Symbol)

    tbl_cols = tbl[:cols].keys
    fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = [fk.last[:assoc_name], fk.last[:inverse_table]] if fk.last[:is_bt]; s }
    # Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table only has
    # foreign keys then it can act as an associative table and thus be used with has_many :through.
    if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config. || []) - (tbl[:pkey].values.first || [])).length.zero?
      fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
    end
  end
end

.load_with_retry(loader, autoloaded = nil) ⇒ Object

Some classes (like Phlex::Testing::Rails) will successfully auto-load after a retry



708
709
710
711
712
713
714
715
716
717
718
719
# File 'lib/brick.rb', line 708

def load_with_retry(loader, autoloaded = nil)
  autoloaded ||= loader.send(:autoloaded_dirs).dup
  begin
    loader.eager_load
  rescue Zeitwerk::SetupRequired
    # This is fine -- we eager load what can be eager loaded
  rescue Zeitwerk::NameError
    if autoloaded != (new_auto = loader.send(:autoloaded_dirs))
      load_with_retry(loader, new_auto.dup) # Try one more time and it could come together
    end
  end
end

.metadata_columns=(value) ⇒ Object



455
456
457
# File 'lib/brick.rb', line 455

def (value)
  Brick.config. = value
end

.mode=(setting) ⇒ Object



326
327
328
# File 'lib/brick.rb', line 326

def mode=(setting)
  Brick.config.mode = setting
end

.model_descrips=(descrips) ⇒ Object

DSL templates for individual models to provide prettier descriptions of objects



552
553
554
# File 'lib/brick.rb', line 552

def model_descrips=(descrips)
  Brick.config.model_descrips = descrips
end

.models_inherit_from=(value) ⇒ Object



434
435
436
# File 'lib/brick.rb', line 434

def models_inherit_from=(value)
  Brick.config.models_inherit_from = value
end

.namify(name, action = nil) ⇒ Object

Convert spaces to underscores if the second character and onwards is mixed case



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/brick.rb', line 184

def namify(name, action = nil)
  has_uppers = name =~ /[A-Z]+/
  has_lowers = name =~ /[a-z]+/
  name.downcase! if has_uppers && action == :downcase
  if name.include?(' ')
    # All uppers or all lowers?
    if !has_uppers || !has_lowers
      name.titleize.tr(' ', '_')
    else # Mixed uppers and lowers -- just remove existing spaces
      name.tr(' ', '')
    end
  else
    action == :underscore ? name.underscore : name
  end
end

.nested_attributes=(anaf) ⇒ Object

Associations upon which to add #accepts_nested_attributes_for logic



524
525
526
# File 'lib/brick.rb', line 524

def nested_attributes=(anaf)
  Brick.config.nested_attributes = anaf
end

.non_tenanted_modelsObject

If multitenancy is enabled, a list of non-tenanted “global” models



179
180
181
# File 'lib/brick.rb', line 179

def non_tenanted_models
  @pending_models ||= {}
end

.not_nullables=(value) ⇒ Object



460
461
462
# File 'lib/brick.rb', line 460

def not_nullables=(value)
  Brick.config.not_nullables = value
end

.order=(value) ⇒ Object



485
486
487
# File 'lib/brick.rb', line 485

def order=(value)
  Brick.config.order = value
end

.path_prefix=(path) ⇒ Object

Any path prefixing to apply to all auto-generated Brick routes



332
333
334
# File 'lib/brick.rb', line 332

def path_prefix=(path)
  Brick.config.path_prefix = path
end

.polymorphics=(polys) ⇒ Object

Polymorphic associations



535
536
537
538
# File 'lib/brick.rb', line 535

def polymorphics=(polys)
  polys = polys.each_with_object({}) { |poly, s| s[poly] = nil } if polys.is_a?(Array)
  Brick.config.polymorphics = polys || {}
end

.relationsObject

All tables and views (what Postgres calls “relations” including column and foreign key info)



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/brick.rb', line 147

def relations
  return {} if (ch = ::ActiveRecord::Base.connection_handler).respond_to?(:connection_pool_list) &&
               ch.connection_pool_list.blank?

  # Key our list of relations for this connection off of the connection pool's object_id
  cp_objid = ActiveRecord::Base.connection_pool.object_id
  @relations ||= {}

  # Have we already seen this database? (Perhaps we're connecting to a replica?)
  @relations[cp_objid] ||= if (db_name = ActiveRecord::Base.connection&.instance_variable_get(:@config)&.fetch(:database, nil)) &&
                              !@relations.key?(cp_objid) &&
                              (existing = @relations.find { |_k, v| v.fetch(:db_name, nil) == db_name })
                             existing.last
                           else
                             Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } }.tap do |h|
                               h[:db_name] = db_name if db_name
                             end
                           end
end

.schema_behavior=(behavior) ⇒ Object

Database schema to use when analysing existing data, such as deriving a list of polymorphic classes for polymorphics in which it wasn’t originally specified.



565
566
567
# File 'lib/brick.rb', line 565

def schema_behavior=(behavior)
  Brick.config.schema_behavior = (behavior.is_a?(Symbol) ? { behavior => nil } : behavior)
end

.schema_behaviour=(behavior) ⇒ Object

For any Brits out there



569
570
571
# File 'lib/brick.rb', line 569

def schema_behaviour=(behavior)
  Brick.schema_behavior = behavior
end

.serializerObject

Get the Brick serializer used by all threads.



669
670
671
# File 'lib/brick.rb', line 669

def serializer
  Brick.config.serializer
end

.serializer=(value) ⇒ Object

Set the Brick serializer. This setting affects all threads.



663
664
665
# File 'lib/brick.rb', line 663

def serializer=(value)
  Brick.config.serializer = value
end

.set_db_schema(params = nil) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/brick.rb', line 123

def set_db_schema(params = nil)
  # If Apartment::Tenant.current is not still the default (usually 'public') then an elevator has brought us into
  # a different tenant.  If so then don't allow schema navigation.
  if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' && apartment_multitenant
    current_schema = (ActiveRecord::Base.execute_sql('SELECT current_schemas(true)')
                                        .first['current_schemas'][1..-2]
                                        .split(',') - ['pg_catalog', 'pg_toast', 'heroku_ext']).first
    is_show_schema_list = current_schema == ::Brick.default_schema
    schema = (is_show_schema_list && params && params['_brick_schema']) || ::Brick.default_schema
    chosen = if is_show_schema_list && ::Brick.db_schemas&.key?(schema)
               Apartment::Tenant.switch!(schema)
               schema
             elsif ::Brick.test_schema
               is_show_schema_list = true
               Apartment::Tenant.switch!(::Brick.test_schema)
               ::Brick.test_schema
             else
               current_schema # Just return the current schema
             end
  end
  [chosen == ::Brick.default_schema ? nil : chosen, is_show_schema_list]
end

.sidescroll=(scroll) ⇒ Object



546
547
548
# File 'lib/brick.rb', line 546

def sidescroll=(scroll)
  Brick.config.sidescroll = scroll
end

.skip_database_views=(value) ⇒ Object



424
425
426
# File 'lib/brick.rb', line 424

def skip_database_views=(value)
  Brick.config.skip_database_views = value
end

.skip_index_hms=(value) ⇒ Object

Skip showing counts for these specific has_many associations when building auto-generated #index views



503
504
505
# File 'lib/brick.rb', line 503

def skip_index_hms=(value)
  Brick.config.skip_index_hms = value
end

.sti_modelsObject



101
102
103
# File 'lib/brick.rb', line 101

def sti_models
  @sti_models ||= {}
end

.sti_namespace_prefixes=(snp) ⇒ Object

Module prefixes to build out and associate with specific base STI models



558
559
560
# File 'lib/brick.rb', line 558

def sti_namespace_prefixes=(snp)
  Brick.config.sti_namespace_prefixes = snp
end

.sti_type_column=(type_col) ⇒ Object



573
574
575
# File 'lib/brick.rb', line 573

def sti_type_column=(type_col)
  Brick.config.sti_type_column = (type_col.is_a?(String) ? { type_col => nil } : type_col)
end

.table_name_prefixes=(value) ⇒ Object



444
445
446
# File 'lib/brick.rb', line 444

def table_name_prefixes=(value)
  Brick.config.table_name_prefixes = value
end

.treat_as_module=(value) ⇒ Object

Causes Decidim to work with this line: Brick.treat_as_module = [‘Decidim::ContentBlocks’]



450
451
452
# File 'lib/brick.rb', line 450

def treat_as_module=(value)
  Brick.config.treat_as_module = value
end

.unexclude_column(table, col) ⇒ Object



316
317
318
319
# File 'lib/brick.rb', line 316

def unexclude_column(table, col)
  puts "Unexcluding #{table}.#{col}"
  true
end

.versionObject



683
684
685
# File 'lib/brick.rb', line 683

def version
  VERSION::STRING
end