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.

janos
113k15 gold badges154 silver badges396 bronze badges
asked Dec 22, 2016 at 11:22
\$\endgroup\$
0

1 Answer 1

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.

answered Dec 22, 2016 at 15:13
\$\endgroup\$
4
  • \$\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\$ Commented Dec 22, 2016 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\$ Commented Dec 22, 2016 at 15:47
  • \$\begingroup\$ Gotcha. One last question: Is method_missing just as fast as define_method? Is there any comparison for performance? \$\endgroup\$ Commented Dec 22, 2016 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\$ Commented Dec 22, 2016 at 16:15

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.