Adding functionality to a gem - Creating a gem from scratch Part 3

This is part three of a series on creating your own gem. This part will cover filling in the functionality of our gem and installing it.

You can find the other parts of the series here: Part 1, Part 2

Modifying our dicebag_wrapper_example.rb file

The first step of adding our gems functionality is to modify our dicebag_wrapper_example.rb file to include the files within out dicebag_wrapper_example directory.

The current setup manually includes the version.rb file and then the functionality of the gem is written inside the rest of the file. Whilst this would be an acceptable setup for our current level of complexity I want to demonstrate a more extendable method for a moderately complex gem.

We will replace the default file (as shown below)

lib/dicebag_wrapper_example.rb

require "dicebag_wrapper_example/version"

module DicebagWrapperExample
  # Your code goes here...
end

with this one

lib/dicebag_wrapper_example.rb

require 'dicebag'

mydir = File.expand_path(File.dirname(__FILE__))

Dir[mydir + '/dicebag_wrapper_example/*.rb'].each {|file| require file }

This new setup includes the Dicebag gem, as it provides the core functionality of the gem, and then includes every .rb file in the dicebag_wrapper_example directory. This directory is where we'll be putting our gem's classes.

Adding our Gem's Functionality

To add our gem's functionality we first need a place to put it. As we saw above we will be putting our functionality in the lib/dicebag_wrapper_example directory, alongside the version.rb file. So first, create a roll.rb file there. Next add the following to it:

lib/dicebag_wrapper_example/roll.rb

module DicebagWrapperExample
  class << self

  end
end

We'll be putting our functions inside class << self. This will mean our functions will be called as follows:

2.0.0-p353 :002 > DicebagWrapperExample.roll('1d6')
2.0.0-p353 :002 > DicebagWrapperExample.d6(1)

As we have no need to encapsulate our methods having all the methods under the DicebagWrapperExample module is fine. However, if our gem had other functionality and we wanted to move them into their own class we would set the file up like this:

module DicebagWrapperExample
  class Roll
    class << self

    end
  end
end

This means that our methods would be called like this:

2.0.0-p353 :002 > DicebagWrapperExample::Roll.roll('1d6')
2.0.0-p353 :002 > DicebagWrapperExample::Roll.d6(1)

Now back to adding functionality.

Okay so now that our file exists we can stub our two methods and start moving along with getting those tests passing.

lib/dicebag_wrapper_example/roll.rb

module DicebagWrapperExample
  class << self

    def roll(dice_string)
    end

    def d6(count)
    end

  end
end

Our gem now has our two functions. If we run our tests now we'll still get four failures, but it won't be due to undefined method errors like last time. Rather, we're failing because both methods are just returning nil.

$ rspec spec
FFFF

Failures:

  1) DicebagWrapperExample .d6 returns a value from a d6
     Failure/Error: expect(roll).to be >= 1
     NoMethodError: undefined method `>=' for nil:NilClass

  2) DicebagWrapperExample .d6 when sent a non-integer throws an error that mentions the method name
     Failure/Error: expect { DicebagWrapperExample.d6(0.1) }.to raise_error(TypeError, "DicebagWrapperExample.d6 requires an Integer")
       expected TypeError with "DicebagWrapperExample.d6 requires an Integer" but nothing was raised

  3) DicebagWrapperExample .roll returns an integer
     Failure/Error: expect(roll.is_a? Integer).to be_true
       expected false to respond to `true?`

  4) DicebagWrapperExample .roll with the string '1d6' sent returns a value from a single d6 being rolled
     Failure/Error: expect(roll).to be >= 1
     NoMethodError: undefined method `>=' for nil:NilClass

Finished in 0.00401 seconds (files took 0.17278 seconds to load)
4 examples, 4 failures

Let's get .roll returning the correct data.

As we mentioned in our last post Dicebag's method of retrieving a single diceroll result can be convoluted when all you want is a value. That's where roll comes in, we're going to package up retrieving a value and abstract away all those details.

This is how we'll do it:

def roll(dice_string)
  dice = DiceBag::Roll.new(dice_string)

  dice.result.total
end

Our roll.rb file now looks like this:

lib/dicebag_wrapper_example/roll.rb

module DicebagWrapperExample
  class << self

    def roll(dice_string)
      dice = DiceBag::Roll.new(dice_string)

      dice.result.total
    end

    def d6(count)
    end

  end
end

Now that .roll is implemented, let's run those tests again.

``bash
$ rspec spec
FF..

Failures:

1) DicebagWrapperExample .d6 returns a value from a d6
Failure/Error: expect(roll).to be >= 1
NoMethodError: undefined method `>=' for nil:NilClass

2) DicebagWrapperExample .d6 when sent a non-integer throws an error that mentions the method name
Failure/Error: expect { DicebagWrapperExample.d6(0.1) }.to raise_error(TypeError, "DicebagWrapperExample.d6 requires an Integer")
expected TypeError with "DicebagWrapperExample.d6 requires an Integer" but nothing was raised

Finished in 1.99 seconds (files took 0.17374 seconds to load)
4 examples, 2 failures
```

Only two failures now. Let's implement .d6 and get all these passing.

First off we'll just get the method returning the correct values, we'll ignore the TypeError requirement for now.

.d6 is essentially another layer of abstraction, this time on top of .roll, we'll use string interpolation to create a dice_string and pass that through to .roll and return the value it gives us.

Our method will, at this stage, look like this:

def d6(count)
  roll("#{count}d6")
end

roll.rb will now look like this:

lib/dicebag_wrapper_example/roll.rb

module DicebagWrapperExample
  class << self

    def roll(dice_string)
      dice = DiceBag::Roll.new(dice_string)

      dice.result.total
    end

    def d6(count)
      roll("#{count}d6")
    end

  end
end

Let's save and run those tests.

$ rspec spec
.F..

Failures:

  1) DicebagWrapperExample .d6 when sent a non-integer throws an error that mentions the method name
     Failure/Error: expect { DicebagWrapperExample.d6(0.1) }.to raise_error(TypeError, "DicebagWrapperExample.d6 requires an Integer")
       expected TypeError with "DicebagWrapperExample.d6 requires an Integer", got #<DiceBag::DiceBagError: Dice Parse Error for string: 0.1d6> with backtrace:
         # ./lib/dicebag_wrapper_example/roll.rb:5:in `new'
         # ./lib/dicebag_wrapper_example/roll.rb:5:in `roll'
         # ./lib/dicebag_wrapper_example/roll.rb:11:in `d6'
         # ./spec/roll_spec.rb:17:in `block (5 levels) in <top (required)>'
         # ./spec/roll_spec.rb:17:in `block (4 levels) in <top (required)>'
     # ./spec/roll_spec.rb:17:in `block (4 levels) in <top (required)>'

Finished in 3.77 seconds (files took 0.16437 seconds to load)
4 examples, 1 failure

Now we just need to make .d6 check that it's received an integer. We'll do that via .is_a? Integer, grammar aside, it'll be perfect.

lib/dicebag_wrapper_example/roll.rb

module DicebagWrapperExample
  class << self

    def roll(dice_string)
      dice = DiceBag::Roll.new(dice_string)

      dice.result.total
    end

    def d6(number)
      if number.is_a? Integer
        roll("#{number}d6")
      else
        raise TypeError, "DicebagWrapperExample requires an Integer"
      end
    end

  end
end

Now we run our tests.

$ rspec spec
....

Finished in 3.96 seconds (files took 0.17224 seconds to load)
4 examples, 0 failures

Great! Our tests are all passing.

Incrementing our version number

With our functionality complete let's increment our version number to version 1.0.0, our release candidate.

lib/dicebag_wrapper_example/version.rb

module DicebagWrapperExample
  VERSION = "1.0.0"
end

Installing our gem and seeing it run in the ruby console

Our gem now has the functionality we set out to create. Let's install it and see it in all its glory.

We'll be installing the gem in the same way as our first post.

First, remember that our gemspec file includes our files in the gem by getting a list from git ls-files so make sure you've staged and/or commit all the new files we've created to the gem's git repository.

After you've done that run the rake install command. This will create our gem in the pkg directory and automatically install the gem. Now let's go into the ruby console and see our gem in action.

$ rake install
dicebag_wrapper_example 1.0.0 built to pkg/dicebag_wrapper_example-1.0.0.gem.
dicebag_wrapper_example (1.0.0) installed.

$ irb

2.0.0-p353 :001 > require 'dicebag_wrapper_example'
 => true

2.0.0-p353 :002 > DicebagWrapperExample::VERSION # Check that we've got the correct version installed and required
 => "1.0.0"

2.0.0-p353 :003 > DicebagWrapperExample.roll('1d6') # Check .roll's basic functionality
 => 5

2.0.0-p353 :004 > DicebagWrapperExample.roll('3d6 + 1d4 + 2d10') # Check that more complicated dice strings work
 => 22

2.0.0-p353 :005 > DicebagWrapperExample.d6(2)
 => 6

2.0.0-p353 :006 > DicebagWrapperExample.d6(1.2) # Check that throwing an error works correctly
TypeError: DicebagWrapperExample.d6 requires an Integer
  from /Users/Lockyy/.rvm/gems/ruby-2.0.0-p353/gems/dicebag_wrapper_example-1.0.0/lib/dicebag_wrapper_example/roll.rb:14:in `d6'
  from (irb):8
  from /Users/Lockyy/.rvm/rubies/ruby-2.0.0-p353/bin/irb:12:in `<main>'

2.0.0-p353 :007 > DicebagWrapperExample.d6('1d6') # Check that throwing an error works correctly
TypeError: DicebagWrapperExample.d6 requires an Integer
  from /Users/Lockyy/.rvm/gems/ruby-2.0.0-p353/gems/dicebag_wrapper_example-1.0.0/lib/dicebag_wrapper_example/roll.rb:14:in `d6'
  from (irb):9
  from /Users/Lockyy/.rvm/rubies/ruby-2.0.0-p353/bin/irb:12:in `<main>'

Our gem is installed and works great when tested manually.

If you want to run the tests on your installed gem you can do so like this

$ rspec ~/.rvm/gems/ruby-2.0.0-p353/gems/dicebag_wrapper_Example-1.0.0/spec

....

Finished in 3.85 seconds (files took 0.18074 seconds to load)
4 examples, 0 failures

This shows that our installed gem also passes all its tests.

If you want to distribute your gem you're packaged and ready to use gem is in the pkg folder. The next post will deal with putting your gem on rubygems.org so the world can use your gem.

Thanks for subscribing!
Like what I'm doing? Subscribe and I'll let you know when I write new stuff.
Want to unsubscribe?
Subscribe