Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Rails::Engine #49

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ end

group :test do
gem "rspec-rails"
gem "super_engine", path: "spec/dummies/with-engine/dummy/engines/super_engine", require: false
end

group :tools do
Expand Down
3 changes: 3 additions & 0 deletions dry-rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "bundler"
spec.add_development_dependency "rake"
spec.add_development_dependency "rspec"

# our super engine used in specs
spec.add_development_dependency "super_engine"
end
27 changes: 16 additions & 11 deletions lib/dry/rails.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

require "dry/rails/railtie"
require "dry/rails/engine"
require "dry/rails/finalizer"
require "dry/rails/container"
require "dry/rails/components"

Expand All @@ -20,14 +22,24 @@ module Dry
#
# @api public
module Rails
extend Configurable
# Set to true to turn off dry-system for main application
# Meant to be used in setup where dry-system is only used within Rails::Engine(s)
#
# @api public
setting :main_app_disabled, default: false

# This is being injected by main app Railtie
# @api private
setting :main_app_name

# 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
Expand All @@ -40,19 +52,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
2 changes: 1 addition & 1 deletion lib/dry/rails/auto_registrars/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def relative_path(dir, file_path)
path = super
return path unless dir.start_with?("app")

path.split("/")[1..-1].join("/")
path.split("/")[1..].join("/")
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/dry/rails/boot/safe_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
end

start do
ApplicationController.include(Dry::Rails::Features::SafeParams)
ActionController::Base.include(Dry::Rails::Features::SafeParams)
zlw marked this conversation as resolved.
Show resolved Hide resolved

if defined?(ActionController::API)
ActionController::API.include(Dry::Rails::Features::SafeParams)
Expand Down
44 changes: 44 additions & 0 deletions lib/dry/rails/engine.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bit puzzling tbh, when I do: Dry::Rails::Engine.container(...) { do something with container } it's not available until reload! call Finalizer and evaluates the config again 🤔 ("worked like that before")

would the same happen if container is used eg. in before_filter - changes done to container would "leak" into next request? this doesn't sound right... 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zlw setting up container should only be available during app's booting phase. Once it's done, we should literally freeze its config (which I believe already happens?)

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
172 changes: 172 additions & 0 deletions lib/dry/rails/finalizer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# 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

# rubocop:disable Metrics/ParameterLists
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
# rubocop:enable Metrics/ParameterLists

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,
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
# rubocop:disable Metrics/ParameterLists
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
# rubocop:enable Metrics/ParameterLists
end
end
end
end
Loading