2
\$\begingroup\$

I am writing a gem for users to interact with a third party api. I want users to have easy and natural access to objects and properties delivered by the JSON payload.

The collection of entities delivered are called Entries so after making an http request I pass the json response to an Entries object like so:

  class Entries
    include Enumerable

    attr_reader :body, :entries

    def initialize(body)
      @body = body
      @entries = body.map { |entry| Entry.new(entry) }
    end

    def each(&block)
      entries.each(&block)
    end
  end

And for my single Entry object I have written:

  class Entry

    attr_reader :properties, :attributes

    def initialize(attributes)
      @attributes = attributes
      @properties = attributes.keys

      properties.each do |prop|
        define_singleton_method prop do
          @attributes[prop]
        end
      end
    end

    def to_s
      "#{attributes[:title]} created on #{attributes[:created_at]}"
    end

    def is_entry?
      true
    end
  end

I haven't tested this much but things seem to be working so far. Users of my gem can now do this:

entries = client.entries(content_type: 'shirts')
entries.each { |entry| puts entry.title }

But I am worried this is an inefficient way of doing things and that perhaps I am using too much memory in these loops.

Also I want to keep magical meta-programming to a minimal but still achieve this. Is this a reasonable way to do this in Ruby? Would love some feedback and advice.

\$\endgroup\$
2
\$\begingroup\$

The first thing I'd do is to create your Entry objects on demand, rather than in Entries#initialize:

class Entries
  include Enumerable
  ...
  def initialize(body)
    @body = body
  end
  ...
  def each(&block)
    @body.each do |entry| 
      block.call(Entry.new(entry))
    end
  end
end

As for dynamically creating methods for each attribute, I'd be wary of doing that. Without knowing your JSON API schema, there could be two possibilities going on:

  1. Each Entry always has the same attributes, so you are spending processing time and memory dynamically generating the same methods every time.
  2. Each Entry may have wildly differing attributes – in which case your clients won't know at runtime which methods each entry object actually responds to, and will have to use some checks of their own, e.g., #respond_to? in order for their code to work reliably.

In either case, you really want your clients to know that each Entry object they receive will have the same attributes at all times. I'd be inclined to explicitly define each attribute method, e.g.:

class Entry
  attr_reader :attributes
  ...
  def title
    attributes[:title]
  end
end

This gives you the option of how to deal with missing attributes in the API. For example, if an array field, such as authors, is missing, you could choose to return an empty array instead:

def authors
  attributes.fetch(:authors, [])
end

I know the non-metaprogramming way can seem like a more boring way of doing things, but even though your entry.rb source code will be much longer, everything will be much clearer and easier to maintain and debug.

And at the end of the day, your gem's users will end up with an interface which is much more predictable – and the methods you include will be more easily documentable through YARD or any other automated documentation tool.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I too am wary of using or overusing metaprogramming. The API schema consists of a small number of common fields for which I could manually define methods as you suggest. However, the majority of fields are undetermined until runtime. In that case, is there any option I have other than metaprogramming. Or should I take a more high level decision to not include dynamic methods in my gem and instead decide to just give over the raw json response translated into a ruby object? Also just for context - this is a headless CMS I have to interact with. So you can imagine the fields are set by users \$\endgroup\$ – Amit Erandole Dec 22 '16 at 15:21
  • \$\begingroup\$ In which case, you may be better off using static methods for any properties that must always be there, e.g., title, and provide an attributes hash to hold those custom ones. If you really wanted your clients to be able to call entry.my_custom_attribute, you could override method_missing, check the method name and if it exists in the attributes, return the value. (With method_missing, always remember to call super for any values you don't handle yourself.) \$\endgroup\$ – Scott Matthewman Dec 22 '16 at 15:47
  • \$\begingroup\$ Gotcha. One last question: Is method_missing just as fast as define_method? Is there any comparison for performance? \$\endgroup\$ – Amit Erandole Dec 22 '16 at 15:49
  • \$\begingroup\$ You should benchmark for your own use case. define_method will run once for every attribute, regardless of whether your clients use it; method_missing will only run on demand, so the relative performance will vary depending on how many custom fields are accessed, I guess. \$\endgroup\$ – Scott Matthewman Dec 22 '16 at 16:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.