Creating a Ruby Gem
24 Sep 2014
Today I extracted a separate gem for the core Tic Tac Toe game logic. Up to now, the game itself with all its logic and collaborators was inside one project together with the terminal UI as well as the Qt UI.
Now there is a gem with the core logic, and that gem is used by the existing project that contains the different UI implementations only.
Creating the gem itself was relatively easy. Just executing bundle gem tictactoe
and moving the existing files into the appropriate folders. Adjust tictactoe.gemspec
with some description values. Done. Well… almost. It also forced me to finally use a proper namespace/module hierarchy for the game. Which is a good thing anyway, as I clearly should have done that earlier. There was just no real need for it yet.
Having the overall game(s) now split into two parts lead to another issue: shared examples were not directly available anymore for the UI project.
There are some ways to configure the gem accordingly. I came up with three possible ways:
1. Put everything needed under lib
This could be done relatively easy. But it felt wrong placing test specific classes into lib
.
After the gem would have been required in another project, the difference is not noticeable, whether the exported artifacts were placed in lib
from the beginning or not.
An example .gemspec
file would then just include the regular lib
directory:
Gem::Specification.new do |spec|
# ... other settings left out for brevity
spec.require_paths = ["lib"]
end
But the very idea of placing code from spec
into lib
scared me enough to not do it this way.
2. Export whole the spec
directory
Another way would be to just include the spec
directory in the require_path
setting of the gem.
Gem::Specification.new do |spec|
# ... other settings left out for brevity
spec.require_paths = ["lib", "spec"]
end
The drawback of this is, that all test files and classes from the spec
directory will be available for every client of the gem.
Even when you ignore the fact that this would increase the total file size of your gem, it also just pushes unnecessary code to every client of the gem.
3. Export parts of spec
The third option includes adding just enough additional files to require_paths
so that clients of the gem get what they’re really interested in.
Gem::Specification.new do |spec|
# ... other settings left out for brevity
spec.require_paths = ["lib", "spec/helpers"]
end
I placed the shared examples and helper class in spec/helpers/tictactoe
.
To keep the namespacing behaviour intact the tictactoe
subdirectory is needed.
Since require_paths
includes spec/helpers
now, everything below that should have the proper namespace to not pollute any client’s load path.
This makes it possible that, as a client of the gem, you do not need to be aware that these files are loaded from a directory that is not the gem’s lib
directory originally.
It’s sufficient to require the shared examples like this:
require 'tictactoe/shared_examples'
So for any client the world is good now.
There is one downside though. In the specs for the gem itself, the path for require
can not be written like in the example above. In the specs of the gem, you need to be aware that the file comes from a directory called helpers
. The require
calls in the gem’s spec_helper show that:
require 'helpers/tictactoe/board_helper'
require 'helpers/tictactoe/shared_examples'
You could probably get around that with some $LOAD_PATH
adjustments, but for the moment it didn’t felt so bad to leave it as it is. It’s a relatively focussed place where the path is specified like that – only in the spec_helper
. If I find myself having the need to specify that path in multiple places in the future, I might change the path handling then.