Make a Ruby gem configurable
Tags: ruby, rails, tutorial, webdev
Published: 2020-07-21 13:32:50

In the Ruby ecosystem, a well established way of customizing gem behavior is by using configuration blocks. Most likely, you came across code that looked like this.

```ruby
MyGem.configure do |config|
  config.some_config = true
  config.some_class = MyApp
  config.some_lambda = ->(variable) { do_important_stuff(variable) }
end
```

This standard way of configuring gems runs once, usually when starting an application, and allows developers to tailor the gem's functionality to their needs.

# Adding the configuration block to a gem

Let's take a look at how this style of configuration can be implemented. We'll divide our implementation in two steps: writing a configuration class and exposing it to users.

## Writing the configuration class

The configuration class is responsible for keeping all the available options and their defaults. It is composed of two things: an initialize method where defaults are set and attribute readers/writers.

Let's take a look at an example, where we can configure an animal and the sound that it makes. If the sound does not match the expected, we want to raise an error while configuring our gem. We'll then access what is configured for animal and sound and use it in our logic.

```ruby
# lib/my_gem/configuration.rb

module MyGem
  class Configuration
    # Custom error class for sounds that don't
    # match the expected animal
    class AnimalSoundMismatch < StandardError; end

    # Animal sound map
    ANIMAL_SOUND_MAP = {
      "Dog" => "Barks",
      "Cat" => "Meows"
    }
    
    # Writer + reader for the animal instance variable. No fancy logic
    attr_accessor :animal

    # Reader only for the sound instance variable.
    # The writer contains custom logic
    attr_reader :sound

    # Initialize every configuration with a default.
    # Users of the gem will override these with their
    # desired values
    def initialize
      @animal = "Dog"
      @sound = "Barks"
    end

    # Custom writer for sound.
    # If the sound variable is not exactly what is
    # mapped in our hash, raise the custom error
    def sound=(sound)
      raise AnimalSoundMismatch, "A #{@animal} can't #{sound}" if SOUND_MAP[@animal] != sound
      
      @sound = sound
    end
  end
end
```

## Exposing the configuration

To expose the configuration means both letting users configure it and allowing the gem itself to read the value of each option.

This is done with a singleton of our Configuration class and a few utility methods.

```ruby
# lib/my_gem.rb

module MyGem
  class << self
    # Instantiate the Configuration singleton
    # or return it. Remember that the instance
    # has attribute readers so that we can access
    # the configured values
    def configuration
      @configuration ||= Configuration.new
    end

    # This is the configure block definition.
    # The configuration method will return the
    # Configuration singleton, which is then yielded
    # to the configure block. Then it's just a matter
    # of using the attribute accessors we previously defined
    def configure
      yield(configuration)
    end
  end
end
```

The gem is now set to be configured by the applications that use it.

```ruby
MyGem.configure do |config|
  # Notice that the config block argument
  # is the yielded singleton of Configuration.
  # In essence, all we're doing is using the
  # accessors we defined in the Configuration class
  config.animal = "Cat"
  config.sound = "Meows"
end
```

# Using the configuration in the gem

Now that we have the customizable Configuration singleton, we can read the values to change behavior based on it.

```ruby
# lib/my_gem/make_sound.rb

module MyGem
  class AnimalSound
    def initialize
      @animal = MyGem.configuration.animal
      @sound = MyGem.configuration.sound
    end

    def make_sound
      "The #{@animal} #{@sound}"
    end
  end
end
```

# Using lambdas for configuration

In specific scenarios, there may be a need for a configuration to not have a predetermined value, but rather to evaluate some logic as the application is running.

For these cases, it is typical to define a lambda for the configuration value. Let's go through an example. The configuration class is similar to our previous case.

```ruby
# lib/my_gem/configuration.rb

module MyGem
  class Configuration
    attr_reader :key_name

    # Define no lambda as the default
    def initialize
      @key_name = nil
    end

    # Raise an error if trying to set the key_name
    # to something other than a lambda
    def key_name=(lambda)
      raise ArgumentError, "The key_name must be a lambda" unless lambda.is_a?(Proc)

      @key_name = lambda
    end
  end
end
```

Now we can configure the lambda to whatever we need. You could even query the database if desired inside the lambda and return values from the app's models.

```ruby
MyGem.configure do |config|
  config.lambda_config = ->(model_name) { model_name == "Post" ? :posts : :articles }
end
```

Finally, the gem can use the lambda and get different results as the app is running.

```ruby
MyGem.configuration.key_name&.call("Article")
=> :articles

MyGem.configuration.key_name&.call("Post")
=> :posts
```

That's about it for configuration blocks. Have you used this before to make your code/gems customizable? Do you know other strategies for configuring third party libraries? Let me know in the comments!