I recently built a gem, https://github.com/kevinluo201/fx_rates, which is an API wrapper for a good exchange rate service, https://fxratesapi.com/. Anyway, what that gem does is not the point here. I simply want to share my experience of making a ruby gem.
Use bundler to build a gem
First, I suggest using bundler
to build a gem. Of course, you can follow the official guide on RubyGem.org's Make your own gem. However, just like other modern projects, it will be much easier to start from boilerplates. bundler
can provide us the ruby gem's boilerplates. Assume the gem you're going to make is called your_gem
. You can initiate the gem by:
bundle gem your_gem
You should get a directory like below:
tree
.
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│ ├── console
│ └── setup
├── lib
│ ├── your_gem
│ │ └── version.rb
│ └── your_gem.rb
├── sig
│ └── your_gem.rbs
├── spec
│ ├── spec_helper.rb
│ └── your_gem_spec.rb
└── your_gem.gemspec
Yours can be different because of different versions of bundler. Besides getting this scaffold, it also has some advantages, like:
- You can use
bundler
to handle the dependencies. - You have predefined Rake tasks to test and release the gem
- git repository is initiated
- etc.
More information can be found at https://bundler.io/guides/creating_gem.html. Now you can start editing the your_gem_name.gemspec
to add the basic information for your gem and the dependencies.
Use Zeitwerk
Second, I suggest using zeitwerk gem to manage the constants like classes and modules via organizing the files. I'm going to explain why.
We need to know one thing first: What happens when we require a gem in ruby? For example,
require 'a_gem'
It tells Ruby to execute the entry point ruby file in that a_gem
. What is the entry point file in a gem? It is always the file with the same name as your gem's under lib/
. In our case, it's lib/your_gem.rb
After we create a gem by bundler, lib/your_gem.rb
file should look like this:
# frozen_string_literal: true
require_relative "your_gem/version"
module YourGem
class Error < StandardError; end
# Your code goes here...
end
Let's look at require_relative "your_gem/version"
. It just means it loads lib/your_gem/version.rb
. You will realize require 'your_gem
only loads the entry file. For other files in the gem, they need to be "required in the entry point file. There's no magic. They won't be loaded or connected automatically.
Now, suppose we want to add a new class, YourGem::Configuration
, we can put it under lib/your_gem/configuration.rb
.
├── lib
│ ├── your_gem
│ │ ├── configuation.rb
│ │ └── version.rb
│ └── your_gem.rb
The file's content can be an empty class:
# lib/your_gem/configuration.rb
module YourGem
class Configuration
end
end
Then we need to require it in the entry point file:
we need to require this new file in the entry point:
# frozen_string_literal: true
require_relative "your_gem/version"
require_relative "your_gem/configuration". # <---- this line
module YourGem
class Error < StandardError; end
# Your code goes here...
end
We can get YourGem::Configuration
after requiring the entry point file. We can repeat this process for every file in the gem. Although it is very clear, zeitwerk
provides another approach to automate this process.
With zeitwek
, it can be rewritten as
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup
module YourGem
class Error < StandardError; end
# Your code goes here...
end
YourGem::Configuration
will be loaded automatically. In fact, all files under lib/your_gem/**/*
will be loaded after loader.setup
is executed, so you don't need to add any line after adding a new file.
It's a pattern of "convention over configuration", so all your files names have to follow the following pattern:
lib/my_gem.rb -> MyGem
lib/my_gem/foo.rb -> MyGem::Foo
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
More details can be found at https://github.com/fxn/zeitwerk
Add configuration
Some gems provide an interface for developers to define global settings, like in rails
Rails.application.configure do
config.eager_loading = false
# other settings
end
It looks super cool! We can also let our gem have this ability. Let's say we want to set a global api_key
. First, we can create a class to hold the settings. We can use YourGem::Configuration
we created above to do that:
module YourGem
class Configuration
attr_accessor :api_key
def initialize
@api_key = nil
end
end
end
Then in the entry_point lib/your_gem.rb
, we add a module-level accessor and method:
module YourGem
class Error < StandardError; end
class << self
attr_accessor :config
def configure
self.config ||= Configuration.new # <--- YourGem::Configuration has been loaded by zeitwerk above
yield(config) if block_given?
end
end
end
By doing this, we can write and read the api_key
by
# write
YourGem.configure do |config|
config.api_key = "your-api-key-here"
end
# read
YourGem.config.api_key
# "your-api-key-here"
How to test the gem before releasing it?
Of course, we want to check out how's our gem doing before releasing it. There are 2 ways to test your gem quickly.
-
bin/console
: it opens theirb
and loads your gem, so you can -
rake install
: it is a rake task set up by bundler. It will build a gem package and invodegem install pkg/your_gem
to install it in the system. As a result, you can truly test it in your other Ruby projects byrequire 'your_gem'
How to release it?
All the things we've done for a gem are to release it on https://rubygems.org/. To release the gem, you need an account on https://rubygems.org/ first. So, sign up for one if you haven't done that yet.
Then execute rake build
to build a gem file under pkg, it should look like pkg/your_gem-0.0.1.gem
. The version number 0.0.1
is defined in the lib/your_gem/version.rb
. Finally, we call
gem push pkg/your_gem-0.0.1.gem
and it will push the gem to https://rubygems.org/ 🎉
Conclusion
I think building a gem is not a daily job for most of the developers. However, gems are an essential part of the Ruby community. Why can we always find a gem for our needs? It is because people share what they build. How do we become people who can share? Knowing how to make a gem is the first step. I hope this article can make you feel making a gem is simpler than you thought. 🙏
Top comments (0)