Skip to content

Commit

Permalink
MONGOID-5829 add a way to ignore paths for model loading (#5904) (#5905)
Browse files Browse the repository at this point in the history
This lets us exclude the `/concerns/` subdirectory that Rails
promotes. Loading the concerns explicitly leads to load errors when
the concerns reference a model that hasn't been loaded yet.
  • Loading branch information
jamis authored Nov 20, 2024
1 parent 058e59e commit fde698f
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 8 deletions.
80 changes: 72 additions & 8 deletions lib/mongoid/loadable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ module Loadable
# (See #model_paths.)
DEFAULT_MODEL_PATHS = %w( ./app/models ./lib/models ).freeze

# The default list of glob patterns that match paths to ignore when loading
# models. Defaults to '*/models/concerns/*', which Rails uses for extensions
# to models (and which cause errors when loaded out of order).
#
# See #ignore_patterns.
DEFAULT_IGNORE_PATTERNS = %w( */models/concerns/* ).freeze

# Search a list of model paths to get every model and require it, so
# that indexing and inheritance work in both development and production
# with the same results.
Expand All @@ -24,17 +31,47 @@ module Loadable
# for model files. These must either be absolute paths, or relative to
# the current working directory.
def load_models(paths = model_paths)
paths.each do |path|
if preload_models.resizable?
files = preload_models.map { |model| "#{path}/#{model.underscore}.rb" }
files = files_under_paths(paths)

files.sort.each do |file|
load_model(file)
end

nil
end

# Given a list of paths, return all ruby files under that path (or, if
# `preload_models` is a list of model names, returns only the files for
# those named models).
#
# @param [ Array<String> ] paths the list of paths to search
#
# @return [ Array<String> ] the normalized file names, suitable for loading
# via `require_dependency` or `require`.
def files_under_paths(paths)
paths.flat_map { |path| files_under_path(path) }
end

# Given a single path, returns all ruby files under that path (or, if
# `preload_models` is a list of model names, returns only the files for
# those named models).
#
# @param [ String ] path the path to search
#
# @return [ Array<String> ] the normalized file names, suitable for loading
# via `require_dependency` or `require`.
def files_under_path(path)
files = if preload_models.resizable?
preload_models.
map { |model| "#{path}/#{model.underscore}.rb" }.
select { |file_name| File.exists?(file_name) }
else
files = Dir.glob("#{path}/**/*.rb")
Dir.glob("#{path}/**/*.rb").
reject { |file_name| ignored?(file_name) }
end

files.sort.each do |file|
load_model(file.gsub(/^#{path}\// , "").gsub(/\.rb$/, ""))
end
end
# strip the path and the suffix from each entry
files.map { |file| file.gsub(/^#{path}\// , "").gsub(/\.rb$/, "") }
end

# A convenience method for loading a model's file. If Rails'
Expand Down Expand Up @@ -71,13 +108,40 @@ def model_paths
DEFAULT_MODEL_PATHS
end

# Returns the array of glob patterns that determine whether a given
# path should be ignored by the model loader.
#
# @return [ Array<String> ] the array of ignore patterns
def ignore_patterns
@ignore_patterns ||= DEFAULT_IGNORE_PATTERNS.dup
end

# Sets the model paths to the given array of paths. These are the paths
# where the application's model definitions are located.
#
# @param [ Array<String> ] paths The list of model paths
def model_paths=(paths)
@model_paths = paths
end

# Sets the ignore patterns to the given array of patterns. These are glob
# patterns that determine whether a given path should be ignored by the
# model loader or not.
#
# @param [ Array<String> ] patterns The list of glob patterns
def ignore_patterns=(patterns)
@ignore_patterns = patterns
end

# Returns true if the given file path matches any of the ignore patterns.
#
# @param [ String ] file_path The file path to consider
#
# @return [ true | false ] whether or not the given file path should be
# ignored.
def ignored?(file_path)
ignore_patterns.any? { |pattern| File.fnmatch?(pattern, file_path) }
end
end

end
86 changes: 86 additions & 0 deletions spec/mongoid/loadable_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# frozen_string_literal: true

require 'spec_helper'

describe Mongoid::Loadable do
let(:lib_dir) { Pathname.new('../../lib').realpath(__dir__) }

shared_context 'with ignore_patterns' do
around do |example|
saved = Mongoid.ignore_patterns
Mongoid.ignore_patterns = ignore_patterns
example.run
ensure
Mongoid.ignore_patterns = saved
end
end

describe '#ignore_patterns' do
context 'when not explicitly set' do
it 'equals the default list of ignore patterns' do
expect(Mongoid.ignore_patterns).to eq Mongoid::Loadable::DEFAULT_IGNORE_PATTERNS
end
end

context 'when explicitly set' do
include_context 'with ignore_patterns'

let(:ignore_patterns) { %w[ pattern1 pattern2 ] }

it 'equals the list of specified patterns' do
expect(Mongoid.ignore_patterns).to eq ignore_patterns
end
end
end

describe '#files_under_path' do
let(:results) { Mongoid.files_under_path(lib_dir) }

include_context 'with ignore_patterns'

context 'when ignore_patterns is empty' do
let(:ignore_patterns) { [] }

it 'returns all ruby files' do
expect(results.length).to be > 10 # should be a bunch of them
expect(results).to include('rails/mongoid')
end
end

context 'when ignore_patterns is not empty' do
let(:ignore_patterns) { %w[ */rails/* ] }

it 'omits the ignored paths' do
expect(results.length).to be > 10 # should be a bunch of them
expect(results).not_to include('rails/mongoid')
end
end
end

describe '#files_under_paths' do
let(:paths) { [ lib_dir.join('mongoid'), lib_dir.join('rails') ] }
let(:results) { Mongoid.files_under_paths(paths) }

include_context 'with ignore_patterns'

context 'when ignore_patterns is empty' do
let(:ignore_patterns) { [] }

it 'returns all ruby files' do
expect(results.length).to be > 10 # should be a bunch
expect(results).to include('generators/mongoid/model/model_generator')
expect(results).to include('fields/encrypted')
end
end

context 'when ignore_patterns is not empty' do
let(:ignore_patterns) { %w[ */model/* */fields/* ] }

it 'returns all ruby files' do
expect(results.length).to be > 10 # should be a bunch
expect(results).not_to include('generators/mongoid/model/model_generator')
expect(results).not_to include('fields/encrypted')
end
end
end
end

0 comments on commit fde698f

Please sign in to comment.