Forem

Kevin Luo
Kevin Luo

Posted on

Some tips for making a ruby gem

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Yours can be different because of different versions of bundler. Besides getting this scaffold, it also has some advantages, like:

  1. You can use bundler to handle the dependencies.
  2. You have predefined Rake tasks to test and release the gem
  3. git repository is initiated
  4. 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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The file's content can be an empty class:

# lib/your_gem/configuration.rb
module YourGem
  class Configuration
  end
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.

  1. bin/console: it opens the irb and loads your gem, so you can
  2. rake install: it is a rake task set up by bundler. It will build a gem package and invode gem install pkg/your_gem to install it in the system. As a result, you can truly test it in your other Ruby projects by require '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
Enter fullscreen mode Exit fullscreen mode

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)