diff --git a/lib/dry/rails.rb b/lib/dry/rails.rb index c7551e0..6830b09 100644 --- a/lib/dry/rails.rb +++ b/lib/dry/rails.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true require "dry/rails/railtie" -require "dry/rails/container" -require "dry/rails/components" +require "dry/rails/engine" module Dry # Initializer interface @@ -18,14 +17,17 @@ module Dry # # @api public module Rails + extend Dry::Configurable + setting :main_app_name + setting :main_app_enabled, default: true + # Set container block that will be evaluated in the context of the container # # @return [self] # # @api public def self.container(&block) - _container_blocks << block - self + Engine.container(config.main_app_name, &block) end # Create a new container class @@ -38,19 +40,12 @@ def self.container(&block) # # @api private def self.create_container(options = {}) - Class.new(Container) { config.update(options) } + Engine.create_container(options) end # @api private def self.evaluate_initializer(container) - _container_blocks.each do |block| - container.class_eval(&block) - end - end - - # @api private - def self._container_blocks - @_container_blocks ||= [] + Engine.evaluate_initializer(config.main_app_name, container) end end end diff --git a/lib/dry/rails/boot/safe_params.rb b/lib/dry/rails/boot/safe_params.rb index f0c014d..4fd21c2 100644 --- a/lib/dry/rails/boot/safe_params.rb +++ b/lib/dry/rails/boot/safe_params.rb @@ -6,7 +6,7 @@ end start do - ApplicationController.include(Dry::Rails::Features::SafeParams) + ActionController::Base.include(Dry::Rails::Features::SafeParams) if defined?(ActionController::API) ActionController::API.include(Dry::Rails::Features::SafeParams) diff --git a/lib/dry/rails/container.rb b/lib/dry/rails/container.rb index ae4b6be..982c7d1 100644 --- a/lib/dry/rails/container.rb +++ b/lib/dry/rails/container.rb @@ -27,7 +27,7 @@ class Container < System::Container # # @api public # @!scope class - setting :features, %i[application_contract safe_params controller_helpers], reader: true + setting :features, default: %i[application_contract safe_params controller_helpers], reader: true # @overload config.auto_register_paths=(paths) # Set an array of path/block pairs for auto-registration @@ -39,7 +39,7 @@ class Container < System::Container # # @api public # @!scope class - setting :auto_register_paths, [].freeze, reader: true + setting :auto_register_paths, default: [].freeze, reader: true # @overload config.auto_inject_constant=(auto_inject_constant) # Set a custom import constant name @@ -48,7 +48,7 @@ class Container < System::Container # # @api public # @!scope class - setting :auto_inject_constant, "Deps", reader: true + setting :auto_inject_constant, default: "Deps", reader: true # @overload config.container_constant=(container_constant) # Set a custom container constant @@ -57,7 +57,7 @@ class Container < System::Container # # @api public # @!scope class - setting :container_constant, "Container", reader: true + setting :container_constant, default: "Container", reader: true # @!endgroup diff --git a/lib/dry/rails/engine.rb b/lib/dry/rails/engine.rb new file mode 100644 index 0000000..d12eb78 --- /dev/null +++ b/lib/dry/rails/engine.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "dry/rails/container" +require "dry/rails/components" + +module Dry + module Rails + module Engine + # Set container block that will be evaluated in the context of the container + # + # @param name [Symbol] + # @return [self] + # + # @api public + def self.container(name, &block) + _container_blocks[name] << block + self + end + + # Create a new container class + # + # This is used during booting and reloading + # + # @param name [Symbol] + # @param options [Hash] Container configuration settings + # + # @return [Class] + # + # @api private + def self.create_container(options = {}) + Class.new(Container) { config.update(options) } + end + + # @api private + def self.evaluate_initializer(name, container) + _container_blocks[name].each do |block| + container.class_eval(&block) + end + end + + # @api private + def self._container_blocks + @_container_blocks ||= Hash.new { |h, k| h[k] = [] } + end + end + end +end diff --git a/lib/dry/rails/finalizer.rb b/lib/dry/rails/finalizer.rb new file mode 100644 index 0000000..0d91311 --- /dev/null +++ b/lib/dry/rails/finalizer.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +module Dry + module Rails + class Finalizer + def self.app_namespace_to_name(app_namespace) + app_namespace.name.underscore.to_sym + end + + def initialize( + railtie:, + app_namespace:, + root_path:, + name: Dry::Rails.config.main_app_name, + container_const_name: Dry::Rails::Container.container_constant, + default_inflector: ActiveSupport::Inflector + ) + @railtie = railtie + @app_namespace = app_namespace + @root_path = root_path + @name = name + @container_const_name = container_const_name + @default_inflector = default_inflector + end + + attr_reader :railtie, + :root_path, + :container_const_name + + # Infer the default application namespace + # + # TODO: we had to rename namespace=>app_namespace because + # Rake::DSL's Kernel#namespace *sometimes* breaks things. + # Currently we are missing specs verifying that rake tasks work + # correctly and those must be added! + # + # @return [Module] + # + # @api public + attr_reader :app_namespace + + # Code-reloading-aware finalization process + # + # This sets up `Container` and `Deps` constants, reloads them if this is in reloading mode, + # and registers default components like the railtie itself or the inflector + # + # @api public + # + # rubocop:disable Metrics/AbcSize + def finalize! + stop_features if reloading? + + container = Dry::Rails::Engine.create_container( + root: root_path, + name: name, + default_namespace: name.to_s, + inflector: default_inflector, + system_dir: root_path.join("config/system"), + bootable_dirs: [root_path.join("config/system/boot")] + ) + + # Enable :env plugin by default because it is a very common requirement + container.use :env, inferrer: -> { ::Rails.env } + + container.register(:railtie, railtie) + container.register(:inflector, default_inflector) + + # Remove previously defined constants, if any, so we don't end up with + # unsused constants in app's namespace when a name change happens. + remove_constant(container.auto_inject_constant) + remove_constant(container.container_constant) + + Dry::Rails::Engine.evaluate_initializer(name, container) + + @container_const_name = container.container_constant + + set_or_reload(container.container_constant, container) + set_or_reload(container.auto_inject_constant, container.injector) + + container.features.each do |feature| + container.boot(feature, from: :rails) + end + + container.refresh_boot_files if reloading? + + container.finalize!(freeze: !::Rails.env.test?) + end + # rubocop:enable Metrics/AbcSize + + # Stops all configured features (bootable components) + # + # This is *crucial* when reloading code in development mode. Every bootable component + # should be able to clear the runtime from any constants that it created in its `stop` + # lifecycle step + # + # @api public + def stop_features + container.features.each do |feature| + container.stop(feature) if container.booted?(feature) + end + end + + # Exposes the container constant + # + # @return [Dry::Rails::Container] + # + # @api public + def container + app_namespace.const_get(container_const_name, false) + end + + # Return true if we're in code-reloading mode + # + # @api private + def reloading? + app_namespace.const_defined?(container_const_name, false) + end + + # Return the default system name + # + # In the dry-system world containers are explicitly named using symbols, so that you can + # refer to them easily when ie importing one container into another + # + # @return [Symbol] + # + # @api private + attr_reader :name + + # Sets or reloads a constant within the application namespace + # + # @api private + attr_reader :default_inflector + + # @api private + def set_or_reload(const_name, const) + remove_constant(const_name) + app_namespace.const_set(const_name, const) + end + + # @api private + def remove_constant(const_name) + if app_namespace.const_defined?(const_name, false) + app_namespace.__send__(:remove_const, const_name) + end + end + end + + module Engine + class Finalizer + def self.new( + railtie:, + app_namespace:, + root_path:, + name: nil, + container_const_name: Dry::Rails::Container.container_constant, + default_inflector: ActiveSupport::Inflector + ) + Dry::Rails::Finalizer.new( + railtie: railtie, + app_namespace: app_namespace, + root_path: root_path, + name: name || ::Dry::Rails::Finalizer.app_namespace_to_name(app_namespace), + container_const_name: container_const_name, + default_inflector: default_inflector + ) + end + end + end + end +end diff --git a/lib/dry/rails/railtie.rb b/lib/dry/rails/railtie.rb index 10a2a14..37f5d2b 100644 --- a/lib/dry/rails/railtie.rb +++ b/lib/dry/rails/railtie.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "rails/railtie" +require "dry/rails/finalizer" module Dry module Rails @@ -8,12 +9,31 @@ module Rails # # @api public class Railtie < ::Rails::Railtie - attr_reader :container_const_name - # This is needed because `finalize!` can reload code and this hook is called every-time # in development env upon a request (in production it's called just once during booting) config.to_prepare do - Railtie.finalize! + Railtie.finalize! if Dry::Rails.config.main_app_enabled + end + + initializer "dry-rails.main-app-container" do + Dry::Rails.config.main_app_name = Dry::Rails::Finalizer.app_namespace_to_name(app_namespace) + end + + # Infer the default application namespace + # + # TODO: we had to rename namespace=>app_namespace because + # Rake::DSL's Kernel#namespace *sometimes* breaks things. + # Currently we are missing specs verifying that rake tasks work + # correctly and those must be added! + # + # @return [Module] + # + # @api public + def app_namespace + @app_namespace ||= begin + top_level_namespace = ::Rails.application.class.to_s.split("::").first + Object.const_get(top_level_namespace) + end end # Code-reloading-aware finalization process @@ -22,51 +42,7 @@ class Railtie < ::Rails::Railtie # and registers default components like the railtie itself or the inflector # # @api public - # - # rubocop:disable Metrics/AbcSize - def finalize! - @container_const_name ||= Dry::Rails::Container.container_constant - - stop_features if reloading? - - root_path = ::Rails.root - - container = Dry::Rails.create_container( - root: root_path, - name: name, - default_namespace: name.to_s, - inflector: default_inflector, - system_dir: root_path.join("config/system"), - bootable_dirs: [root_path.join("config/system/boot")] - ) - - # Enable :env plugin by default because it is a very common requirement - container.use :env, inferrer: -> { ::Rails.env } - - container.register(:railtie, self) - container.register(:inflector, default_inflector) - - # Remove previously defined constants, if any, so we don't end up with - # unsused constants in app's namespace when a name change happens. - remove_constant(container.auto_inject_constant) - remove_constant(container.container_constant) - - Dry::Rails.evaluate_initializer(container) - - @container_const_name = container.container_constant - - set_or_reload(container.container_constant, container) - set_or_reload(container.auto_inject_constant, container.injector) - - container.features.each do |feature| - container.boot(feature, from: :rails) - end - - container.refresh_boot_files if reloading? - - container.finalize!(freeze: !::Rails.env.test?) - end - # rubocop:enable Metrics/AbcSize + delegate :finalize!, to: :finalizer alias_method :reload, :finalize! # Stops all configured features (bootable components) @@ -76,75 +52,29 @@ def finalize! # lifecycle step # # @api public - def stop_features - container.features.each do |feature| - container.stop(feature) if container.booted?(feature) - end - end + delegate :stop_features, to: :finalizer # Exposes the container constant # # @return [Dry::Rails::Container] # # @api public - def container - app_namespace.const_get(container_const_name, false) - end + delegate :container, to: :finalizer - # Return true if we're in code-reloading mode - # # @api private - def reloading? - app_namespace.const_defined?(container_const_name, false) - end + delegate :set_or_reload, to: :finalizer - # Return the default system name - # - # In the dry-system world containers are explicitly named using symbols, so that you can - # refer to them easily when ie importing one container into another - # - # @return [Symbol] - # # @api private - def name - app_namespace.name.underscore.to_sym - end + delegate :remove_constant, to: :finalizer - # Infer the default application namespace - # - # TODO: we had to rename namespace=>app_namespace because - # Rake::DSL's Kernel#namespace *sometimes* breaks things. - # Currently we are missing specs verifying that rake tasks work - # correctly and those must be added! - # - # @return [Module] - # - # @api public - def app_namespace - @app_namespace ||= begin - top_level_namespace = ::Rails.application.class.to_s.split("::").first - Object.const_get(top_level_namespace) - end - end + private - # Sets or reloads a constant within the application namespace - # - # @api private - def default_inflector - ActiveSupport::Inflector - end - - # @api private - def set_or_reload(const_name, const) - remove_constant(const_name) - app_namespace.const_set(const_name, const) - end - - # @api private - def remove_constant(const_name) - if app_namespace.const_defined?(const_name, false) - app_namespace.__send__(:remove_const, const_name) - end + def finalizer + @finalizer ||= Finalizer.new( + railtie: self, + app_namespace: app_namespace, + root_path: ::Rails.root + ) end end end diff --git a/spec/dummy-6.x/dummy/config/environments/test.rb b/spec/dummy-6.x/dummy/config/environments/test.rb index bae3b39..c045a26 100644 --- a/spec/dummy-6.x/dummy/config/environments/test.rb +++ b/spec/dummy-6.x/dummy/config/environments/test.rb @@ -18,7 +18,7 @@ # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { - "Cache-Control" => "public, max-age=#{1.hour.to_i}" + "Cache-Control" => "public, max-age=3600" } # Show full error reports and disable caching.