dry-rails
dry-rails copied to clipboard
Injection issue in rails 7 & ruby 3.1
We have more than 20 rails app using rails 6 & below and ruby 2.7 & below, there are implemented service pattern using dry injection, it is broken on rails 7 & ruby 3.1
I'm getting an error looks like
Started POST "/api/v1/posts/search.json" for ::1 at 2022-02-16 00:26:43 +0000
ArgumentError (wrong number of arguments (given 1, expected 0)):
actionpack (7.0.2.2) lib/action_controller/metal.rb:150:in `initialize'
actionpack (7.0.2.2) lib/action_dispatch/routing/url_for.rb:108:in `initialize'
dry-auto_inject (0.9.0) lib/dry/auto_inject/strategies/kwargs.rb:71:in `block (2 levels) in define_initialize_with_splat'
dry-auto_inject (0.9.0) lib/dry/auto_inject/strategies/kwargs.rb:22:in `new'
dry-auto_inject (0.9.0) lib/dry/auto_inject/strategies/kwargs.rb:22:in `block (2 levels) in define_new'
actionpack (7.0.2.2) lib/action_controller/metal.rb:251:in `dispatch'
actionpack (7.0.2.2) lib/action_dispatch/routing/route_set.rb:49:in `dispatch'
actionpack (7.0.2.2) lib/action_dispatch/routing/route_set.rb:32:in `serve'
actionpack (7.0.2.2) lib/action_dispatch/journey/router.rb:50:in `block in serve'
actionpack (7.0.2.2) lib/action_dispatch/journey/router.rb:32:in `each'
actionpack (7.0.2.2) lib/action_dispatch/journey/router.rb:32:in `serve'
actionpack (7.0.2.2) lib/action_dispatch/routing/route_set.rb:850:in `call'
rack (2.2.3) lib/rack/etag.rb:27:in `call'
rack (2.2.3) lib/rack/conditional_get.rb:40:in `call'
rack (2.2.3) lib/rack/head.rb:12:in `call'
actionpack (7.0.2.2) lib/action_dispatch/middleware/callbacks.rb:27:in `block in call'
activesupport (7.0.2.2) lib/active_support/callbacks.rb:99:in `run_callbacks'
actionpack (7.0.2.2) lib/action_dispatch/middleware/callbacks.rb:26:in `call'
actionpack (7.0.2.2) lib/action_dispatch/middleware/executor.rb:14:in `call'
actionpack (7.0.2.2) lib/action_dispatch/middleware/actionable_exceptions.rb:17:in `call'
actionpack (7.0.2.2) lib/action_dispatch/middleware/debug_exceptions.rb:28:in `call'
actionpack (7.0.2.2) lib/action_dispatch/middleware/show_exceptions.rb:26:in `call'
railties (7.0.2.2) lib/rails/rack/logger.rb:36:in `call_app'
railties (7.0.2.2) lib/rails/rack/logger.rb:25:in `block in call'
activesupport (7.0.2.2) lib/active_support/tagged_logging.rb:99:in `block in tagged'
activesupport (7.0.2.2) lib/active_support/tagged_logging.rb:37:in `tagged'
activesupport (7.0.2.2) lib/active_support/tagged_logging.rb:99:in `tagged'
railties (7.0.2.2) lib/rails/rack/logger.rb:25:in `call'
actionpack (7.0.2.2) lib/action_dispatch/middleware/remote_ip.rb:93:in `call'
actionpack (7.0.2.2) lib/action_dispatch/middleware/request_id.rb:26:in `call'
rack (2.2.3) lib/rack/runtime.rb:22:in `call'
activesupport (7.0.2.2) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
actionpack (7.0.2.2) lib/action_dispatch/middleware/server_timing.rb:20:in `call'
actionpack (7.0.2.2) lib/action_dispatch/middleware/executor.rb:14:in `call'
actionpack (7.0.2.2) lib/action_dispatch/middleware/static.rb:23:in `call'
rack (2.2.3) lib/rack/sendfile.rb:110:in `call'
actionpack (7.0.2.2) lib/action_dispatch/middleware/host_authorization.rb:137:in `call'
railties (7.0.2.2) lib/rails/engine.rb:530:in `call'
puma (5.6.2) lib/puma/configuration.rb:252:in `call'
puma (5.6.2) lib/puma/request.rb:77:in `block in handle_request'
puma (5.6.2) lib/puma/thread_pool.rb:340:in `with_force_shutdown'
puma (5.6.2) lib/puma/request.rb:76:in `handle_request'
puma (5.6.2) lib/puma/server.rb:441:in `process_client'
puma (5.6.2) lib/puma/thread_pool.rb:147:in `block in spawn_thread'
To Reproduce
- create a new app using rails 7 with api mode
- create a service with a single method so we have this structure under app directory
app/
controllers/
api/
v1/
...
...
services/
v1/
module V1
class PostService
def search(keyword)
"OK"
end
end
end
- create a container class named
di_container.rb
onlib/marka/
directory and register and initialize the PostService class
require 'dry-container'
require 'dry-auto_inject'
module Marka
class DiContainer
extend Dry::Container::Mixin
register :v1_post_service do
V1::PostService.new
end
end
INJECT = Dry::AutoInject(Marka::DiContainer)
end
- create a new controller called
posts_controller
underapp/controllers/api/v1/
directory, and try to inject the method which registered onMarka
module
require 'marka/di_container'
module Api
module V1
class PostsController < ApplicationController
include Marka::INJECT[:v1_post_service]
def search
render json: { status: "OK" }
end
end
end
Expected behavior
Upgrade our apps to rails 7 using the same pattern.
My environment
- Affects my production application: YES due to development issue
- Ruby version: 3.1
- Rails 7.0.2.2
- OS: MacOS Mojave 10.14.6
- dry-auto_inject (0.9.0)
- dry-container (0.9.0)
stakeoverflow question : https://stackoverflow.com/q/71131422/2858044
This is a change in Rails that broke your code, by using auto-inject you define a constructor and it looks like it's no longer compatible with Rails 7. dry-auto_inject shouldn't be used in 3rd-party libraries exactly because of this. We can't do anything about this here so I'm going to move this issue to dry-rails and we can see how it could be done there.
This was moved from dry-auto_inject because we could come up with a nice integration with the controller API. Marking it as help-wanted as I don't have time (for now at least) to work on this.
I thank you @solnic for your response and help to moved this issue.
I thought dry-auto-_inject
is supported the ruby ecosystem, including rails. I have use that (starting rails 4 & ruby 2.3) which is since 5 years ago and stable for production until now with more than 20 rails apps using rails 6 and below, and still thinking upgrade them early to rails 7.
I need to find another way to make it work without big changes, for the moment I try to use dry-rails
, and I'm not sure about this strategy
Add a path app/services
to container
# config/initializers/marka.rb
Dry::Rails.container do
config.component_dirs.add "app/services"
end
and in controller I can call looks like
MyApp::Container['v1.post_service'].search(params[:keyword])
What do you think about this?
I thought dry-auto-_inject is supported the ruby ecosystem, including rails.
I should clarify - dry-auto_inject
works with Rails but you assume you can use it with any class, which is not the case because the purpose of dry-auto_inject
is to define a constructor method that will receive dependencies that are automatically resolved from the configured container. Because of this, it is not advices to include injection modules in classes that you don't own because it may break them.
We should definitely explain this in the docs 🙂
MyApp::Container['v1.post_service'].search(params[:keyword]) What do you think about this?
Actually, dry-rails gives you a resolve
helper in controllers, so this can become resolve('v1.post_service').search(params[:keyword])
. You could also provide a temporary solution while you're in the process of migrating to dry-rails by simply defining method_missing
in your application controller, something like this should work:
def method_missing(name, *args)
if container.key?("v1.#{name}")
resolve("v1.#{name}")
else
super
end
end
This is obviously a hack but it should help with the transition 🙂
On our project we updated dry-rb to the latest version and faced the same issue, so we decided to build our own injection strategy for controllers:
# frozen_string_literal: true
require "dry/auto_inject/dependency_map"
module Dry
module AutoInject
class Strategies
class Resolve < Module
InstanceMethods = Class.new(Module)
attr_reader :container
attr_reader :dependency_map
attr_reader :instance_mod
def initialize(container, *dependency_names)
super()
@container = container
@dependency_map = DependencyMap.new(*dependency_names)
@instance_mod = InstanceMethods.new
end
# @api private
def included(klass)
define_resolvers
klass.send(:include, instance_mod)
super
end
private
def define_resolvers
instance_mod.class_exec(container, dependency_map) do |container, dependency_map|
dependency_map.to_h.each do |name, key|
define_method name do
container[key]
end
end
end
end
end
register :resolve, Resolve
end
end
end
and then in controllers we do:
include YourApp::Import.resolve[
'your.operations.name',
]
Hi @solnic ! I'm also struggling with this issue after upgrading from dry-rails
v0.2 to the latest. I don't think that it's a Rails problem because I'm still at Rails v6.1.6.1. I'm also tried to reproduce the bug in test for dry-rails
and it's failed. Like this:
class ApiUsersController < ActionController::API
include Dummy::Deps[
mailer: "mailer"
]
...
end
ArgumentError:
wrong number of arguments (given 1, expected 0)
# ./spec/requests/api_users_spec.rb:16:in `block (4 levels) in <top (required)>'
I think there is something wrong with dry-auto_inject but I don't know yet what exactly. Maybe you can give some advice?
Thanks!
I'm not sure that this is a smarter one but it fixes the problem https://github.com/k0va1/dry-rails/commit/767154a3e8e320b54a6725783afd7df5aa3e0acd
Hi @solnic I just got informed, it's weird the problem is solved when I use action controller base instead of using the api mode. meanwhile the api mode is a lightweight version, which is exclude some modules from base.