DEV Community

loading...
Cover image for Making Your Ruby Gem Configurable

Making Your Ruby Gem Configurable

Rodrigo Walter Ehresmann
Sofware Developer with a passion for problem-solving. English learner.
・5 min read

Ruby gems are pretty much a module. It's not a big deal to us to declare a module, but how do we create those richest configuration files we usually have in /initializers folder of a Ruby on Rails application? Let's check it out.

First of all, there are many ways to make your gem configurable. I'll present you one, and further, we'll give it a look at how other gems implement it.

Example of implementation

I'll call my example gem gsdk. You can assume the gem structure is the default of bundle gem gsdk (Bundler version 1.17.3), but this isn't important for the context of this post, it's just to give you some orientation. This gem will have a token and a secret key I'd like to inform as configuration, so let's put that in code:

# gsdk/lib/gsdk/configuration

module Gsdk
  class Configuration
    attr_accessor :token, :secret_key

    def initialize(token = nil, secret_key = nil)
      @token = token
      @secret_key = secret_key
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

As simple as that, we have the configuration class that makes token and secret key available through attr_accessor.

Gsdk is a module, but the Configuration is a class instance. How do we make the instance available to be used anywhere, by any class that exists inside the Gsdk module?

module Gsdk
  class << self
    attr_accessor :configuration
  end

  def self.configuration
    @@configuration ||= Configuration.new
  end
end
Enter fullscreen mode Exit fullscreen mode

It's a matter of scope. Although it's a module, modules are implemented as classes (Module.class == Class) and can make use of instance or class variables too. However, we need to inform that to the module, and we do so in a code block inside the class << self. The block code is enough by itself, but I wanna initialize with an empty Configuration instance if configuration is accessed, that's why we have self.configuration.

Now it is possible to configure the gem and make the token and secret key available in the module.

module Gsdk
  class Caller
    def call
      puts "Token: #{Gsdk.configuration.token}"
      puts "Secret key: #{Gsdk.configuration.secret_key}"
    end
  end
end
Enter fullscreen mode Exit fullscreen mode
(base) ➜  gsdk git:(master) ✗ ./bin/console
2.6.6 :001 > configuration = Gsdk::Configuration.new('my_token', 'my_secret_key')
 => #<Gsdk::Configuration:0x000055b8ada3d638 @token="my_token", @secret_key="my_secret_key"> 
2.6.6 :002 > Gsdk.configuration = configuration
 => #<Gsdk::Configuration:0x000055b8ada3d638 @token="my_token", @secret_key="my_secret_key"> 
2.6.6 :003 > Gsdk::Caller.new.call
Token: my_token
Secret key: my_secret_key
 => nil 
Enter fullscreen mode Exit fullscreen mode

The gem is already configurable, but we still missing what we proposed to answer in the first paragraph of this post: how to do what was shown above but in a configuration block. Now we know that it's a matter of scope, the answer should be easier to understand :

module Gsdk

...

  def self.configure
    yield(configuration)
  end
end
Enter fullscreen mode Exit fullscreen mode

The class method self.configure allows us to achieve our goal:

Gsdk.configure do |config|
  config.token = "your_token"
  config.secret_key = "your_secret_key"
end
Enter fullscreen mode Exit fullscreen mode
(base) ➜  gsdk git:(master) ✗ ./bin/console
2.6.6 :001 > Gsdk.configure do |config|
2.6.6 :002 >       config.token = "your_token"
2.6.6 :003?>     config.secret_key = "your_secret_key"
2.6.6 :004?>   end
 => "your_secret_key" 
2.6.6 :005 > Gsdk.configuration
 => #<Gsdk::Configuration:0x000055f935a2e818 @token="your_token", @secret_key="your_secret_key"> 
2.6.6 :006 > 
Enter fullscreen mode Exit fullscreen mode

With yield we're calling the empty Configuration instance (config.class == Gsdk::Configuration), that uses attr_accessor for token and secret_key, making them available in the code block.

Implementation alternatives

