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
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
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
(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
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
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
(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 >
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
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
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
(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"
Sidekiq.configure_server do |config|
config.redis = { url: ENV.fetch('REDIS_URL') }
end
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
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
(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"
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
Top comments (2)
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!
Nice! Happy to help (: