Hey again @stephan! So here’s a way past your (initially reported) error. In config/initializers/load_hanami.rb
:
require "hanami"
require "dry/operation"
module SportsballHanami
class App < Hanami::App
end
class Action
end
class View
end
end
Hanami.boot
# Define base operation class after booting Hanami
module SportsballHanami
class Operation < Dry::Operation
end
end
p Hanami.app.slices[:hanami_predictor].keys
The problem here is to do with Hanami’s operation extension, which enhances Dry::Operation
to leverage facilities provided by a full Hanami app. Specifically, it’s this line that is the issue. It runs when the class is defined, and it expects the Hanami app/slice to have already been prepared, because it’s looking for slice.namespace::Deps
. This is fine in a multi-file app, because you prepare the app/slice before loading other files. But in a single-file Hanami setup like you’re building here, you were defining your operation class before the Hanami was prepared, which is why the operation extension couldn’t find the Deps
mixin.
The simple workaround here is to define that class after booting the Hanami app.
There’s a more thorough fix, though, which is to take a different approach in operation extension code, with it looking up container-based dependencies at the time of calling .new
instead of needing to locate the container (via the Deps
mixin at the time of class definition). You can see an example of this approach in our action extension. I thought we might’ve discovered a simpler approach for the operation extension, but your experiments have made it clear the other way is more flexible.
I hope this explanation is helpful!
The current arrangement of that initializer code is definitely a hack, and I would like to work with you to make it as clean as possible. The first step would be fixing that operation extension.
By the way, I noticed your action and view classes didn’t inherit from Hanami::Action
and Hanami::View
. Was that intentional, or just an oversight?
Now I did mention initial error up the top of my reply, because after I made that slight fix, something else came up. When running the rails runner
command again, I saw this:
❯ bin/rails runner 'puts "Hello, world!"'
["inflector", "logger", "notifications", "rack.monitor"]
/Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/zeitwerk-2.7.1/lib/zeitwerk/loader.rb:574:in 'block (3 levels) in Zeitwerk::Loader#raise_if_conflicting_directory': loader (Zeitwerk::Error)
#<Zeitwerk::Loader:0x000000011d4d6688
@autoloaded_dirs=[],
@autoloads={},
@collapse_dirs=#<Set: {}>,
@collapse_glob_patterns=#<Set: {}>,
@dirs_autoload_monitor=#<Monitor:0x000000011d5d3bd0>,
@eager_load_exclusions=#<Set: {}>,
@eager_loaded=false,
@ignored_glob_patterns=
#<Set:
{"/Users/tim/Source/scratch/hanami_rails_sample/lib/assets",
"/Users/tim/Source/scratch/hanami_rails_sample/lib/tasks"}>,
@ignored_paths=
#<Set: {"/Users/tim/Source/scratch/hanami_rails_sample/lib/tasks"}>,
@inflector=Rails::Autoloaders::Inflector,
@initialized_at=2025-02-13 14:22:35.124353 +1100,
@logger=nil,
@mutex=#<Thread::Mutex:0x000000011d5d3bf8>,
@namespace_dirs={},
@on_load_callbacks={},
@on_setup_callbacks=[],
@on_unload_callbacks={},
@reloading_enabled=false,
@roots={"/Users/tim/Source/scratch/hanami_rails_sample/lib" => Object},
@setup=false,
@shadowed_files=#<Set: {}>,
@tag="rails.main",
@to_unload={}>
wants to manage directory /Users/tim/Source/scratch/hanami_rails_sample/app/controllers, which is already managed by
#<Zeitwerk::Loader:0x0000000100dbac80
@autoloaded_dirs=[],
@autoloads=
{"/Users/tim/Source/scratch/hanami_rails_sample/app/controllers" =>
#<Zeitwerk::Cref:0x000000011de32b00
@cname=:Controllers,
@mod=SportsballHanami,
@path="SportsballHanami::Controllers">,
"/Users/tim/Source/scratch/hanami_rails_sample/app/helpers" =>
#<Zeitwerk::Cref:0x000000011de32808
@cname=:Helpers,
@mod=SportsballHanami,
@path="SportsballHanami::Helpers">,
"/Users/tim/Source/scratch/hanami_rails_sample/app/jobs" =>
#<Zeitwerk::Cref:0x000000011de32308
@cname=:Jobs,
@mod=SportsballHanami,
@path="SportsballHanami::Jobs">,
"/Users/tim/Source/scratch/hanami_rails_sample/app/mailers" =>
#<Zeitwerk::Cref:0x000000011de31fc0
@cname=:Mailers,
@mod=SportsballHanami,
@path="SportsballHanami::Mailers">,
"/Users/tim/Source/scratch/hanami_rails_sample/app/models" =>
#<Zeitwerk::Cref:0x000000011de31c78
@cname=:Models,
@mod=SportsballHanami,
@path="SportsballHanami::Models">},
@collapse_dirs=#<Set: {}>,
@collapse_glob_patterns=#<Set: {}>,
@dirs_autoload_monitor=#<Monitor:0x000000011dc9bf80>,
@eager_load_exclusions=#<Set: {}>,
@eager_loaded=false,
@ignored_glob_patterns=#<Set: {}>,
@ignored_paths=#<Set: {}>,
@inflector=
#<Dry::System::Plugins::Zeitwerk::CompatInflector:0x000000011de37510
@config=
#<Dry::Configurable::Config values={namespace_separator: ".", autoloader: #<Zeitwerk::Loader:0x0000000100dbac80 ...>, component_dirs: #<Dry::System::Config::ComponentDirs:0x000000011ddb0cb8 @dirs={"app" => #<Dry::System::Config::ComponentDir:0x000000011de1adc0 @__config__=#<Dry::Configurable::Config values={namespaces: #<Dry::System::Config::Namespaces namespaces={nil => #<Dry::System::Config::Namespace path=nil key=nil const="sportsball_hanami">}>, auto_register: #<Proc:0x000000011de19600 /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/hanami-2.2.1/lib/hanami/app.rb:142 (lambda)>, loader: #<Class:0x000000011ddf71e0>, add_to_load_path: false, instance: nil, memoize: false}>, @path="app">}, @defaults=#<Dry::System::Config::ComponentDir:0x000000011ddb0ba0 @__config__=#<Dry::Configurable::Config values={loader: Dry::System::Loader::Autoloading, add_to_load_path: false, auto_register: true, instance: nil, memoize: false, namespaces: #<Dry::System::Config::Namespaces namespaces={}>}>, @path=nil>>, name: :sportsball_hanami, root: #<Pathname:/Users/tim/Source/scratch/hanami_rails_sample>, provider_registrar: #<Class:0x000000011ddf8ea0>, provider_dirs: ["config/providers"], registrations_dir: "config/registrations", env: :development, inflector: #<Dry::Inflector>, resolver: #<Dry::Core::Container::Resolver:0x000000011dbd1d48>, registry: #<Dry::Core::Container::Registry:0x000000011dbd12f8 @_mutex=#<Thread::Mutex:0x000000011dbd1280>, @factory=#<Dry::Core::Container::Item::Factory:0x000000011ddb37b0>>, exports: nil, auto_registrar: Dry::System::AutoRegistrar, manifest_registrar: Dry::System::ManifestRegistrar, importer: Dry::System::Importer}>>,
@initialized_at=2025-02-13 14:22:35.212627 +1100,
@logger=nil,
@mutex=#<Thread::Mutex:0x000000011dc9bfa8>,
@namespace_dirs=
{"SportsballHanami::Controllers" =>
["/Users/tim/Source/scratch/hanami_rails_sample/app/controllers"],
"SportsballHanami::Helpers" =>
["/Users/tim/Source/scratch/hanami_rails_sample/app/helpers"],
"SportsballHanami::Jobs" =>
["/Users/tim/Source/scratch/hanami_rails_sample/app/jobs"],
"SportsballHanami::Mailers" =>
["/Users/tim/Source/scratch/hanami_rails_sample/app/mailers"],
"SportsballHanami::Models" =>
["/Users/tim/Source/scratch/hanami_rails_sample/app/models"]},
@on_load_callbacks={},
@on_setup_callbacks=[],
@on_unload_callbacks={},
@reloading_enabled=false,
@roots=
{"/Users/tim/Source/scratch/hanami_rails_sample/app" => SportsballHanami},
@setup=true,
@shadowed_files=#<Set: {}>,
@tag="e67976",
@to_unload={}>
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/zeitwerk-2.7.1/lib/zeitwerk/loader.rb:568:in 'Hash#each_key'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/zeitwerk-2.7.1/lib/zeitwerk/loader.rb:568:in 'block (2 levels) in Zeitwerk::Loader#raise_if_conflicting_directory'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/zeitwerk-2.7.1/lib/zeitwerk/loader.rb:564:in 'Array#each'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/zeitwerk-2.7.1/lib/zeitwerk/loader.rb:564:in 'block in Zeitwerk::Loader#raise_if_conflicting_directory'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/zeitwerk-2.7.1/lib/zeitwerk/loader.rb:561:in 'Thread::Mutex#synchronize'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/zeitwerk-2.7.1/lib/zeitwerk/loader.rb:561:in 'Zeitwerk::Loader#raise_if_conflicting_directory'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/zeitwerk-2.7.1/lib/zeitwerk/loader/config.rb:122:in 'Zeitwerk::Loader::Config#push_dir'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/application/finisher.rb:30:in 'block (2 levels) in <module:Finisher>'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/application/finisher.rb:25:in 'Array#each'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/application/finisher.rb:25:in 'block in <module:Finisher>'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/initializable.rb:32:in 'BasicObject#instance_exec'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/initializable.rb:32:in 'Rails::Initializable::Initializer#run'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/initializable.rb:61:in 'block in Rails::Initializable#run_initializers'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/tsort.rb:231:in 'block in TSort.tsort_each'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/tsort.rb:353:in 'block (2 levels) in TSort.each_strongly_connected_component'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/tsort.rb:434:in 'TSort.each_strongly_connected_component_from'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/tsort.rb:352:in 'block in TSort.each_strongly_connected_component'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/tsort.rb:350:in 'Rails::Initializable::Collection#each'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/tsort.rb:350:in 'Method#call'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/tsort.rb:350:in 'TSort.each_strongly_connected_component'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/tsort.rb:229:in 'TSort.tsort_each'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/tsort.rb:208:in 'TSort#tsort_each'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/initializable.rb:60:in 'Rails::Initializable#run_initializers'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/application.rb:440:in 'Rails::Application#initialize!'
from /Users/tim/Source/scratch/hanami_rails_sample/config/environment.rb:5:in '<main>'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/bundled_gems.rb:82:in 'Kernel.require'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/bundled_gems.rb:82:in 'block (2 levels) in Kernel#replace_require'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/bootsnap-1.18.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in 'Kernel#require'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/zeitwerk-2.7.1/lib/zeitwerk/core_ext/kernel.rb:34:in 'Kernel#require'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/application.rb:416:in 'Rails::Application#require_environment!'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/command/actions.rb:20:in 'Rails::Command::Actions#boot_application!'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/commands/runner/runner_command.rb:30:in 'Rails::Command::RunnerCommand#perform'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/thor-1.3.2/lib/thor/command.rb:28:in 'Thor::Command#run'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/thor-1.3.2/lib/thor/invocation.rb:127:in 'Thor::Invocation#invoke_command'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/command/base.rb:178:in 'Rails::Command::Base#invoke_command'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/thor-1.3.2/lib/thor.rb:538:in 'Thor.dispatch'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/command/base.rb:73:in 'Rails::Command::Base.perform'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/command.rb:65:in 'block in Rails::Command.invoke'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/command.rb:143:in 'Rails::Command.with_argv'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/command.rb:63:in 'Rails::Command.invoke'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/railties-8.0.1/lib/rails/commands.rb:18:in '<main>'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/bundled_gems.rb:82:in 'Kernel.require'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/bundled_gems.rb:82:in 'block (2 levels) in Kernel#replace_require'
from /Users/tim/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/bootsnap-1.18.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in 'Kernel#require'
from bin/rails:4:in '<main>'
tl;dr we have conflicting Zeitwerk loaders - both Hanami and Rails have loaders and Hanami’s loader are attempting to manage the same root directory.
And I see that @cllns has just given you a way to fix this, so I might stop typing now and just post this 