Help developers make other developers happier

This is the checklist for authors and maintainers of Ruby gems to help them build better open-source software.

Ruby is designed to make programmers happy. –Matz

This document is focused exclusively on code aspects: API design, architecture, and documentation (because any code is useless without docs). OSS principles already have a good coverage in GitHub’s open source guide.

API Design

Simple things should be simple, complex things should be possible. –Alan Kay

Compare achieving the same result with Net::HTTP and HTTParty:

# Net::HTTP get JSON with query string
uri = URI('http://example.com/index.json')
params = { limit: 10, page: 3 }
uri.query = URI.encode_www_form(params)

res = Net::HTTP.get_response(uri)
puts JSON.parse(res.body) if res.is_a?(Net::HTTPSuccess)

# HTTParty
puts HTTParty.get('http://example.com/index.json', limit: 10, page: 3)

Do not limit the functionality only to simple cases, i.e., allow making intricate things possible.

HTTParty, for example, still allows to control underlying HTTP engine fully:

HTTParty.get(
  'http://example.com/index.json',
  { limit: 10, page: 3 },
  basic_auth: {},
  headers: {},
  open_timeout: 2,
  read_timeout: 3
)

80% of users use only 20% of the functionality.

One common pattern for sensible defaults is the convention over configuration:

class Post < ActiveRecord::Base
  # in the world with no defaults
  belongs_to :user, foreign_key: :user_id, class_name: 'User', primary_key: :id
  # and with CoC
  belongs_to :user
end

Try to walk in the shoes of your users.

Ruby community is mature, and there are a lot of best practices. The less people think when using your library, the better.

For example, predicate methods (smth?) should return true/false and not something else (e.g., 0/1). But take a look at this example from Ruby core:

1.nonzero?
# => 1
0.nonzero?
# => nil
0.zero?
# => true
1.zero?
# => false

Confusing, isn’t it?

Consider another example (see this issue):

# Amorail is an API client
Amorail::Lead.find ANY_NONEXISTENT_ID
# => false
# why false? we're looking for an object,
# nil makes more sense when nothing is found

Also, check the story of a confusing retries argument.

Where N is typically equal to 2.

First, kwargs are more readable and do not depend on the order.

Secondly, kwargs allocate less objects compared to options = {} argument.

Example:

# From influxdb-ruby

# not-good
influxdb.write_point(data, precision, retention_policy, database)

# much better
influxdb.write_point(data, precision: precision, rp: retention, db: database)

Always provide error messages: error classes for machines, error messages for humans (check out this talk).

Use ArgumentError if a method is called with wrong or missing arguments.

Provide custom error classes for library’s logic related exceptions:

# https://github.com/influxdata/influxdb-ruby/blob/master/lib/influxdb/client/http.rb
def resolve_error(response)
  if response =~ /Couldn\'t find series/
    raise InfluxDB::SeriesNotFound, response
  end
  raise InfluxDB::Error, response
end

Avoid using negative words (“bad”, “wrong”, etc) in error messages. Use neutral words (“incorrect”, “unexpected”) instead.

When re-raising exceptions, make sure you’re not losing the cause: use raise MyException, "description" instead of raise MyException.new("description") (or use a secret :cause keyword).

Avoid monkey-patching core classes. Consider using Refinements instead (see, for example, database_rewinder).

Patch other non-core libs using Module#prepend (read this exhaustive StackOverflow answer).

Make it hard to shoot yourself in the foot.

For example, Minitest uses long and shaming method name to disable random order for tests (#i_suck_and_my_tests_are_order_dependent!).

Another great idea is to show flashy warnings. Consider this Sidekiq example:

if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test?
  puts('**************************************************')
  puts("⛔️ WARNING: Sidekiq testing API enabled,
    but this is not the test environment.
    Your jobs will not go to Redis.")
  puts('**************************************************')
end

Codebase

Code is written once but read many times.

Your code should have consistent style (i.e. naming, formatting, etc.). And it would be great to respect the community’s style: take a look at the Ruby Style Guide or the StandardRB project.

Compare the following two snippets:

# rubocop:disable all

def some_kinda_fun a, even = false
  x = if even then a+1 else a end
  {:x => a, :y => x}
end

def some_kinda_fun(a, even: false)
  x = even ? a + 1 : a
  { x: a, y: x }
end

# rubocop:enable all

Which one is more readable?

Coverage makes sense for libraries.

But readable test cases make even more sense, especially in integration scenarios (because such tests can be used as documentation).

Architecture

Write code for others, not for yourself.

For example, Active Job is a great abstraction for background jobs: it supports different adapters, and it’s easy to build your own.

On the other hand, Action Cable code is tightly coupled with its server implementation. That makes other WebSocket servers impossible to use without a fair amount of monkey-patching (at least, unless some refactoring is done).

Whenever you write a library for a particular database, framework, or something else, think beforehand if it is going to work with alternatives.

There are different ways to build extensible libraries, e.g., by providing middleware (like Rack, Faraday and Sidekiq) or plugin (like Shrine and Devise) functionality.

The key idea here is to provide an ability to extend functionality by avoiding patching or high coupling.

Logging helps people to identify problems but should be controllable (severity levels, custom outputs, (possibly) filtering).

The easiest way to provide flexible logging is to allow users to specify the Logger instance themselves:

GemCheck.logger = Logger.new(STDOUT)

Avoid puts logging.

Help developers to easily test the code that uses your library: provide custom matchers (like Pundit), testing adapters (like Active Job), or mocks (like Fog).

Ensure your code can be configured to be less computational-heavy in tests, like Devise does:

Devise.setup do |config|
  config.stretches = Rails.env.test? ? 1 : 11
end

Provide different ways to configure your library: manually through the code, from YAML files, or from environmental variables. See, for example, aws-sdk.

Integration libraries must support twelve-factor-able configuration. You can use anyway_config to accomplish this.

Use sensible defaults for configuration (e.g., for Redis connection it’s good to use localhost:6379 by default) and environment variables names (e.g., REDIS_URL for Redis, like Sidekiq does).

More dependencies–more chances for failure, harder upgrades.

You don’t need the whole rails if you’re only using active_model. You don’t need the whole active_support if you only need a couple of patches (consider using ad-hoc refinements instead).

Do not add library that is only used in some use cases as a dependency (e.g., Rails does not add redis as a default dependency, it only suggests that you may add it yourself).

Monitor your dependencies for CVE (see bundler-audit) or let DependencyCI do all the work for you.

There is more than one major Ruby implementation, and at least three popular: MRI, JRuby and Rubinius (and TruffleRuby is coming). The fact that MRI is much more popular than others does not mean you should ditch the rest.

Concurrent Ruby is an excellent example of interoperability.

You should at least provide the information whether other platforms are supported or not (just add them to your CI and check–that’s easy!).

Ractor is a new addition to Ruby 3.0. It brings real parallelism to Ruby. However, it has some limitations: for example, class and global variables couldn’t be accessible from non-main Ractors.

Try to avoid having shared state in globals, class or class instance variables, so your code could be used in Ractors.

Documents

A program is only as good as its documentation. –Joe Armstrong

It is not always necessary to write a book, or even RDocs: well-written Readme could be sufficient (see awesome-readme for examples).

Provide benchmarks in any form if your library is more performant than others (at least, tell users how much memory/CPU/time can be saved with your solution).

Your documentation contains a lot of code examples? Make sure their have correct syntax and consistent style. For example, for Ruby snippets you can use rubocop-md.

And don’t forget about the language! We use yaspeller-ci for that.

A good example is much better than documentation.

Provide code snippets, Wiki pages for specific scenarios–just show people how you are using your own code!

There are several reasons for that: sharing knowledge and helping others to contribute and debug potential problems easily.

See this great example from rbspy.

It should be clear to users what is the current state of the project and which versions of software (the language itself, dependencies) are supported (you can use badges in your Readme).

Help your users to easily upgrade without thinking about breaking changes. For example, you can follow SemVer rules, or use your own semantics (just don’t forget to explain it to your users).

See, for example, how Rails do that.

Wondering why? Just read the keepchangelog.com.

Looking for an automation? Take a look at github-changelog-generator and loglive.

Your commits history is also a kind of changelog, so, use meaningful messages (git-cop can help you with it).

Keeping release notes (e.g. through GitHub Releases) is also a good idea. You can automate release notes generation with Release Drafter.

Make the process of upgrading less painful.

See, for example, Hanami.

Or even better–provide a migration script! Like graphql-ruby did.

Misc

OSS that stops evolution gradually dies. –Matz

Try to prevent compatibility issues by monitoring dependencies upgrades (Depfu or Dependabot could help here).

Run your tests against ruby-head, Rails master, whatever–just add it to your CI, it’s easy!

Sooner or later people will try to contribute to your work. Is your development process transparent, or does it require a lot of effort to setup?

For example, Rails has a rails-dev-box to help you start developing easily.

Docker is also a good way to make dependency management simpler.

Keep version of a gem at RubyGems up-to-date with your main repository releases.

When should I bump a version?

  • Security fix
  • Fixing a regression bug (like Ruby 2.3.3)
  • Adding a feature – wait for next planned release
  • Fixing a bug that was there for a long time – wait for next planned release

It’s a good practice to publish RC/beta versions prior to releasing a major update.

You can automate your release process by using, for example, gemsmith / gem-release or CI services (e.g. Travis supports RubyGems deployments).

While it makes a great sense to add to your repository as more documentation and examples as possible, have a huge test suite and development guides, it’s better not to include this unrelated data to the final package. Be reasonable about your gem content. The only essential parts, in the most cases, are basic README, license information, and the source code itself.

Whitelisting gem content protects you from occasional and undesirable “additions” in the future.

Gem::Specification.new do |spec|
  # ...
  spec.files =  Dir.glob("lib/**/*") + Dir.glob("bin/**/*") + %w[README.md LICENSE.txt CHANGELOG.md]
  # ...
end