From 8237c4d33035bd131865f1e73577748892a75f0a Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Thu, 10 Oct 2019 06:43:10 +0200 Subject: [PATCH] support for overrides in :zeitwerk mode inflectors --- .../dependencies/zeitwerk_integration.rb | 10 +++- activesupport/test/zeitwerk_inflector_test.rb | 49 +++++++++++++++++++ .../autoloading_and_reloading_constants.md | 26 +++++----- 3 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 activesupport/test/zeitwerk_inflector_test.rb diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb index 2cf55713bd..2155fba0a9 100644 --- a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb +++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb @@ -54,8 +54,16 @@ module ActiveSupport end module Inflector + # Concurrent::Map is not needed. This is a private class, and overrides + # must be defined while the application boots. + @overrides = {} + def self.camelize(basename, _abspath) - basename.camelize + @overrides[basename] || basename.camelize + end + + def self.inflect(overrides) + @overrides.merge!(overrides) end end diff --git a/activesupport/test/zeitwerk_inflector_test.rb b/activesupport/test/zeitwerk_inflector_test.rb new file mode 100644 index 0000000000..f41279fe92 --- /dev/null +++ b/activesupport/test/zeitwerk_inflector_test.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/dependencies/zeitwerk_integration" + +class ZeitwerkInflectorTest < ActiveSupport::TestCase + INFLECTOR = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector + + def reset_overrides + INFLECTOR.instance_variable_get(:@overrides).clear + end + + def camelize(basename) + INFLECTOR.camelize(basename, nil) + end + + setup do + reset_overrides + @original_inflections = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en] + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: @original_inflections.dup) + end + + teardown do + reset_overrides + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: @original_inflections) + end + + test "it camelizes regular basenames with String#camelize" do + ActiveSupport::Inflector.inflections do |inflect| + inflect.acronym("SSL") + end + + assert_equal "User", camelize("user") + assert_equal "UsersController", camelize("users_controller") + assert_equal "Point3d", camelize("point_3d") + assert_equal "SSLError", camelize("ssl_error") + end + + test "overrides take precendence" do + # Precondition, ensure we are testing something. + assert_equal "MysqlAdapter", camelize("mysql_adapter") + + INFLECTOR.inflect("mysql_adapter" => "MySQLAdapter") + assert_equal "MySQLAdapter", camelize("mysql_adapter") + + # The fallback is still in place. + assert_equal "UsersController", camelize("users_controller") + end +end diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index 5fb0a1fc32..eec8093295 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -274,38 +274,42 @@ By default, Rails uses `String#camelize` to know which constant should a given f 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. -The easiest way is to define an acronym in `config/initializers/inflections.rb`: +The easiest way is to define acronyms in `config/initializers/inflections.rb`: ```ruby ActiveSupport::Inflector.inflections(:en) do |inflect| - inflect.acronym 'HTML' + inflect.acronym "HTML" + inflect.acronym "SSL" end ``` -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: +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: ```ruby # config/initializers/zeitwerk.rb -inflector = Object.new -def inflector.camelize(basename, _abspath) - basename == "html_parser" ? "HTMLParser" : basename.camelize -end - Rails.autoloaders.each do |autoloader| - autoloader.inflector = inflector + autoloader.inflector.inflect( + "html_parser" => "HTMLParser", + "ssl_error" => "SSLError" + ) end ``` -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: +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`: ```ruby # config/initializers/zeitwerk.rb Rails.autoloaders.each do |autoloader| autoloader.inflector = Zeitwerk::Inflector.new - autoloader.inflector.inflect("html_parser" => "HTMLParser") + autoloader.inflector.inflect( + "html_parser" => "HTMLParser", + "ssl_error" => "SSLError" + ) end ``` +There is no global configuration that can affect said instances, they are deterministic. + 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. Troubleshooting