Can a 3rd party gem "inject" itself into app initialization?

Hello,

I’m attempting to make a contribution to the alba ruby gem so that it could automatically configure the itself when it’s installed in the context of a Hanami or Rails application.

Alba is a JSON serialization gem which requires calling Alba.inflector = SomeInflector to transform keys into lowerCamel etc. By default, the inflector is nil but I’d like it to be automatically set to :active_support for a Rails app and :dry for a Hanami app.

I believe I found the “correct” way to do this in Rails (Add Alba::Railtie to set default inflector in Rails apps by trevorturk · Pull Request #274 · okuramasafumi/alba · GitHub) but I’m having trouble finding a similar option in Hanami.

I noticed that Hanami doesn’t automatically require 3rd party gems, and there seems to be a nice system for creating your own initializers etc, but I haven’t found a way for a provider to submit a block of code to be executed when the app is initialized, etc. (I’m not sure if the provider system is what I’m looking for, tbh!)

No worries at all if this isn’t possible (or isn’t advisable) but I’d love any advice from someone with more experience than me.

Thanks!

@trevorturk Please try with providers:

config/providers/alba.rb

# frozen_string_literal: true

Hanami.application.register_provider :alba do
  prepare do
    require "alba"
    Alba.inflector = target["inflector"]
  end
end

cc: @timriley

Thank you so much for the reply!

I gave this a shot and wasn’t able to get it working quite as I expected. It looks to me like the prepare block isn’t being called, at least not when I would expect it.

Here’s the relevant line where I would expect to see an exception raised: Add Alba::Railtie to set default inflector in Rails apps by trevorturk · Pull Request #274 · okuramasafumi/alba · GitHub

Here’s the Hanami app I’m using to test: bookshelf/Gemfile at main · trevorturk/bookshelf · GitHub

Here’s the terminal output from my testing:

~/Code/hanami/bookshelf (master #) $ hanami console
bookshelf[development]> require 'alba'
RuntimeError: this file is loaded
from /Users/trevorturk/Code/alba/lib/alba/hanami_provider.rb:3:in `<top (required)>'

~/Code/hanami/bookshelf (master #) $ hanami console
bookshelf[development]> require 'alba'
RuntimeError: register_provider is called
from /Users/trevorturk/Code/alba/lib/alba/hanami_provider.rb:6:in `block in <top (required)>'

~/Code/hanami/bookshelf (master #) $ hanami console
bookshelf[development]> require 'alba'
=> true

In this test, I have 3 raise calls where I’m raising an exception to test things out. In the first case, you can see the file is required as expected. In the second case, you can see register_provider is called. In the third case, you can see the prepare block isn’t being called, or else I’d expect to see another exception saying RuntimeError: prepare is called.

Perhaps I’m misunderstanding when the prepare block is expected to be called, though. I’m only just running hanami console and then require "alba" which I thought may be enough to trigger the prepare block being called.

Apologies for all the noise here. I hope I’ve provided enough detail to explain my issue. Please let me know if I can provide anything else, or if there’s something else for me to test, etc.

Thank you!

As far as I can tell, the Hanami console does not finalize your container. So that means the provider is lazy-loaded. You can trigger this provider by running Hanami.app.prepare(:alba) or just Hanami.prepare for all providers.

I think during a full system boot your providers should be started for you. Is that a correct assumption @jodosha?

Thank you for the suggestions! I’m not sure if I’m testing things correctly, but I don’t believe I see the intended behavior here…

$ hanami console

bookshelf[development]> Hanami.app.prepare(:alba)
Dry::System::ProviderNotFoundError: Provider :alba not found

Hanami.prepare
=> Bookshelf::App

bookshelf[development]> require 'alba'
=> true

bookshelf[development]> Alba.inflector
=> nil

So, perhaps it seems like the provider isn’t being set. Keeping in mind, the register_provider call is happening in the gem, but still when I require 'alba' the other exceptions are raised, but the register_provider block doesn’t seem to be called.

I was reading through the Hanami 2.0 docs (congrats!) and found this section: V2.0: Booting | Hanami Guides which helped me understand the system a bit better, and I think we may have things working as expected when we call Hanami.boot:

$ hanami console
bookshelf[development]> require 'alba'
=> true
bookshelf[development]> Alba.inflector
=> nil
bookshelf[development]> Hanami.boot
RuntimeError: prepare is called
from alba/lib/alba/hanami_provider.rb:8

This is what I was hoping to see. And then, to see things working without the debugging exceptions in place:

~/Code/hanami/bookshelf (main) $ hanami console
bookshelf[development]> require 'alba'
=> true
bookshelf[development]> Alba.inflector
=> nil
bookshelf[development]> Hanami.boot
=> Bookshelf::App
bookshelf[development]> Alba.inflector
=> #<Dry::Inflector>

And then I think we’re all set.

So, I think we can chalk this up to me not understanding a few things about Hanami:

  • Hanami’s default app scaffold doesn’t call Bundler.require etc, so you need to require 'alba' somewhere
  • Hanami only makes providers lazy loadable when you call Hanami.prepare but doesn’t fully load them (including calling the prepare block)
  • Hanami.boot fully loads all providers, so we see the behavior I expected

I think this all makes sense architecturally, just a bit less “magic” than Rails but with the benefit of a more flexible and performant system etc.

If anyone has comments or feedback, I’d love any advice you may have. I’ll try to wrap up the PR for the Alba gem, and I hope this conversation will be helpful for other gem authors that want to have Hanami providers built-in.

As a note, I would say that Hanami.prepare not calling the prepare block may be a bit confusing. Perhaps it would be worth considering adjusting the naming, but I understand with 2.0 just released that may be difficult, and I think this is easily attributable to my being new to Hanami generally.

Thanks!

I think this is not quite how you want to develop a third-party provider, this is pretty new stuff so I would not be surprised if this is undocumented outside of a CHANGELOG.

Providers come from dry-system, not from Hanami per se. This means that you really want to write a dry-system provider that can be used by anything built with dry-system, Hanami included.

First, build an actual class instance instead of using the DSL

require "dry/system/provider/source"

module Alba
  class Provider < Dry::System::Provider::Source
    def prepare
      require 'alba'
    end

    def start
      Alba.inflector = target_container["inflector"]
    end
  end
end

Then, you can detect Hanami and inject it automatically. This is the best of both worlds: if someone is not using Hanami but does have dry-system, they can still use this.

if defined?(Hanami)
  Hanami.app.register_provider(:alba, source: Alba::Provider)
end

Ok, that’s great advice, thank you. I’ve updated the Alba PR. (It won’t let me post the link again for some reason, apologies.)

Please do let me know if you have any feedback on the specifics – for example, we could change the name of the file “dry_system_provider” and check if defined?(Dry::System::Provider) – I’m not sure if linguistically we want to stay at the Dry::System level, though, so I kept the names shorter.

Also, please let me know if this would be useful in any Hanami docs. I see the guides are fairly new, and I don’t want to propose a new section for gem authors etc, but it may be worth considering establishing recommended practices for others like me, and I’d be happy to contribute if helpful.

Thanks again!