Skip to content

Commit 035ea91

Browse files
committed
support for overrides in :zeitwerk mode inflectors
1 parent 8f6915a commit 035ea91

File tree

3 files changed

+73
-12
lines changed

3 files changed

+73
-12
lines changed

activesupport/lib/active_support/dependencies/zeitwerk_integration.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,16 @@ def require_dependency(filename)
5454
end
5555

5656
module Inflector
57+
# Concurrent::Map is not needed. This is a private class, and overrides
58+
# must be defined while the application boots.
59+
@overrides = {}
60+
5761
def self.camelize(basename, _abspath)
58-
basename.camelize
62+
@overrides[basename] || basename.camelize
63+
end
64+
65+
def self.inflect(overrides)
66+
@overrides.merge!(overrides)
5967
end
6068
end
6169

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
require "active_support/dependencies/zeitwerk_integration"
5+
6+
class ZeitwerkInflectorTest < ActiveSupport::TestCase
7+
INFLECTOR = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector
8+
9+
def reset_overrides
10+
INFLECTOR.instance_variable_get(:@overrides).clear
11+
end
12+
13+
def camelize(basename)
14+
INFLECTOR.camelize(basename, nil)
15+
end
16+
17+
setup do
18+
reset_overrides
19+
@original_inflections = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en]
20+
ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: @original_inflections.dup)
21+
end
22+
23+
teardown do
24+
reset_overrides
25+
ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: @original_inflections)
26+
end
27+
28+
test "it camelizes regular basenames with String#camelize" do
29+
ActiveSupport::Inflector.inflections do |inflect|
30+
inflect.acronym("SSL")
31+
end
32+
33+
assert_equal "User", camelize("user")
34+
assert_equal "UsersController", camelize("users_controller")
35+
assert_equal "Point3d", camelize("point_3d")
36+
assert_equal "SSLError", camelize("ssl_error")
37+
end
38+
39+
test "overrides take precendence" do
40+
# Precondition, ensure we are testing something.
41+
assert_equal "MysqlAdapter", camelize("mysql_adapter")
42+
43+
INFLECTOR.inflect("mysql_adapter" => "MySQLAdapter")
44+
assert_equal "MySQLAdapter", camelize("mysql_adapter")
45+
46+
# The fallback is still in place.
47+
assert_equal "UsersController", camelize("users_controller")
48+
end
49+
end

guides/source/autoloading_and_reloading_constants.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -274,38 +274,42 @@ By default, Rails uses `String#camelize` to know which constant should a given f
274274

275275
It could be the case that some particular file or directory name does not get inflected as you want. For instance, `html_parser.rb` is expected to define `HtmlParser` by default. What if you prefer the class to be `HTMLParser`? There are a few ways to customize this.
276276

277-
The easiest way is to define an acronym in `config/initializers/inflections.rb`:
277+
The easiest way is to define acronyms in `config/initializers/inflections.rb`:
278278

279279
```ruby
280280
ActiveSupport::Inflector.inflections(:en) do |inflect|
281-
inflect.acronym 'HTML'
281+
inflect.acronym "HTML"
282+
inflect.acronym "SSL"
282283
end
283284
```
284285

285-
Doing so affects how Active Support inflects globally. That may be fine in some applications, but perhaps you prefer a more controlled technique that does not have a global effect. In such case, you can override the actual inflector in an initializer:
286+
Doing so affects how Active Support inflects globally. That may be fine in some applications, but you can also customize how to camelize individual basenames independently from Active Support by passing a collection of overrides to the default inflectors:
286287

287288
```ruby
288289
# config/initializers/zeitwerk.rb
289-
inflector = Object.new
290-
def inflector.camelize(basename, _abspath)
291-
basename == "html_parser" ? "HTMLParser" : basename.camelize
292-
end
293-
294290
Rails.autoloaders.each do |autoloader|
295-
autoloader.inflector = inflector
291+
autoloader.inflector.inflect(
292+
"html_parser" => "HTMLParser",
293+
"ssl_error" => "SSLError"
294+
)
296295
end
297296
```
298297

299-
As you see, that still uses `String#camelize` as fallback. If you instead prefer not to depend on Active Support inflections at all and have absolute control over inflections, do this instead:
298+
That technique still depends on `String#camelize`, though, because that is what the default inflectors use as fallback. If you instead prefer not to depend on Active Support inflections at all and have absolute control over inflections, configure the inflectors to be instances of `Zeitwerk::Inflector`:
300299

301300
```ruby
302301
# config/initializers/zeitwerk.rb
303302
Rails.autoloaders.each do |autoloader|
304303
autoloader.inflector = Zeitwerk::Inflector.new
305-
autoloader.inflector.inflect("html_parser" => "HTMLParser")
304+
autoloader.inflector.inflect(
305+
"html_parser" => "HTMLParser",
306+
"ssl_error" => "SSLError"
307+
)
306308
end
307309
```
308310

311+
There is no global configuration that can affect said instances, they are deterministic.
312+
309313
You can even define a custom inflector for full flexibility. Please, check the [Zeitwerk documentation](https://github.com/fxn/zeitwerk#custom-inflector) for further details.
310314

311315
Troubleshooting

0 commit comments

Comments
 (0)