One of the foundations stones of the Ruby on Rails Framework is to keep your controller slim, or as slim as possible! In one of the first apps that I built, trying to get a job as a junior Rails developer, I put everything but the kitchen sink inside my controllers – one of the bad habits I developed writing scripts for so long.
After a while, you start to get the hang of keeping everything as clean and as short as possible. In this context, I was playing around building an app to check stocks and cryptocurrency prices and, as expected, I was going to need to use an API to get the information that I needed. In this case, we are faced with three options:
- you can look for a gem that does the magic for you;
- you can implement the Ruby code that fetches the data and put it as service in your app;
- OR you can start with your first steps to become the magician yourself and build your own gem, and then install it in your Rails app as you would install any other gem.
Creating Your Own Gem
This is a quick tutorial (and a consolidation of other guides that I used) on how to start with your own first gem to use on your Rails project. Here I am sharing some insights that I had while building mine.
When you check the guide on how to build gems on Rubygems, they will lead you step by step creating every file by hand. While I believe it is worth reading it and learning about every aspect of the gem structure, notice that this guide was based on “Gem Sawyer, Modern Day Ruby Warrior” guide written in 2010 and even the first note on the “make your own gem” part of the guide mentions that a lot of people use Bundler to create gems. For the very first gem, I would recommend using the Bundler approach to get the same structure of most gems and to make it easier to publish it correctly later.
First Things First: Naming Your Gem
There are only two hard things in Computer Science: cache invalidation and naming things.
— Phil Karlton
Before you create your gem, you must consider the name you will give to it. This is important for two reasons:
- There is a guideline on the proper use of underscore ( _ ) and dash ( – ) as separation symbols for multiple words names. According to the guide:
- if a class or module has multiple words, use underscores to separate them. This matches the file the user will require, making it easier for the user to start using your gem, and
- if you are adding functionality to another gem, use a dash. This usually corresponds to a slash ( / ) in the require statement (and therefore your gem’s directory structure) and a double colon ( :: ) in the name of your main class or module.
The goal of these guidelines is not only to provide the user with a clue on how to require the files in your gem, but also to let Bundler require your gem with no extra configurations. The following chart exemplifies this:
GEM NAME |
REQUIRE STATEMENT |
MAIN CLASS OR MODULE |
ruby_parser |
require ‘ruby_parser’ |
RubyParser |
rdoc-data |
require ‘rdoc/data’ |
RDoc::Data |
net-http-persistent |
require ‘net/http/persistent’ |
Net::HTTP::Persistent |
net-http-digest_auth |
require ‘net/http/digest_auth’ |
Net::HTTP::DigestAuth |
- Another point to take into consideration while choosing the gem’s name is that if you wish to share your gem later, for example in Rubygems, the name should be unique. So, make sure to check if the name has already been taken.
Creating the Structure of the Gem
Once you are settled with a name, you can create the files that you will need with:
~/gem_tutorial/my_example_gem$ bundle gem <gem_name>
It will automatically create a folder with the name <gem_name> or, if you are in the folder where you want the files to be created you can also use the:
~/gem_tutorial/my_example_gem$ bundle gem .
It will read the current folder name and create the files with the correct name inside the current folder.
For example, let’s create a gem called example_gem:
~/gem_tutorial/my_example_gem$ bundle gem my_example_gem
Creating gem 'my_example_gem'...
MIT License enabled in config
Code of conduct enabled in config
create my_example_gem/Gemfile
create my_example_gem/lib/my_example_gem.rb
create my_example_gem/lib/my_example_gem/version.rb
create my_example_gem/my_example_gem.gemspec
create my_example_gem/Rakefile
create my_example_gem/README.md
create my_example_gem/bin/console
create my_example_gem/bin/setup
create my_example_gem/.gitignore
create my_example_gem/.travis.yml
create my_example_gem/.rspec
create my_example_gem/spec/spec_helper.rb
create my_example_gem/spec/my_example_gem_spec.rb
create my_example_gem/LICENSE.txt
create my_example_gem/CODE_OF_CONDUCT.md
As you can see, the bundle creates all this nicely structured set of files, but the files that you will mostly work with are:
- lib/my_example_gem.rb – this is where you will place the code
- lib/my_example_gem/version.rb – this file will be important publish updates/corrections to your gem
- spec/my_example_gem_spec.rb – you will write the tests to your code
- gemspec – metadata about the gem, such as author, description, summary
Your code should be written in the *.rb file in lib, in the context of our example I created two simple functions on lib/my_example_gem.rb: one that prints a silly message and one that actually sends a request to an URL and parses the JSON result into a hash (as we would with an API call).
require "my_example_gem/version" require 'excon' require 'json' module MyExampleGem class Error < StandardError; end def self.say_hello_gem "Hello, Gem!" end def self.call_api(url) response = Excon.get(url) return nil if response.status != 200 JSON.parse(response.body) end end
Once your code is ready, you need also to change the metadata on my_example_gem.gemspec. You will not be able to build the gem or install it with the default data on the file. You need the very least to change the following fields:
- spec.summary
- spec.description
- spec.homepage
- spec.metadata[“allowed_push_host”]
- spec.metadata[“source_code_uri”]
- spec.metadata[“changelog_uri”]
For example:
require_relative 'lib/my_example_gem/version' Gem::Specification.new do |spec| spec.name = "my_example_gem" spec.version = MyExampleGem::VERSION spec.authors = ["Your Name"] spec.email = ["youremail@example.com"] spec.summary = "My Example gem for a simple API call" spec.description = "This is an example gem for a quick tutorial" spec.homepage = "https://github.com/<git_username>/my_example_gem.git" spec.license = "MIT" spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") spec.metadata["allowed_push_host"] = "https://rubygems.org" spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = "https://github.com/<git_username>/my_example_gem.git" #spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] end
When you have your source code ready and the metadata on gemspec ready you can build your gem with:
~/gem_tutorial/my_example_gem$ gem build my_example_gem.gemspec
Successfully built RubyGem Name: my_example_gem Version: 0.1.0 File: my_example_gem-0.1.0.gem
Once built, you can install it with:
~/gem_tutorial/my_example_gem$ gem install my_example_gem-0.1.0.gem Successfully installed my_example_gem-0.1.0 Parsing documentation for my_example_gem-0.1.0 Installing ri documentation for my_example_gem-0.1.0 Done installing documentation for my_example_gem after 0 seconds 1 gem installed
If everything worked out fine, you can import your gem on irb to check if the methods are working properly.
~/gem_tutorial/my_example_gem$ irb 2.7.0 :001 > require 'my_example_gem' => true 2.7.0 :002 > MyExampleGem.say_hello_gem => "Hello, Gem!" 2.7.0 :003 > MyExampleGem.call_api('https://api.coingecko.com/api/v3/ping') => {"gecko_says"=>"(V3) To the Moon!"} 2.7.0 :004 >
Test Your Gem!
Cool! Everything seems to be working, but before you publish you code, you must write your tests! Testing is just as essential for gems as it is for Rails. It ensures that the gem is working correctly and helps to detect if a later change in the code will break something that is already working. It is something of an assurance to other users, that the code is working as expected!
The Bundler that creates the initial structure already sets rspec files that you will need to test your gem, so you don’t need to manually add anything to use rspec, but you do need to install it. So you run bundle install on your gem root folder:
~/gem_tutorial/my_example_gem$ bundle install Fetching gem metadata from https://rubygems.org/... Resolving dependencies... Fetching rake 12.3.3 Installing rake 12.3.3 Using bundler 2.1.4 Fetching diff-lcs 1.4.4 Installing diff-lcs 1.4.4 Using my_example_gem 0.1.0 from source at `.` Fetching rspec-support 3.9.3 Installing rspec-support 3.9.3 Fetching rspec-core 3.9.2 Installing rspec-core 3.9.2 Fetching rspec-expectations 3.9.2 Installing rspec-expectations 3.9.2 Fetching rspec-mocks 3.9.1 Installing rspec-mocks 3.9.1 Fetching rspec 3.9.0 Installing rspec 3.9.0 Bundle complete! 3 Gemfile dependencies, 9 gems now installed. Bundled gems are installed into `./vendor/bundle`
Now add your test codes to spec/my_example_gem_spec.rb.
RSpec.describe MyExampleGem do it "has a version number" do expect(MyExampleGem::VERSION).not_to be nil end it "it greets the user" do expect(MyExampleGem.say_hello_gem).to be_a(String) end it "return JSON(hash) when API url" do expect(MyExampleGem.call_api('https://api.coingecko.com/api/v3/ping')).not_to be_empty end end
Ok, ok! This is not a great test, and I am working on writing better tests, but you get the idea.
To test your gem, you can either run:
~/gem_tutorial/my_example_gem$ rspec spec/example_gem_spec.rb
OR
~/gem_tutorial/my_example_gem$ bundle exec rspec spec MyExampleGem has a version number it greets the user return JSON(hash) when API url Finished in 1.57 seconds (files took 0.21686 seconds to load) 3 examples, 0 failures
Publish It
Great! If everything has tested correctly, you are good to go! If you want to publish your gem to Rubygems, you will need to create an account there and then all you need to do is:
~/gem_tutorial/example_gem$ gem push my_example_gem-0.1.0.gem
It will prompt you for your email and password and/or MFA code if you configured your account on Rubygems to use MFA and then you are done! If you want, you can also push your code to GitHub and pipeline your production/updates on your gem from GitHub straight to Rubygems (see more). I personally have not tried this approach yet, but I’ll surely try on my next update.
Update the Gem
Last but not least, the lib/my_example_gem/version.rb file will need to be changed when you need to fix, refactor or update your published gem. You will not be able to publish the edited gem if you do not change the version written on this file prior to the gem push command.
Conclusion
And that is it! Once published to Rubygems, you can add your recently created gem to your Gemfile of your Rails project and install it as any other readily available gem. Once you install it, you will be able to call its functions, preferably, from within a Model’s method and then you call the method from the Controller.
Summary
In this mini guide we saw how to build a gem to integrate with a Ruby on Rails Project. A quick summary of the steps performed are as follow:
- Be careful to choose a unique gem name if we want to publish the gem later.
- Run bundle gem <gem_name>.
- Edit lib/<gem_name>.rb to add the code that needs to be implemented.
- Add the necessary metadata info on <gem_name>.gemspec.
- Build the gem with gem build <gem_name>.gemspec.
- Install the gem to test it with gem install <gem_name>.gem.
- Install the rspec to add tests with bundle install.
- Add the test functions on spec/<gem_name>_spec.rb.
- Run the tests with bundle exec rspec spec.
- Publish your gem to Rubygems with gem push <gem_name>.gem.
- Add your gem to your Gemfile on your Rails project.
If you have any comments, suggestions, corrections or if you do it differently feel free to share your ideas and insights!