How to get ideas for a gem, when you don't know what to make

5 May 2015

Ruby is currently (and will likely remain) my favourite ever coding language, so I really want to get to know every part of it as well as I can. This has given me an itch to learn how to create and publish gems for quite a while, so recently I decided to give it a try. I've gained a lot from the enormous array of community-written gems that people have made available, so becoming part of the community and contributing something back felt like a worthy thing to aspire to.

However, when I sat down to get started I ran into a problem before I could even begin: what do I make?

At the time, I didn't have a project that seemed like a good candidate for being an independent gem, and especially not one that other people might want to use. So what to do if you want to get something out there and you're stuck for inspiration? Well, here's the options as I see them:

  1. Scan over all your recent work, whether it's for major projects, or just messing about, and look for duplication. Anything that's occurring more than once is a candidate for becoming a gem. This could be something really simple. For example, a bunch of scopes and mixed-in methods that you use in more than one active record model. You could add them to any active record class by calling a class method that your gem can add to ActiveRecord::Base e.g. the way the searchkick gem does, allowing you to do this:

    # In the gem
    ActiveRecord::Base.send(:extend, Searchkick::Model) if defined?(ActiveRecord)
    
     # In your code
     class Product < ActiveRecord::Base
       searchkick
     end
    

    You may feel you'd be better served by making a module to mix in within your app to start with. However, gems don't necessarily need to be public - you can install them from a local folder on your filesystem, or from a gihub repo (public or private).

     gem 'my_gem', github: 'myaccount/my_gem'
     gem 'my_other_gem', path: '/Users/me/code/my_other_gem'
    

    Not a bad way to practice before doing something bigger.

  2. Imagine a cool extra feature that any gem you're using could have, then make a gem that monkey-patches the new feature into existence. This is the approach that Resque Scheduler takes. The main Resque class ends up with a load of new stuff mixed in from a module in the gem:

    # Within the gem
    Resque.extend Resque::Scheduler::Extension
    
    # In your code
    Resque.enqueue_at(1.day.from_now, SendReminderEmails)
    
  3. Look at the gems you are currently using. Do any of them have a plugin structure where third party gems can add functionality? Resque Web, for example, is currently structured like this, so extending the interface with a new tab page is easily done by packing up a small Rails Engine into a new gem. If you find a gem like this, then there may be an easy-ish suggestion or two in the issue tracker. Find the gem on Rubygems, click on the issues link (which will probably go to the issues page for the gem's Github repo), and see what's there. Alternatively, ask the maintainers directly.

This third approach worked for me, when I realised that Resque Web had recently been extracted into a separate gem ready for Resque 2.x (rather than being bundled with Resque 1.x), and moved from a Sinatra app to being a Rails engine. This included a new way to define plugins cleanly via Rails Engines that could be mounted inside the Resque Web Engine itself. Perfect! I noticed that various other Resque-related gems which provide extensions to Resque Web had not been updated yet, so their old Sinatra code would need to be ported over to a Rails engine in order to be usable with the new gem. Given that one of these was resque scheduler, which I wanted to use in a project, this seemed like an ideal bit of open source work to have a go at.

Anyway, there is now a nice shiny new gem called resque-scheduler-web, which works very well, and which seems to be popular. I'm very proud of it, although it's taken a lot more work than I thought!

Things I learned building the gem:

Sometimes extracting part of an existing gem into a new gem can be a good thing.

The strange thing is that I didn't initially think of making this as a good project to make into a gem. I needed the functionality, so I just made a pull request for the main Resque Scheduler gem with the code and tests refactored as a Rails engine. This made sense at the time, as I was working on code which already contained the functionality for the resque web extension (as well as the code for the actual scheduler), so why not just update it in place? However, as one of the Resque Scheduler maintainers pointed out, it seems to work better as a separate gem. Firstly, this is because it is intrinsically doing a different job, so it's better if the two gems can vary independently. And secondly, because the old Sinatra interface is still the default for Resque 1.x, so we should not yet replace the Sinatra code in the current Resque Scheduler gem, which is still the recommended interface.

Gem maintainers can provide valuable insights you may not otherwise think of.

Had I not had a discussion in a pull request thread on Github about it, I may not have realised that making a new gem was a better idea, and more convenient for people. Good thing I did! Also, gem maintainers in my experience seem pretty friendly, and appreciate it if you reach out and offer to help. If you are not sure of whether or how to improve an existing gem by making a plugin of some sort, reach out and ask the maintainers about it. They may well point you in a better direction than you would have come up with yourself, making your life easier and your gem more popular.

Test coverage is everything. Aim for 100%, and take test refactoring into account.

Releasing a gem means that people are inevitably going to end up using it in their projects, so not having solid test coverage puts the success of their work at risk. Trouble is, whilst moving the code and tests over to the new gem and porting them to Rspec, it became clear that the coverage was far from perfect. Some entire pages had nothing but an integration test asserting that the response was a 200 status code! I couldn't leave things like that, so I decided to write a full set of integration tests covering all of the existing functionality. Unfortunately, this made the whole project take quite a lot longer than I had envisaged, so make sure you take this into account when starting out.

Conclusion

Overall, making my first gem was a great experience, but could certainly have been an easier ride.

Mountain Road logo