DEV Community

Cover image for Making Your Ruby Gem Configurable
Rodrigo Walter Ehresmann
Rodrigo Walter Ehresmann

Posted on • Edited on

Making Your Ruby Gem Configurable

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.


Options if you like my content and would like to support me directly (never required, but much appreciated):

BTC address: bc1q5l93xue3hxrrwdjxcqyjhaxfw6vz0ycdw2sg06

buy me a coffee

Top comments (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

Nice! Happy to help (: