I love Ruby. I’ve also, unrelated to my love of Ruby, had a paralyzing fear of sharing my Ruby with anyone else. Probably because I’m so emotionally attached to it? Anyways. I got over it and started publishing my first Ruby gem: a Ruby wrapper for the Ravelry API. Here’s the basics of how I set up and published my first gem, as well as a step-by-step guide for making it configurable.
This is the first post I’ve written on configurable Ruby gems. You can read the second post here: Configurable Ruby gems: Custom error messages and testing.
If you don’t want to read through the whole post and just want to get the code, a gist is available here.
Bundler + RubyGems = Happy gems!
I opted to use Bundler to manage everything about my
*.gemspec file and gem business, as well as handle local development. There’s a lovely guide here on how everything works, but I’ll go over the basics.
In the examples below, I’m using
ravelry as my gem name. You’ll replace this with your gem name.
Bundler: Create your gem
This command will create a new directory with a Git repository, as well as the required
Before you get super attached to your gem’s name, make sure the name isn’t taken by searching RubyGems.org.
bundle gem ravelry
Bundler: Build your gem
When you’re ready to build the gem and publish it, or if you want to test a specific version of a gem in an isolated environment, update the date and version then run this command:
gem build ravelry.gemspec
This will create a file/folder/thing called
ravelry-0.0.0.gem. The version
0.0.0 will be replaced with whatever you have in your
gemspec’s “version” setting.
RubyGems: Publishing the gem
First, you need to sign up for a RubyGems account. Make sure you follow the additional instructions for setting up your credentials locally or you won’t get very far.
Then, when you’re ready, run this command:
gem push ravelry-0.0.0.gem
This will published your gem to the RubyGems.org server, and give you a nice little page, like this!
Now that we’ve got those basics covered, let’s talk about making your gem configurable.
Setting API keys (and more): two options
For my Ravelry gem, I needed to have a way for users to set API keys. There are two approaches to this that I considered:
- Having the user set
ENVvariables for the required keys
- Setting up a configuration block that you pass required keys to (sometimes these are
The former was easier, so I went with that option initially. However, as the gem envolved and I started to use it in a Rails application, I decided to clean it up and go with option two.
Using a configuration block will also allow me to add optional settings to the gem in the future as I add more API endpoints. Maybe I wanted to give users an option to receive raw JSON instead of Ruby objects? Or maybe provide different methods for pagination? Regardless of what options I may invent later, creating a way configure settings for a gem is the easiest way to go.
Gem file structure
Pretty much every gem I’ve used utilizes a module syntax, and the Ravelry gem is no different.
My gem starting out looks like this, most of which was provided by Bundler:
/lib ravelry.rb /ravelry *.rb /spec ravelry_spec.rb spec_helper.rb /helpers /ravelry *_spec.rb .gitignore .rspec .ruby-version .yardopts CODE_OF_CONDUCT.md Gemfile Gemfile.lock LICENSE.txt ravelry.gemspec README.md VERSION
What does it all do?
spec folder mirrors the
lib folder, and will continue to do so as the structure gets more complex. The
lib folder contains everything my Gem needs to succeed, and will be the code that runs in a user’s environment when they’re using the Gem.
lib/ravelry.rb file is used to open the
Ravelry module, and to require all of the necessary files (more on that later).
Most of the
dotfiles are for settings I want to be maintained across the project, or for Gem settings (like Yard, for documentation):
.gitignore .rspec .ruby-version .yardopts
Others are required to build the Gem and work with other Ruby gems:
Gemfile Gemfile.lock ravelry.gemspec
Some can be handled inline in my
ravelry.gemspec, but I prefer to have them as separate files:
CODE_OF_CONDUCT.md LICENSE.txt README.md
And I forgot to delete my
VERSION file. lol whoops.
Configuration block: the end goal
We want the users of our gem to be able to do something like this:
Ravelry.configure do |config| config.access_key = '' config.secret_key = '' config.personal_key = '' end
- Our gem users to only have to write this configuration block once per application
- Our configuration settings to be available throughout the gem (and the application using the gem)
So how do we do this?
Creating a configuration block
I started with my
lib/ravelry.rb file, where I require all of my files and create the Ravelry module. It looks like this:
# blah blah # require stuff... module Ravelry; end
This is not super helpful. In fact, all it does is create the
Ravelry module. So let’s fix it up!
We need to do a few things when we create a configuration block:
- Create a
Configurationclass inside of our
- Determine what things we want to be configured and add them to our class with the appropriate
- On our
Ravelrymodule, create an
attr_accessorfor the instance of the
Configurationclass (instance variables on a module - so cool!)
- Create a method to return the configuration settings
- Write some tests!
- Write some documentation!
Steps 1-2: A
Configuration class with accessible settings
I need to have three things accessible to users of my gem:
- Ravelry access key
- Ravelry secret key
- Ravelry personal key
These are things I can’t provide, but the user can get from their Ravelry account.
So let’s make the class!
Don’t forget to require the file in
module Ravelry class Configuration attr_accessor :access_key, :secret_key, :personal_key def initialize @access_key = nil @secret_key = nil @personal_key = nil end end end
I set these values to
nil for every new instance of the
Configuration class because I want to be transparent about the purpose of the class to people who read the gem, and to people who want to contribute to the gem.
This indicates that these keys are required and are not provided by me, so they must be provided by the user.
In the future, when options are added, they may be provided with default values instead of
nil, but for now we just need
Steps 3-4: Making the configuration accessible
Now that we’ve created our
Configuration class, we need to set up our module to use it at the top level.
It is very important that you require your
lib/ravelry/configuration.rb file before anything else, but especially before you write the code in the top level of your module. Basically, just require it first.
require 'ravelry/configuration' module Ravelry class << self attr_accessor :configuration end def self.configuration @configuration ||= Configuration.new end def self.reset @configuration = Configuration.new end def self.configure yield(configuration) end end
What’s happening here? Let’s break it down.
Where it happens:
class << self attr_accessor :configuration end
Modules in Ruby, just like classes, can have instance variables and all of the perks that come with them.
Because we want our users to be able to both read and write their configuration settings, we use
class << self bit tells our
Ravelry module that this instance variable is on the module scope.
Technically, we only need an
attr_writer here because we have a method that does the reading the instance variable
@configuration for us. So why use
attr_accessor? Frankly, I prefer it because it indicates we’ll be reading whatever value set here later, even if we are actually retrieving it using a different method.
Passing a block to our class
Where it happens:
def self.configure yield(configuration) end
There is no other way to put it: this shit is just magic. I really love Ruby at times like this.
When we call
Ravelry.configure, we pass it a block that actually creates a new instance of the
Ravelry::Configuration class using whatever we’ve set inside of the block.
Ravelry.configure do |config| config.access_key = '' config.secret_key = '' config.personal_key = '' end Ravelry.configuration.access_key # => ''
Assuming the rest of your configuration set up is the name, is roughly equivalent to this:
config = Ravelry::Configuration.new config.access_key = '' config.secret_key = '' config.personal_key = '' Ravelry.configuration = config # two ways to access here here: config.access_key # => '' Ravelry.configuration.access_key # => ''
So why bother
yielding a block? Why not just do it the second way?
In short, the block is the more Ruby way of doing this. It’s cleaner, and that way your code isn’t just floating out in space in your application, it’s nestled nicely inside of your namespaced block.
Writing and reading the configuration
Where it happens:
def self.configuration @configuration ||= Configuration.new end
The writing actually happens thanks to the
attr_accessor we set up earlier, but this is where reading comes in.
When we call
Ravelry.configuration, we’re either going to return:
- The instance variable
@configurationthat we setup with our
- A new instance of
Ravelry::Configurationwhere everything is set to
Why not just return
nil if they haven’t set it up yet?
nil isn’t helpful to the users of our gem.
Instead, returning an instance of
Ravelry::Configuration will show them what settings need to be configured if they haven’t read the documentation.
Also, I think it’s just makes you a nicer person.
Where it happens:
def self.reset @configuration = Configuration.new end
I can’t imagine a scenario (outside of testing) where I would want to reset my configuration, but someone out there might need it! This will reset the
@configuration settings to
Step 5: Tests!
So it turns out I didn’t actually write any tests for my configuration block… and I am only realizing that now as I am writing this blog post. I’m a bad role model.
Most of my tests user fixtures instead of making API calls. But! I have one test where I make real API call.
I did, however, add the configuration settings into my global RSpec config:
RSpec.configure do |config| config.before(:all) do Ravelry.configure do |config| config.access_key = ENV['RAV_ACCESS'] config.secret_key = ENV['RAV_SECRET'] config.personal_key = ENV['RAV_PERSONAL'] end end end
Without explicitly writing tests for my configuration setup, I know it works because my one API call where I use my
Ravelry.configuration settings succeeds.
But what about my failure case? Well I better figure that out and write some tests…
Step 6: Documentation
If you don’t write documentation for your configuration setup, people won’t know how to use your gem unless they are a wizard and can figure it out from your code.
This is mean, don’t make them do this.
It takes about 30 seconds to write the 4 lines in your
README.md explaining how to pass options to your configuration block. In fact, you can copy this!
You did it!
Ok, more accurately, I did it and you read about it. But! That means you can do it now!