7 Ruby Standard libraries you should get to grips with

phawk

Pete Hawkins

Posted on October 22, 2021

7 Ruby Standard libraries you should get to grips with

Ruby has a lot of useful things in its Standard Library.

Learning what’s included and how to use it can be very helpful, especially when writing small scripts where you don’t want to pull in bundler and start making a Gemfile etc.

These libraries are robust, maintained by the core team and should be extremely reliable, and outlive most Ruby gems you come across.

1. Net/HTTP and open-uri

I wanted to start with something I need in most applications I build, and that’s the ability to perform an HTTP request.

In the past I have opted to use the HTTP.rb gem, but for simple tasks it’s really useful to learn Net/http or even open-uri for simple GET requests.

require "net/http"
require "json"

# Net/HTTP GET request
uri = URI("https://swapi.dev/api/films/1/")
resp = Net::HTTP.get(uri)
json = JSON.parse(resp)
puts json["title"] # => "A New Hope"

# Net/HTTP POST request
uri = URI("https://example.org/users")
params = {
  "name" => "Bob",
  "email" => "bob@example.org",
}
res = Net::HTTP.post_form(uri, params)
puts res.body

# Net/HTTP Full form: delete/put/patch requests
uri = URI("http://example.org/some_path")

Net::HTTP.start(uri.host, uri.port) do |http|
  request = Net::HTTP::Delete.new uri
  # or request = Net::HTTP::Patch.new uri
  # or request = Net::HTTP::Put.new uri

  response = http.request(request) # Net::HTTPResponse object
  puts response.body
end
Enter fullscreen mode Exit fullscreen mode

Things are even simpler with open-uri if you only need a GET request:

require "open-uri"
require "json"

URI.open("https://swapi.dev/api/films/1/") do |io|
  json = JSON.parse(io.read)
  puts json["title"] # => "A New Hope"
end

# or
uri = URI("https://swapi.dev/api/films/1/")
puts uri.open.read
Enter fullscreen mode Exit fullscreen mode

2. CSV

When I’m creating an admin section, I often need to export some form of reports to measure how an application is performing. Ruby has this built in and to take some data and place it into a CSV is pretty simple.

require "csv"

module CustomerExport
  module_function

  def generate(customers)
    CSV.generate do |csv|
      # Define the headers
      csv << %w[id name email country_code created_at updated_at]

      # Add a new row for each customer record
      customers.each do |customer|
        csv <<
          [
            customer.id,
            customer.name,
            customer.email,
            customer.country_code,
            customer.created_at,
            customer.updated_at
          ]
      end
    end
  end
end

CustomerExport.generate(Customer.all)
Enter fullscreen mode Exit fullscreen mode

And if you wanted to make a Rails route to download this...

module Admin
  class ExportsController < AdminBaseController
    def customers
      customers = Customer.all
      respond_to do |format|
        format.csv do
          filename = "customers-#{Time.zone.now.strftime("%Y-%m-%d")}"
          filename += ".csv"
          headers["Content-Type"] ||= "text/csv"
          headers["Content-Disposition"] =
            "attachment; filename=\"#{filename}\""
          render plain: CustomerExport.generate(customers)
        end
      end
    rescue => e
      flash[:error] = "Failed to generate export: #{e.message}"
      redirect_to admin_path
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Parsing CSVs (for example: importing CSV files from users) is also built in and you can read the documentation for more on that.

3. SimpleDelegator

I like to use SimpleDelegator to add extra behaviour to ActiveRecord objects, such as having Stripe specific code wrapped up into StripeWrappedUser or something similar, this avoids placing lots of methods on your ActiveRecord object, but can then augment that class with extra functionality.

require "delegate"
require "digest"

class User < Struct.new(:id, :email)
end

class BillingUser < SimpleDelegator
  def billing_system_id
    Digest::SHA1.hexdigest(id.to_s + email)
  end
end

bob = User.new(1, "bob@example.com")
bob = BillingUser.new(bob)
puts bob.email # => bob@example.com
puts bob.billing_system_id # => 186da93f0b39990e034a80cc4b45c8ec253f2a1a
Enter fullscreen mode Exit fullscreen mode

4. Struct and OpenStruct

Struct is included in Ruby core, it doesn’t need required, and is the stricter of the two objects, and also much more performant.

I find it particularly useful for two things:

  1. As a value object, giving a name to some piece of data, that would otherwise be a Hash.
  2. In tests to create mock objects that can be passed in to represent fake things, like a FakeUser.
User = Struct.new(:id, :email)
alice = User.new(1, "alice@example.org")
puts alice.id # => 1
puts alice.email # => alice@example.org

# Can also be defined with additional methods
class User < Struct.new(:id, :email)
  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end
end
Enter fullscreen mode Exit fullscreen mode

OpenStruct meanwhile is a lot more flexible and can be a neat wrapper for a Hash or JSON from an API etc.

require "ostruct"

data = OpenStruct.new(id: 5, name: "Jeffrey")

# New attributes can be set at runtime:
data.email = "jeff@example.org"

puts data.id # => 5
puts data.name # => "Jeffrey"
puts data.email # => "jeff@example.org"
Enter fullscreen mode Exit fullscreen mode

5. PStore

PStore is a really simple key-value datastore and can be helpful when you need somewhere to store data in a non-production system (i.e. this is not a good replacement for ActiveRecord).

require "pstore"

store = PStore.new("my_db.pstore")

# Write some data
store.transaction do
  store["person-1"] = { name: "Bob", email: "bob@example.org" }
end

# Read some data
store.transaction(true) do
  puts store["person-1"] # => {:name=>"Bob", :email=>"bob@example.org"}
end
Enter fullscreen mode Exit fullscreen mode

6. Minitest

When working on smaller scripts, I like to pull in a test framework and make some assertions while test-driving some code.

I usually prefer RSpec when working with Rails, but it’s more effort to set up than Minitest, which comes included with Ruby.

The nice thing about Minitest, it is just Ruby – there’s nothing magical going on and it’s very easy to understand.

require "minitest/test"
require "minitest/autorun"

class APIResponse < Struct.new(:body, :error)
  def success?
    error.nil?
  end
end

class APIResponseTest < Minitest::Test
  def test_successful_responses
    resp = APIResponse.new("Well done!", nil)
    assert resp.success?
  end

  def test_error_responses
    resp = APIResponse.new(nil, StandardError.new("400 bad request"))
    refute resp.success?
  end
end

# Running:
# ..
# Finished in 0.000957s, 2089.8640 runs/s, 2089.8640 assertions/s.
# 2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
Enter fullscreen mode Exit fullscreen mode

7. Logger

Using logger can clean up a lot of your debug output when writing a script and you can then control the log level to show more/less output. Note: Use Rails.logger in Rails.

require "logger"

logger = Logger.new(STDOUT)

# Setting :warn will only show warning, error and fatal.
logger.level = :warn

# Can tweak log output to be less verbose
logger.formatter = proc do |severity, datetime, progname, msg|
  "#{severity[0...1]} #{datetime.strftime('%H:%M %b %d %Y')}: #{msg}\n"
end

logger.debug("Created logger") # wont be displayed in current log level
logger.info("Program started") # wont be displayed in current log level
logger.warn("Nothing to do!")
logger.error("Something really bad!")

# =>
# W 14:56 Oct 22 2021: Nothing to do!
# E 14:56 Oct 22 2021: Something really bad!
Enter fullscreen mode Exit fullscreen mode

There’s plenty more in the Ruby Standard Library to get familiar with, but these 7 are things I come back to, time and time again.

💖 💪 🙅 🚩
phawk
Pete Hawkins

Posted on October 22, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related