Wilson E. Husin
Would it be bad?

Would it be bad?

Metaprogramming JSON to Ruby

Photo by Claudio Schwarz on Unsplash

Metaprogramming JSON to Ruby

And how to get your code reviewers hate it.

Wilson E. Husin's photo
Wilson E. Husin

Published on Dec 8, 2021

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

  • The Ruby parts

I was looking for an excuse to revisit Ruby after writing a lot of Go in the recent months, so I thought I would do something with Bored API.

I had learned about Bored API recently from a talk by Justin Searls. In short, Bored API is an endpoint returning random values with the option of querying based on parameters.

❯ curl -sSL https://boredapi.com/api/activity?type=busywork | jq
{
  "activity": "Draft your living will",
  "type": "busywork",
  "participants": 1,
  "price": 0,
  "link": "https://www.investopedia.com/terms/l/livingwill.asp",
  "key": "2360432",
  "accessibility": 0.5
}

The Ruby parts

Let's try making a Ruby client for Bored API! Something simple that would let us do:

Activity.new(response).type # => "busywork"

Naive

Given a JSON string to process, a boring (ha ha) approach can look something like this:

require "json"

class Activity
  attr_reader :activity,
    :type,
    :participants,
    :price,
    :link,
    :key,
    :accessibility

  # @param raw_string [String] JSON string returned by Bored API
  def initialize(raw_string)
    response_data = JSON.parse raw_string

    @activity = response_data["activity"]
    @type = response_data["type"]
    @participants = response_data["participants"]
    @price = response_data["price"]
    @link = response_data["link"]
    @key = response_data["key"]
    @accessibility = response_data["accessibility"]
  end

  # Quack
  def name
    activity
  end
end

Now with a valid input, we can expect the following to work:

Activity.new(response).name # => "Draft your living will!"

Alright, that worked. attr_reader saved us from writing multiple method definitions for the properties. However, we are currently front-loading the data parsing, which we can optimize with memoization tricks.

Lazy evaluation

One trick that we will do here is to store the raw_string value and parse only when needed.

def initialize(raw_string)
  @raw_string = raw_string
end

def response_data
  @response_data ||= JSON.parse @raw_string
end

Although that means we can't use attr_reader anymore. To maintain functionality, we need to add something like the following to every property.

def participants
  @participants ||= response_data["participants"]
end

Now that class doesn't look so elegant anymore, does it? More than half of the methods defined in the class look like copy-pasted template.

(Mis)using method_missing

If you, like me, have a fond memory of the talk Wat by Gary Bernhardt, you might remember that Ruby has a method method_missing in BasicObject (doc).

So, instead of writing def participants like above, we can write one method to cover all the properties of an Activity.

PROPERTIES = %i[activity type participants price link key accessibility]
def method_missing(method_name, *args, &block)
  super unless PROPERTIES.include? method_name
  response_data[method_name.to_s]
end

More memoization with class_eval

Some might say that they dislike method_missing and would rather use class_eval (doc) like how Faraday gem does it. This approach would also allow us to bring back memoization per property, instead of accessing the Hash object on every method call.

PROPERTIES = %i[activity type participants price link key accessibility]
PROPERTIES.each do |prop|
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def #{prop}
      @#{prop} ||= response_data["#{prop}"]
    end
  RUBY
end

Following convention with define_method

We have reached a point where we stray further from truth, where arguably, this is an over-complication of attr_reader. What if we could make something like attr_reader? Let me introduce you to define_method (doc).

module Bored
  def prop_reader(*props)
    props.each do |prop|
      define_method prop do |p|
        response_data[p.to_s]
      end
    end
  end

  class Activity
    prop_reader :activity,
      :type,
      :participants,
      :price,
      :link,
      :key,
      :accessibility
    ...
  end
end

Thanks for reading this silly thought experiment! I had lots of fun writing it.

In a less silly fashion, I once wrote about getting familiar with Ruby syntax and building duck-typed objects in Ruby.

If you have thoughts, I would love to hear them in any social platforms / links available on this blog!

 
Share this