This is one of the final post leading up the the launch of the Build a Ruby Gem Ebook, which is now available for sale in 3 packages, including 14 chapters of code and over 2 hours of screencasts.

The world isn’t black and white (as much as we’d like to believe it is). Just because our gem’s functionality may work for us, doesn’t mean it’ll work for everyone.

Fortunately, we can give users the ability to add custom configuration data, allowing them to adapt our code to their own use. In this post, we’ll adapt the mega_lotto gem to take a configuration block that modifies the number of integers returned from the #draw method output.

Use Case

Our mega_lotto gem provides the functionality to randomly draw 6 numbers. However, let’s assume that someone else has taken interest in the code for our gem, but needs the code to generate 10 random numbers within the same range.

One option would be for them to fork the code and modify the gem to fit their needs. However, now there’s a randomly forked gem with the same name and it’s unclear why one should be used over the other, especially if no changes to the README were made.

Rather than go down that path, we can make our existing mega_lotto gem more flexible by returning 6 integers by default, while also providing an interface to customize this value for those with the need.

Our goal is to adapt our gem to take a configuration block like this:

MegaLotto.configure do |config| config.drawing_count = 10 end

Implementation

Let’s first write some specs for the desired functionality. Because the .configure method above is off the main MegaLotto namespace, we’ll create the spec file spec/mega_lotto_spec.rb . With this spec, we’ll assert that after running the configuration block above, the #drawing method returns an array (like before), but this time with 10 integers instead:

require "spec_helper" describe MegaLotto do describe "#configure" do before do MegaLotto.configure do |config| config.drawing_count = 10 end end it "returns an array with 10 elements" do draw = MegaLotto::Drawing.new.draw expect(draw).to be_a(Array) expect(draw.size).to eq(10) end end end

This spec serves as higher level integration spec because it’s accessing the public API of our gem. Because of this, we can be sure that once this spec passes, our implementation is complete. As expected, when we run this spec, it fails:

MegaLotto #configure returns an array with 10 elements (FAILED - 1) Failures: 1) MegaLotto#config returns an array with 10 elements Failure/Error: MegaLotto.configure do |config| NoMethodError: undefined method `configure` for MegaLotto:Module # ./spec/mega_lotto_spec.rb:6 Finished in 0.00131 seconds 1 example, 1 failure

Now that we have a spec to work against, let’s continue our implementation.

The failure above complained that there was no MegaLotto.configure method, so let’s add it:

module MegaLotto def self.configure end end

Re-running our specs gives us a different failure message this time:

MegaLotto #configure returns an array with 10 elements (FAILED - 1) Failures: 1) MegaLotto#configure returns an array with 10 elements Failure/Error: expect(draw.size).to eq(10) expected: 10 got: 6 (compared using ==) # ./spec/mega_lotto_spec.rb:15 Finished in 0.00246 seconds 1 example, 1 failure

The output now shows that the code still returned 6 integers, which we expected because our .configure method hasn’t done anything yet.

Because we’re using a object-oriented language like Ruby, we can create a Configuration class whose responsibility will be…(drumroll)….configuration!

Let’s start with some specs:

# spec/mega_lotto/configuration_spec.rb require "spec_helper" module MegaLotto describe Configuration do describe "#drawing_count" do it "default value is 6" do Configuration.new.drawing_count = 6 end end describe "#drawing_count=" do it "can set value" do config = Configuration.new config.drawing_count = 7 expect(config.drawing_count).to eq(7) end end end end

Running the configuration specs produces:

/Users/bhilkert/Dropbox/code/mega_lotto/spec/ mega_lotto/configuration_spec.rb:4:in `<module:MegaLotto>`: uninitialized constant MegaLotto::Configuration (NameError)

Let’s add the Configuration class:

# lib/mega_lotto/configuration.rb module MegaLotto class Configuration end end

Let’s try again:

/Users/bhilkert/Dropbox/code/mega_lotto/spec/ mega_lotto/configuration_spec.rb:4:in `<module:MegaLotto>`: uninitialized constant MegaLotto::Configuration (NameError)

What??? Same message…Even though we added the Configuration class above, our gem doesn’t load it. So we can dive in to the entry file lib/mega_lotto.rb and add the appropriate require statement:

require "mega_lotto/version" require "mega_lotto/configuration" require "mega_lotto/drawing" begin require "pry" rescue LoadError end module MegaLotto def self.configure end end

Now with the Configuration class properly loaded, let’s run our specs again:

MegaLotto::Configuration #drawing_count default value is 6 (FAILED - 1) #drawing_count= can set value (FAILED - 2) Failures: 1) MegaLotto::Configuration#drawing_count default value is 6 Failure/Error: expect(config.drawing_count).to eq(6) NoMethodError: undefined method `drawing_count` for #<MegaLotto::Configuration> # ./spec/mega_lotto/configuration_spec.rb:8 2) MegaLotto::Configuration#drawing_count= can set value Failure/Error: config.drawing_count = 7 NoMethodError: undefined method `drawing_count=` for #<MegaLotto::Configuration> # ./spec/mega_lotto/configuration_spec.rb:15 Finished in 0.00175 seconds 2 examples, 2 failures

Even though we still have failures, we’re making progress. The failures above relate to the lack of a #drawing_count= method, so let’s add an accessor for it:

module MegaLotto class Configuration attr_accessor :drawing_count end end

Note: We could’ve just added an attr_writer to satisfy the spec. However, I know I’m going to need a getter down the road, so I chose to do it at the same time.

With our accessor in place, let’s check the specs again:

MegaLotto::Configuration #drawing_count= can set value #drawing_count default value is 6 (FAILED - 1) Failures: 1) MegaLotto::Configuration#drawing_count default value is 6 Failure/Error: expect(config.drawing_count).to eq(6) expected: 6 got: nil (compared using ==) # ./spec/mega_lotto/configuration_spec.rb:8 Finished in 0.00239 seconds 2 examples, 1 failure

Still a failure, but we’re slowly making more progress. The default value isn’t getting set so we’ll change that in the implementation:

module MegaLotto class Configuration attr_accessor :drawing_count def initialize @drawing_count = 6 end end end

Running the specs one more time for the Configuration class shows that we’re good:

MegaLotto::Configuration #drawing_count default value is 6 #drawing_count= can set value Finished in 0.00172 seconds 2 examples, 0 failures

Running the specs for the main spec/mega_lotto.rb class again:

MegaLotto #configure returns an array with 10 elements (FAILED - 1) Failures: 1) MegaLotto#configure returns an array with 10 elements Failure/Error: expect(draw.size).to eq(10) expected: 10 got: 6 (compared using ==) # ./spec/mega_lotto_spec.rb:15:in `block (3 levels) in <top (required)>' Finished in 0.00168 seconds 1 example, 1 failure

We still have the same failures from before, but it’s because we didn’t change the MegaLotto::Drawing to actually use the new configuration class. Since we have this awesome new class, let’s make use of it in MegaLotto::Drawing :

module MegaLotto class Drawing def draw MegaLotto.configuration.drawing_count.times.map { single_draw } end private def single_draw rand(0...60) end end end

Running the specs for the drawing class gives us the following output:

MegaLotto::Drawing #draw each element is less than 60 (FAILED - 1) each element is an integer (FAILED - 2) returns an array (FAILED - 3) using the default drawing count returns an array with 6 elements (FAILED - 4) Failures: 1) MegaLotto::Drawing#draw each element is less than 60 Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw } NoMethodError: undefined method `configuration` for MegaLotto:Module # ./lib/mega_lotto/drawing.rb:4:in `draw' # ./spec/mega_lotto/drawing_spec.rb:6 # ./spec/mega_lotto/drawing_spec.rb:19 2) MegaLotto::Drawing#draw each element is an integer Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw } NoMethodError: undefined method `configuration` for MegaLotto:Module # ./lib/mega_lotto/drawing.rb:4:in `draw' # ./spec/mega_lotto/drawing_spec.rb:6 # ./spec/mega_lotto/drawing_spec.rb:13 3) MegaLotto::Drawing#draw returns an array Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw } NoMethodError: undefined method `configuration` for MegaLotto:Module # ./lib/mega_lotto/drawing.rb:4:in `draw' # ./spec/mega_lotto/drawing_spec.rb:6 # ./spec/mega_lotto/drawing_spec.rb:9 4) MegaLotto::Drawing#draw using the default drawing count returns an array with 6 elements Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw } NoMethodError: undefined method `configuration` for MegaLotto:Module # ./lib/mega_lotto/drawing.rb:4:in `draw' # ./spec/mega_lotto/drawing_spec.rb:6 # ./spec/mega_lotto/drawing_spec.rb:26 Finished in 0.00219 seconds 4 examples, 4 failures

Well…I guess it’s clear that it doesn’t have a configuration accessor, huh? Let’s add it to lib/mega_lotto.rb :

module MegaLotto class << self attr_accessor :configuration end def self.configure end end

and our specs:

MegaLotto::Drawing #draw each element is less than 60 (FAILED - 1) each element is an integer (FAILED - 2) returns an array (FAILED - 3) using the default drawing count returns an array with 6 elements (FAILED - 4) Failures: 1) MegaLotto::Drawing#draw each element is less than 60 Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw } NoMethodError: undefined method `drawing_count` for nil:NilClass # ./lib/mega_lotto/drawing.rb:4:in `draw' # ./spec/mega_lotto/drawing_spec.rb:6 # ./spec/mega_lotto/drawing_spec.rb:19 2) MegaLotto::Drawing#draw each element is an integer Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw } NoMethodError: undefined method `drawing_count` for nil:NilClass # ./lib/mega_lotto/drawing.rb:4:in `draw' # ./spec/mega_lotto/drawing_spec.rb:6 # ./spec/mega_lotto/drawing_spec.rb:13 3) MegaLotto::Drawing#draw returns an array Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw } NoMethodError: undefined method `drawing_count` for nil:NilClass # ./lib/mega_lotto/drawing.rb:4:in `draw' # ./spec/mega_lotto/drawing_spec.rb:6 # ./spec/mega_lotto/drawing_spec.rb:9 4) MegaLotto::Drawing#draw using the default drawing count returns an array with 6 elements Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw } NoMethodError: undefined method `drawing_count` for nil:NilClass # ./lib/mega_lotto/drawing.rb:4:in `draw' # ./spec/mega_lotto/drawing_spec.rb:6 # ./spec/mega_lotto/drawing_spec.rb:26 Finished in 0.00146 seconds 4 examples, 4 failures

A different message this time, related to the fact that the configuration accessor has no #drawing_count method. This makes sense because we don’t actually return anything from #configuration . Let’s instantiate a new Configuration object and see where that gets us:

module MegaLotto class << self attr_writer :configuration end def self.configuration Configuration.new end def self.configure end end

Now, the Drawing class specs are passing:

MegaLotto::Drawing #draw each element is an integer each element is less than 60 returns an array using the default drawing count returns an array with 6 elements Finished in 0.01007 seconds 4 examples, 0 failures

Let’s flip back to the spec file spec/mega_lotto_spec.rb and see where we are:

MegaLotto #configure returns an array with 10 elements (FAILED - 1) Failures: 1) MegaLotto#configure returns an array with 10 elements Failure/Error: expect(draw.size).to eq(10) expected: 10 got: 6 (compared using ==) # ./spec/mega_lotto_spec.rb:15 Finished in 0.00167 seconds 1 example, 1 failure

Still failing, but at least we have what seems like the pieces setup to implement the global configuration. The .configure methods needs to yield the configuration block to a new instance of the Configuration class. However, we’ll need to memoize the configuration instance, so when the Drawing class accesses #drawing_count , it returns the initial configuration value:

module MegaLotto class << self attr_writer :configuration end def self.configuration @configuration ||= Configuration.new end def self.configure yield(configuration) end end

Note: it’s important to return the class instance variable from .configuration and check if that is set rather than the reader method, otherwise it’d get stuck in a loop and never return.

Running our specs again, we see that we’re green:

MegaLotto #configure returns an array with 10 elements Finished in 0.00168 seconds 1 example, 0 failures

For the sake of sanity, let’s run the whole suite to make sure everything is covered:

$ rake ....... Finished in 0.00688 seconds 7 examples, 0 failures

…and we’re good! Except, if we run our entire suite a few times in a row, we’ll eventually see a failure:

Failures: 1) MegaLotto::Drawing#draw returns an Array with 6 elements Failure/Error: expect(drawing.size).to eq(6) expected: 6 got: 10 (compared using ==) # ./spec/mega_lotto/drawing_spec.rb:13 Finished in 0.00893 seconds 7 examples, 1 failure

What’s going on???

In the setup of the spec for MegaLotto.configure , we added the following before block:

before :each do MegaLotto.configure do |config| config.drawing_count = 10 end end

Because this configuration is global, if this spec is run before the others in our suite, the remaining specs will use it. So when the specs for MegaLotto::Drawing run, 10 elements are return instead of the 6, the default, and we see the failure.

For global values like this, it’s best to clean up after each spec to ensure the system is back to a default state. In our case, we can implement a .reset method on MegaLotto and set the configuration back to a new instance of the Configuration class. Let’s start with a spec for this in spec/mega_lotto_spec.rb :

describe ".reset" do before :each do MegaLotto.configure do |config| config.drawing_count = 10 end end it "resets the configuration" do MegaLotto.reset config = MegaLotto.configuration expect(config.drawing_count).to eq(6) end end

As expected, we see failure because we have yet to implement the .reset method:

Failures: 1) MegaLotto.reset resets the configuration Failure/Error: MegaLotto.reset NoMethodError: undefined method `reset` for MegaLotto:Module # ./spec/mega_lotto_spec.rb:28 Finished in 0.00762 seconds 8 examples, 1 failure

Let’s do that now:

module MegaLotto class << self attr_writer :configuration end def self.configuration @configuration ||= Configuration.new end def self.reset @configuration = Configuration.new end def self.configure yield(configuration) end end

Our specs for the .reset method pass, so now we need to make use of it to clean up after our .configure spec:

describe "#configure" do before :each do MegaLotto.configure do |config| config.drawing_count = 10 end end it "returns an array with 10 elements" do draw = MegaLotto::Drawing.new.draw expect(draw).to be_a(Array) expect(draw.size).to eq(10) end after :each do MegaLotto.reset end end

Now we can be sure that our specs pass no matter the order of execution.

Local Configuration

The configuration approach above implements a global configuration object. The downside is that we can’t have multiple instances of our code running with different configurations. To avoid this, we could isolate the configuration class and only pass it to those objects that need it. By doing this, we’d avoid needing the MegaLotto.configure method entirely.

With this in mind, Drawing might look like:

module MegaLotto class Drawing attr_accessor :config def initialize(config = Configuration.new) @config = config end def draw config.drawing_count.times.map { single_draw } end private def single_draw rand(0...60) end end end

We can supply our own configuration object during instantiation if the defaults aren’t appropriate. In this case, as long as the object responds to drawing_count , everything will work.

require 'ostruct' config = OpenStruct.new(drawing_count: 10) MegaLotto::Drawing.new(config).draw #=> [23, 4, 21, 33, 48, 12, 43, 13, 2, 5]

Both approaches are certainly valid, so I’ll leave it to you to decide which approach is best for your gem.

Implementations in the Wild

The CarrierWave gem is a popular choice to support avatar uploading. The author(s) realized that not everyone would want to store upload assets on the local system, so they offered the functionality to support Amazon S3 and other similar block storage services. In order to set this value, you’d use a configure block almost identical to the one we wrote above.

Thoughtbot wrote a great article about the configuration implementation in their Clearance gem. It’s worth reading even if you don’t plan to use Clearance.

Summary

Keeping gems configurable means balancing your use case with the use cases of others. The more flexibility you offer to users of your gem, the more users will find value in your work. However, there’s a point when offering too much configuration can make the internals of a gem unnecessarily complicated. As you probably know, Ruby is a language full of conventions and it’s best to provide reasonable defaults and only adapt if the need arises.

One approach to balance complexity is to create a system where users can write their own middleware to modify the behavior of the default system. Mike Perham created a middleware system for Sidekiq allowing users to add functionality as they wish. Doing so doesn’t require the gem to change at all when unique use cases arise. The implementation of such system is beyond the scope of this book. However, if you want to learn more, Sidekiq’s implementation is a great place to start.