Converting a Ruby Class Library to a Gem
Note: Updated on 2022-06-04 to reflect experience with actually maintaining these gems.
This blog post talks about my experiences converting a handful of Ruby class libraries to gems. As do a lot of software engineers, I have a series of routines that I bring into almost every Ruby project I tackle that deal with what I consider core stuff you always need: url handling, database stuff, time parsing and so on. Normally I just copy these from project to project but the sheer plethora of them has recently made me see the need to go down the gem route:
❯ mdfind -name url_common.rb | wc -l
62
All of these files are named *common.rb so you will see a number of these on my Github page. I don't claim that any of them are particularly wonderful, brilliant, complete or even well coded; I simply find them useful.
In order to figure out which was the right version of the 62 different files I found above, I wrote a separate blog post about using mdfind.
The How
Here is the quick tldr of how to build a gem.
- Create a gems directory where you can group all the gems you have. Once you have one, I suspect you're going to have many.
- Change into that gems directory.
- Create a repo for the gem and clone it locally.
- Change into the repo you just cloned.
- Do a gem signin
- Do a bundle gem project_name
- Edit project_name/project_name.gemspec - THIS NEEDS TO HAVE RELEASE LEVEL DEPENDENCIES
- Update your Gemfile with any dependencies - THIS NEEDS TO HAVE BOTH DEVELOPMENT LEVEL AND RELEASE LEVEL DEPENDENCIES
- Change into project_name
- Do a bundle install
- Do a bundle exec rake build; this gives you the pkg/* stuff below (see next command).
- Do a gem push pkg/url_common-0.1.0.gem
- Oh and write the code and the tests. This exists within the lib directory structure.
Tips and Tricks
1. Change into the project_name/lib Directory to Run bundle install
My first attempt at following the directions gave me this:
❯ bundle install
Could not locate Gemfile
And the easy solution was to change into the project_name/lib directory. So:
cd url_common/lib
Yep. I was an idiot for not realizing this. Sigh.
2. Change into the project_name/lib Directory to Run bundle exec rake build
Similar to 1 above, my attempt to run bundle exec rake build gave me this failure:
❯ bundle exec rake build
Could not locate Gemfile or .bundle/ directory
Again. I was an idiot. And, again, same solution – change to the right directory.
3. Use irb for Debugging
In order to debug the gem you are building:
- Change into the project_name/lib directory
- run irb
- require your gem i.e. require 'url_common'
And now you can execute commands from your gem like:
UrlCommon.foo
4. If You Have a Class Library then You Don't Need class project_name
My prior class libraries were all structured like this:
class ProjectName
def self.foo
end
def self.bar
end
end
That now becomes something like this:
require "url_common/version"
require 'any_gem_you_need'
module UrlCommon
class Error < StandardError; end
def self.foo
end
def self.bar
end
end
Given that modules provide a namespace just as a class does and the . syntax invokes methods uniformly, this lets you invoke your "class methods" the same way you did when they were actually class methods.
5. The .try Method is a Rails Thing Not a Ruby Thing
Even though I like the semantic clarity of .try(:method_symbol), you can use &.method_name instead:
#return parts.hostname.sub(/^www\./, '') + parts.try(:path) + '?' + parts.query
return parts.hostname.sub(/^www\./, '') + parts&.path + '?' + parts.query
This is a Ruby 2.3 change so it should be available to everyone by now.
6. Within Your Gem You Can't Reference the Namespace
I hit this error:
1) UrlCommon.url_base should return the url base w/o the www
Failure/Error: base_domain = UrlCommon.get_base_domain(url)
NoMethodError:
undefined method `get_base_domain' for UrlCommon::UrlCommon:Class
# ./lib/url_common.rb:84:in `url_base'
# ./spec/url_common_spec.rb:97:in `block (3 levels) in <top (required)>'
This came from this line of code:
def self.url_base(url, base_domain=nil)
if base_domain.nil?
base_domain = UrlCommon.get_base_domain(url)
end
#...
end
and the fix turned out to be:
def self.url_base(url, base_domain=nil)
if base_domain.nil?
base_domain = get_base_domain(url)
end
#...
end
despite there being a def self.get_base_domain method defined in the module. Shrug
Gems for Development versus Gems for Deployment
When you develop a gem, it needs gems for development which are located in Gemfile. The same gems also need to go into the gem_name.gemspec file. So, for example, I have a file called:
url_common.gemspec
which has lines like these:
spec.add_dependency 'fuzzyurl', '~> 0.9.0'
spec.add_dependency 'mechanize', '~> 2.6'
This allows you have say byebug in your main Gemfile for debugging but NOT have that be released with your gem.
The Release Process - Example
The first step is to update the version.rb file to reflect a new version number. Please note that for a new release to be made, there must be a new version number.
I work on the url_common gem in this directory:
/Users/sjohnson/Sync/coding/gems/url_common/
to do a release:
cd /Users/sjohnson/Sync/coding/gems/url_common/url_common
bundle exec rake build
You will then see output like this:
url_common 0.1.1 built to pkg/url_common-0.1.1.gem.
And you release it as:
gem push pkg/url_common-0.1.1.gem
And here's the really important part:
- After you do a gem push, set a 3 minute timer on your phone
- When it goes off, change to an app using your gem
- Do a bundle update
It normally takes a bit for the Ruby Gems site to recognize an update. The timer helps you remember to check.
Conclusion
I haven't tried to build a gem in years. Building a gem is substantially easier in 2020 than it was circa 2007 - 2009. Kudos to the entire Ruby tooling team. Recommended.
Sources
See these sources:
Posted In: #ruby