Skip to content

Commit

Permalink
let push_dir accept a namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
fxn committed Jul 14, 2020
1 parent b24730c commit b5a63c2
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 24 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,22 @@ app/models/user.rb -> User
app/controllers/admin/users_controller.rb -> Admin::UsersController
```

Alternatively, you can associate a custom namespace to a root directory by passing a class or module object in the optional `namespace` keyword argument.

For example, Active Job queue adapters have to define a constant after their name in `ActiveJob::QueueAdapters`.

So, if you declare

```ruby
require "active_job"
require "active_job/queue_adapters"
loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
```

your adapter can be stored directly in that directory instead of the canonical `lib/active_job/queue_adapters`.

Please, note that the given namespace must be non-reloadable, though autoloaded constants in that namespace can be. That is, if you associate `app/api` with an existing `Api` module, that module should not be reloadable. However, if the project defines and autoloads the class `Api::V2::Deliveries`, that one can be reloaded.

<a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
### Implicit namespaces

Expand Down
23 changes: 17 additions & 6 deletions lib/zeitwerk/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,19 @@ def dirs
# or descendants.
#
# @param path [<String, Pathname>]
# @param namespace [Class, Module]
# @raise [Zeitwerk::Error]
# @return [void]
def push_dir(path)
def push_dir(path, namespace: Object)
# Note that Class < Module.
unless namespace.is_a?(Module)
raise Error, "#{namespace.inspect} is not a class or module object, should be"
end

abspath = File.expand_path(path)
if dir?(abspath)
raise_if_conflicting_directory(abspath)
root_dirs[abspath] = true
root_dirs[abspath] = namespace
else
raise Error, "the root directory #{abspath} does not exist"
end
Expand Down Expand Up @@ -268,7 +274,9 @@ def setup
mutex.synchronize do
break if @setup

actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) }
actual_root_dirs.each do |root_dir, namespace|
set_autoloads_in_dir(root_dir, namespace)
end
do_preload

@setup = true
Expand Down Expand Up @@ -368,8 +376,11 @@ def eager_load
mutex.synchronize do
break if @eager_loaded

queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) }
queue.map! { |dir| [Object, dir] }
queue = []
actual_root_dirs.each do |root_dir, namespace|
queue << [namespace, root_dir] unless eager_load_exclusions.member?(root_dir)
end

while to_eager_load = queue.shift
namespace, dir = to_eager_load

Expand Down Expand Up @@ -498,7 +509,7 @@ def all_dirs

# @return [<String>]
def actual_root_dirs
root_dirs.keys.delete_if do |root_dir|
root_dirs.reject do |root_dir, _namespace|
!dir?(root_dir) || ignored_paths.member?(root_dir)
end
end
Expand Down
34 changes: 31 additions & 3 deletions test/lib/zeitwerk/test_autovivification.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
require "test_helper"

class TestAutovivification < LoaderTest
test "autoloads a simple constant in an autovivified module" do
module Namespace; end

test "autoloads a simple constant in an autovivified module (Object)" do
files = [["admin/x.rb", "Admin::X = true"]]
with_setup(files) do
assert_kind_of Module, Admin
assert Admin::X
end
end

test "autovivifies several levels in a row" do
test "autoloads a simple constant in an autovivified module (Namespace)" do
files = [["admin/x.rb", "#{Namespace}::Admin::X = true"]]
with_setup(files, namespace: Namespace) do
assert_kind_of Module, Namespace::Admin
assert Namespace::Admin::X
end
end

test "autovivifies several levels in a row (Object)" do
files = [["foo/bar/baz/woo.rb", "Foo::Bar::Baz::Woo = true"]]
with_setup(files) do
assert Foo::Bar::Baz::Woo
end
end

test "autoloads several constants from the same namespace" do
test "autovivifies several levels in a row (Namespace)" do
files = [["foo/bar/baz/woo.rb", "#{Namespace}::Foo::Bar::Baz::Woo = true"]]
with_setup(files, namespace: Namespace) do
assert Namespace::Foo::Bar::Baz::Woo
end
end

test "autoloads several constants from the same namespace (Object)" do
files = [
["app/models/admin/hotel.rb", "class Admin::Hotel; end"],
["app/controllers/admin/hotels_controller.rb", "class Admin::HotelsController; end"]
Expand All @@ -26,4 +43,15 @@ class TestAutovivification < LoaderTest
assert Admin::HotelsController
end
end

test "autoloads several constants from the same namespace (Namespace)" do
files = [
["app/models/admin/hotel.rb", "class #{Namespace}::Admin::Hotel; end"],
["app/controllers/admin/hotels_controller.rb", "class #{Namespace}::Admin::HotelsController; end"]
]
with_setup(files, namespace: Namespace, dirs: %w(app/models app/controllers)) do
assert Namespace::Admin::Hotel
assert Namespace::Admin::HotelsController
end
end
end
60 changes: 57 additions & 3 deletions test/lib/zeitwerk/test_callbacks.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
require "test_helper"

class TestCallbacks < LoaderTest
test "autoloading a file triggers on_file_autoloaded" do
module Namespace; end

test "autoloading a file triggers on_file_autoloaded (Object)" do
def loader.on_file_autoloaded(file)
if file == File.realpath("x.rb")
$on_file_autoloaded_called = true
Expand All @@ -17,7 +19,23 @@ def loader.on_file_autoloaded(file)
end
end

test "requiring an autoloadable file triggers on_file_autoloaded" do
test "autoloading a file triggers on_file_autoloaded (Namespace)" do
def loader.on_file_autoloaded(file)
if file == File.realpath("x.rb")
$on_file_autoloaded_called = true
end
super
end

files = [["x.rb", "#{Namespace}::X = true"]]
with_setup(files, namespace: Namespace) do
$on_file_autoloaded_called = false
assert Namespace::X
assert $on_file_autoloaded_called
end
end

test "requiring an autoloadable file triggers on_file_autoloaded (Object)" do
def loader.on_file_autoloaded(file)
if file == File.realpath("y.rb")
$on_file_autoloaded_called = true
Expand All @@ -37,7 +55,27 @@ def loader.on_file_autoloaded(file)
end
end

test "autoloading a directory triggers on_dir_autoloaded" do
test "requiring an autoloadable file triggers on_file_autoloaded (Namespace)" do
def loader.on_file_autoloaded(file)
if file == File.realpath("y.rb")
$on_file_autoloaded_called = true
end
super
end

files = [
["x.rb", "#{Namespace}::X = true"],
["y.rb", "#{Namespace}::Y = #{Namespace}::X"]
]
with_setup(files, namespace: Namespace, load_path: ".") do
$on_file_autoloaded_called = false
require "y"
assert Namespace::Y
assert $on_file_autoloaded_called
end
end

test "autoloading a directory triggers on_dir_autoloaded (Object)" do
def loader.on_dir_autoloaded(dir)
if dir == File.realpath("m")
$on_dir_autoloaded_called = true
Expand All @@ -52,4 +90,20 @@ def loader.on_dir_autoloaded(dir)
assert $on_dir_autoloaded_called
end
end

test "autoloading a directory triggers on_dir_autoloaded (Namespace)" do
def loader.on_dir_autoloaded(dir)
if dir == File.realpath("m")
$on_dir_autoloaded_called = true
end
super
end

files = [["m/x.rb", "#{Namespace}::M::X = true"]]
with_setup(files, namespace: Namespace) do
$on_dir_autoloaded_called = false
assert Namespace::M::X
assert $on_dir_autoloaded_called
end
end
end
29 changes: 28 additions & 1 deletion test/lib/zeitwerk/test_eager_load.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
require "fileutils"

class TestEagerLoad < LoaderTest
test "eager loads independent files" do
module Namespace; end

test "eager loads independent files (Object)" do
loaders = [loader, new_loader(setup: false)]

$tel0 = $tel1 = false
Expand All @@ -27,6 +29,31 @@ class TestEagerLoad < LoaderTest
end
end

test "eager loads independent files (Namespace)" do
loaders = [loader, new_loader(setup: false)]

$tel0 = $tel1 = false

files = [
["lib0/app0.rb", "module #{Namespace}::App0; end"],
["lib0/app0/foo.rb", "class #{Namespace}::App0::Foo; $tel0 = true; end"],
["lib1/app1/foo.rb", "class App1::Foo; end"],
["lib1/app1/foo/bar/baz.rb", "class App1::Foo::Bar::Baz; $tel1 = true; end"]
]
with_files(files) do
loaders[0].push_dir("lib0", namespace: Namespace)
loaders[0].setup

loaders[1].push_dir("lib1")
loaders[1].setup

Zeitwerk::Loader.eager_load_all

assert $tel0
assert $tel1
end
end

test "eager loads dependent loaders" do
loaders = [loader, new_loader(setup: false)]

Expand Down
16 changes: 15 additions & 1 deletion test/lib/zeitwerk/test_explicit_namespace.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
require "test_helper"

class TestExplicitNamespace < LoaderTest
test "explicit namespaces are loaded correctly" do
module Namespace; end

test "explicit namespaces are loaded correctly (Object)" do
files = [
["app/models/hotel.rb", "class Hotel; X = 1; end"],
["app/models/hotel/pricing.rb", "class Hotel::Pricing; end"]
Expand All @@ -13,6 +15,18 @@ class TestExplicitNamespace < LoaderTest
end
end

test "explicit namespaces are loaded correctly (Namespace)" do
files = [
["app/models/hotel.rb", "class #{Namespace}::Hotel; X = 1; end"],
["app/models/hotel/pricing.rb", "class #{Namespace}::Hotel::Pricing; end"]
]
with_setup(files, namespace: Namespace, dirs: "app/models") do
assert_kind_of Class, Namespace::Hotel
assert Namespace::Hotel::X
assert Namespace::Hotel::Pricing
end
end

test "explicit namespaces are loaded correctly even if #name is overridden" do
files = [
["app/models/hotel.rb", <<~RUBY],
Expand Down
27 changes: 23 additions & 4 deletions test/lib/zeitwerk/test_push_dir.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,29 @@
require "pathname"

class TesPushDir < LoaderTest
test "accepts dirs as strings and stores their absolute paths" do
module Namespace; end

test "accepts dirs as strings and associates them to the Object namespace" do
loader.push_dir(".")
assert loader.root_dirs == { Dir.pwd => true }
assert loader.root_dirs == { Dir.pwd => Object }
assert loader.dirs.include?(Dir.pwd)
end

test "accepts dirs as pathnames and stores their absolute paths" do
test "accepts dirs as pathnames and associates them to the Object namespace" do
loader.push_dir(Pathname.new("."))
assert loader.root_dirs == { Dir.pwd => true }
assert loader.root_dirs == { Dir.pwd => Object }
assert loader.dirs.include?(Dir.pwd)
end

test "accepts dirs as strings and associates them to the given namespace" do
loader.push_dir(".", namespace: Namespace)
assert loader.root_dirs == { Dir.pwd => Namespace }
assert loader.dirs.include?(Dir.pwd)
end

test "accepts dirs as pathnames and associates them to the given namespace" do
loader.push_dir(Pathname.new("."), namespace: Namespace)
assert loader.root_dirs == { Dir.pwd => Namespace }
assert loader.dirs.include?(Dir.pwd)
end

Expand All @@ -19,4 +33,9 @@ class TesPushDir < LoaderTest
e = assert_raises(Zeitwerk::Error) { loader.push_dir(dir) }
assert_equal "the root directory #{dir} does not exist", e.message
end

test "raises if the namespace is not a class or module object" do
e = assert_raises(Zeitwerk::Error) { loader.push_dir(".", namespace: :foo) }
assert_equal ":foo is not a class or module object, should be", e.message
end
end
38 changes: 37 additions & 1 deletion test/lib/zeitwerk/test_reloading.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
require "fileutils"

class TestReloading < LoaderTest
module Namespace; end

test "enabling reloading after setup raises" do
e = assert_raises(Zeitwerk::Error) do
loader = Zeitwerk::Loader.new
Expand All @@ -18,7 +20,7 @@ class TestReloading < LoaderTest
assert loader.reloading_enabled?
end

test "reloading works if the flag is set" do
test "reloading works if the flag is set (Object)" do
files = [
["x.rb", "X = 1"], # top-level
["y.rb", "module Y; end"], # explicit namespace
Expand Down Expand Up @@ -50,6 +52,40 @@ class TestReloading < LoaderTest
end
end

test "reloading works if the flag is set (Namespace)" do
files = [
["x.rb", "#{Namespace}::X = 1"], # top-level
["y.rb", "module #{Namespace}::Y; end"], # explicit namespace
["y/a.rb", "#{Namespace}::Y::A = 1"],
["z/a.rb", "#{Namespace}::Z::A = 1"] # implicit namespace
]
with_setup(files, namespace: Namespace) do
assert_equal 1, Namespace::X
assert_equal 1, Namespace::Y::A
assert_equal 1, Namespace::Z::A

ns_object_id = Namespace.object_id
y_object_id = Namespace::Y.object_id
z_object_id = Namespace::Z.object_id

File.write("x.rb", "#{Namespace}::X = 2")
File.write("y/a.rb", "#{Namespace}::Y::A = 2")
File.write("z/a.rb", "#{Namespace}::Z::A = 2")

loader.reload

assert_equal 2, Namespace::X
assert_equal 2, Namespace::Y::A
assert_equal 2, Namespace::Z::A

assert Namespace.object_id == ns_object_id
assert Namespace::Y.object_id != y_object_id
assert Namespace::Z.object_id != z_object_id

assert_equal 2, Namespace::X
end
end

test "reloading raises if the flag is not set" do
e = assert_raises(Zeitwerk::ReloadingDisabledError) do
loader = Zeitwerk::Loader.new
Expand Down
Loading

0 comments on commit b5a63c2

Please sign in to comment.