Let's check how big and consolidated gems achieve roughly the same we did above. To identify this we can check two things: (1) where is the method that accepts a configuration block, and (2) how a configuration option is set. The place to get the name of the method and the configuration options is the configuration file usually pointed out in the gem's documentation.

Devise.setup do |config|
...

config.password_length = 6..128

...
end
Enter fullscreen mode Exit fullscreen mode

This is part of the configuration file generated by Devise using their rails generate devise:install. Looking into lib/devise.rb:

module Devise

...

  mattr_accessor :password_length
  @@password_length = 6..128

...

  def self.setup
    yield self
  end

...
end
Enter fullscreen mode Exit fullscreen mode

Devise uses Rails specific mattr_accessor that provides getters and setters in a class/module level, and self.setup is the equivalent of our self.configure implemented in the previous section.

We could achieve the same as Devise eliminating the use of Gsdk::Configuration and changing our module to:

module Gsdk
  class << self
    attr_accessor :token, :secret_key
  end

  def self.configure
    yield self
  end
end
Enter fullscreen mode Exit fullscreen mode
(base) ➜  gsdk git:(master) ✗ ./bin/console
2.6.6 :001 > Gsdk.configure do |config|
2.6.6 :002 >       config.token = "your_token"
2.6.6 :003?>     config.secret_key = "your_secret_key"
2.6.6 :004?>   end
 => "your_secret_key" 
2.6.6 :005 > Gsdk.secret_key
 => "your_secret_key" 
2.6.6 :006 > Gsdk.token
 => "your_token" 
Enter fullscreen mode Exit fullscreen mode
  Sidekiq.configure_server do |config|
    config.redis = { url: ENV.fetch('REDIS_URL') }
  end
Enter fullscreen mode Exit fullscreen mode

This is part of Sidekiq configuration I have in an initializer of a Rails project. Looking into lib/sidekiq.rb:

...

module Sidekiq

...

  def self.redis
    ...
  end

  def self.redis=(hash)
    ...
  end

  def self.configure_server
    yield self if server?
  end

...

end
Enter fullscreen mode Exit fullscreen mode

In Sidekiq they explicitly implement getters and setters, which we saw being accomplished before using attr_accessor and mattr_accessor. Once again, we can change our implementation to work in the same manner:

module Gsdk
  def self.token
    @token
  end

  def self.token=(token)
    @token = token
  end

  def self.secret_key
    @secret_key
  end

  def self.secret_key=(secret_key)
    @secret_key = secret_key
  end

  def self.configure
    yield self
  end
end
Enter fullscreen mode Exit fullscreen mode
(base) ➜  gsdk git:(master) ✗ ./bin/console
2.6.6 :001 > Gsdk.configure do |config|
2.6.6 :002 >       config.token = "your_token"
2.6.6 :003?>     config.secret_key = "your_secret_key"
2.6.6 :004?>   end
 => "your_secret_key" 
2.6.6 :005 > Gsdk.token
 => "your_token" 
2.6.6 :006 > Gsdk.secret_key
 => "your_secret_key" 
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this post, we saw how to make a gem configurable. We implemented our configuration strategy and further checked how Devise and Sidekiq gems are implemented to be configurable.

All three implementations explored in this post are different one from another, but they summed up to getters and setters. You can explicitly implement getters and setters on your own or use something like attr_accessor; make use of instance variables or class variables.

Which one is the best implementation strategy? My response would be: don't overthink this. You can argue that to use mattr_accessor you need an extra dependency, or that instance variables in modules are simply poor design. But the point is that we're talking about something trivial, so start with what you think is the best for you.


This is it! If you have any comments or suggestions, don't hold back, let me know.

Discussion (2)

Collapse
cescquintero profile image
Francisco Quintero 🇨🇴

Wow, such nice timing for you posting this 😁

I'm working on something and was figuring out how to code a configuration block like Devise or other gems. This is gold.

Thanks for sharing!

Collapse
rwehresmann profile image
Rodrigo Walter Ehresmann Author

Nice! Happy to help (: