Contributing to Ruby

Contributing to a well-known, established project such as Ruby may seem daunting at first, but once you get stuck in it’s not too bad. In this post, I’ll show how you can get started, with a high-level overview of the steps I took to extend the core Range class, released in Ruby 2.6.0.

What?

Ruby supports Range objects representing sets of values, such as (1..10) and ('a'...'f') ; an example use case is to help prevent out-of-bounds values:

def valid_loan_value? ( loan_value ) ( 1000 .. 5000 ). cover? ( loan_value ) end

My Range extension allows cover? to accept another Range, rather than just a single element:

( 1 .. 10 ). cover? (( 3 .. 6 )) # => true

which can make certain range-checks more succinct and readable.

Why?

At Bamboo, certain customers are allowed to top-up their loans: the existing loan is settled, and the customer receives an additional amount of their choice (from a fixed range). To ensure we don’t lend outside our criteria, we ensure that the Range of amounts (offset by a “settlement” amount) is covered by our valid loan value Range . The code we want to write looks like:

MIN_VALUE = 100 MAX_VALUE = 500 def valid_top_up_loan_value? ( settlement_amount ) top_up_value_min = MIN_VALUE + settlement_amount top_up_value_max = MAX_VALUE + settlement_amount top_up_value_range = ( top_up_value_min .. top_up_value_max ) ( 1000 .. 5000 ). cover? ( top_up_value_range ) end

For example, we have

valid_top_up_loan_value? ( 1234 ) # => true

as top_up_value_range = (1334..1834) , which is covered by (1000..5000) .

How?

Having worked out the feature I’d like to use, my first stop was to open the Ruby issue tracker, where changes to Ruby (well, MRI, really) are planned and discussed, and search for similar issues. The search functionality is a little unwieldy, but persevere - you can find many interesting discussions and ideas when hunting for an existing issue!

After finding an issue that sounded like what I needed, I added a “+ 1 - we’d like this!”.

As it happened, I had actually already implemented a similar method in Ruby as a monkey-patch to the Range class, and had an idea about some tests for the new feature which I attached to the ticket (these tests were very important for catching edge-cases, later).

To start building a change to MRI (in C), the first step is to obtain the MRI source code and ensure you can build-and-run the resulting Ruby binary. Something like:

$ ./configure && make # lots of output... $ ./ruby --disable-gems -e 'puts "It works..."' It works...

I then started looking at the implementation of the Range Ruby class, which is contained in range.c , specifically, the range_cover function. I also added some tests that I wanted to pass to the test_cover method in the test-suite.

After finding the right place to make the change, the next (trickier) step is to get stuck in and make the required changes in C and add tests. I made heavy use of lldb to help debug errors, and referred to the excellent Ruby Under a Microscope book to help understand MRI’s internals.

Once I had (what I thought was) a working patch, I committed my changes in git, then exported the (v1) patch with git format patch HEAD~1 -v1 , and uploaded the file to the MRI bug-tracker.

After some discussion, fixing an error and encorporating some feedback I was asked by Matz himself to justify the change. With my use-case, and Masaya Tarui’s (an MRI-committer), Matz accepted the suggested change, but asked for me to change the implementation slightly - rather than add a new method, extend cover? to also accept a Range argument.

After making the requested change, and having a few more discussions, my final amended patch was exported with git format patch HEAD~1 -v6 , uploaded to bug-tracker, and committed a few days later, and released in Ruby 2.6.0.

Conclusion