Crafting Rubies: Best Practices While Cutting Gems
To some, cutting gems is considered an art for the minerally inclined. In the Ruby world, however, gem cutting is a matter of life. Creating RubyGems can be easy, but also a trap for a novice. By following some general community best practices, you can create gems that can live in harmony within the Ruby ecosystem, and be enjoyed by consumers and contributors alike for years to come.
Make use of gem authoring tools
With the exception of experienced cutters, or those who want or need to have complete control over every line of code, there are gem authoring tools that can automate the majority of the manual work required, and avoid amateur mistakes, when publishing gems for the first time.
The biggest of the gem authoring tools at the moment is Jeweler (RubyGems, GitHub), which aims to provide a set of wizard based gem automation tools. It has tasks to manage releasing, versioning, dependencies, executables and Rake tasks all out of the box; it can be a real time saver in the long run for frequent or beginner gem publishers.
Use load paths and
It’s very easy to clobber the load path or use
require horribly in Ruby without realising the detriments of doing so. A fantastic article by Josh Peek on the Ruby on Rails blog details the ins and outs of the load path and require’ing, which is well worth the reading time. Some of the more important points of his article are:
- RubyGems (and most other package managers) add the
lib/directory of your gem to the load path; in some cases this is prepended to the load path. If your
lib/directory contains any files named the same as any core Ruby files (eg.
string.rb), your gems’ file will take precedence. As such, follow the convention to keep all your files under a directory named the same as your gem within your
- Most package managers, and testing frameworks, will automatically add your
lib/and test folders (
features/being two examples) to the load path. If providing an executable, or a
Rakefile, you can add the
lib/directory to the path there (
$:.unshift(path_to_lib)). As a result, remove code using things like
File.dirname(__FILE__)within any requires, as you can safely assume that you can always require relative to your
Don’t depend on anything outside of your project’s
lib/folder (with the exception of other gems); some package managers only copy across your
├── test_gem.gemspec ├── Gemfile ├── Rakefile ├── README ├── bin │ └── test_gem ├── lib │ ├── test_gem │ │ └── some_module_or_class.rb │ └── test_gem.rb └── spec/features/test └── *
Don’t require rubygems inside your gem
When you include
require 'rubygems' in your gem, you take away the user’s choice to use a package manager other than RubyGems. There are many powerful and full featured alternatives to RubyGems, including Rip (RubyGems, GitHub), and Dpkg (used mainly by sysadmins who need to manage gems using Debian packages).
Ryan Tomayko has a brilliant article on his blog entitled ‘Why “require ‘rubygems'” is wrong”, which details the technical reasoning and best practices behind leaving it out.
Use a meaningful versioning pattern
Versioning gems can be a painful experience, but it’s a mountain that has been conquered by very intelligent people (several times at that). The Semantic Versioning system aims to make versioning more predictable, and therefore safer, and it can be applied to gems with minimal effort. The semver.org website includes a detailed specification, as well as best practices when versioning software.
Some important takeaway points from the specification are:
- Versions should be in the format X.Y.Z (Major.Minor.Patch).
- You can use special version numbers when pushing out test release versions, eg. 1.0.0beta1, 1.0.0alpha3, 1.0.0rc1.
- You can make feature changes and bug fixes in Major versions.
- You can make backwards compatible feature changes and bug fixes in Minor versions.
- You can only make backwards compatible bug fixes in Patch versions.
- The API should be considered public and stable from 1.0.0 and onwards.
Harness the power of Bundler for testing
A little known secret of Bundler is that it can automatically use the dependencies within your
.gemspec file, whether manually provided or generated by a gem tool, to create a
source :rubygems gemspec
With the simplest of
Gemfiles above, you can distribute an easily maintained
Gemfile.lock, and use
bundle exec to wrap your test runs and prevent gem version collisions.
Have a decent way to track issues publicly
Creating a gem without any issues is a myth. However, remembering that most of the users of any gems you create will also be technically adept, it’s a matter of course that you will have people who are ready and willing with the required details for providing a fix.
Providing a decent way of tracking issues seems obvious, but you’d be surprised at how difficult it can be to find a place to submit bugs, feature requests, or other issues to some gem maintainers.
If you’re hosting your gem on GitHub, you get a fantastic issue tracker for free. Some people prefer hosting on things like Lighthouse; whatever you choose, just make sure it’s linked to within the README in an obvious way.
Make contributing easy
Tangential to the last point is the idea that contributing to a gem should be easy. Make sure that you have a clear license that explains what and how people are allowed to contribute. Also try to make it worth their while to contribute back to the original code by offering clear instructions and restrictions that you apply to any incoming source code.
Hosting your code on GitHub is a fantastic way to nurture this practice, as Git makes it dead simple to work in a fork-alter-merge pattern, and GitHub wraps this in Pull Request goodness.
GitHub has a great help section on Pull Requests which should prove invaluable to Git/GitHub newcomers.
Once public, it stays public
Once you’ve published a gem, even if you’ve made a catastrophic mistake, it should be your absolutely last resort to yank the gem. Even more important is that, if you do finding yourself having to yank a version, under no circumstances should you reuse the version number of the copy you yanked.
System administrators, and developers for that matter, rely heavily on gem numbers being immutable and ever available. Even if you have added a password or other piece of important security information that you need removed, you’re likely better just to change the password (you’ll need to do this anyway), and publish a follow-up release.
The help section at RubyGems has a very detailed set of instructions on gem yanking should you, for whatever reason, still feel you need to exercise this right to arms.
Naming and casing
Naming is probably the first thing you will find yourself doing, and perhaps one of the most important steps you need to get right.
There are some commonly adhered to rules about casing, which you shouldn’t break unless you really have a good reason. The following rules apply to the name of the gem, as well as any file names within the gem:
- No upper-case letters. This will inevitably cause problems for users on Windows or OS X, which both use case insensitive file systems by default.
- Use underscores for spaces. An example, “Test Class” should become “test_class”.
- Try to keep one module or class per file, and name the file after that module or class. An example, if a file contains a module called “EventGenerator”, the file should be called “event_generator”.
If you follow these pretty simple rules, all of a sudden it becomes a lot easier for people to make assumptions about how to
require, and contribute to your code; a little effort here goes a long way.
So you want to create your own gems? By following the above best practices, you’re likely to avoid treading on the toes of your forebearers. A lot of very smart, very intelligent people are responsible for making gem cutting as easy as it is today, and in the process they offer valuable tools and techniques that require some level of mastery. Learn these tools, follow the best practices above, and you’ll be on your way to helping create a safer, cleaner and more efficient Ruby code community.