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.
1 Answer 1
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:
- Each
Entry
always has the same attributes, so you are spending processing time and memory dynamically generating the same methods every time. - 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.
-
\$\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– Amit Erandole2016年12月22日 15:21:51 +00:00Commented 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 anattributes
hash to hold those custom ones. If you really wanted your clients to be able to callentry.my_custom_attribute
, you could overridemethod_missing
, check the method name and if it exists in the attributes, return the value. (With method_missing, always remember to callsuper
for any values you don't handle yourself.) \$\endgroup\$ScottM– ScottM2016年12月22日 15:47:19 +00:00Commented Dec 22, 2016 at 15:47 -
\$\begingroup\$ Gotcha. One last question: Is
method_missing
just as fast asdefine_method
? Is there any comparison for performance? \$\endgroup\$Amit Erandole– Amit Erandole2016年12月22日 15:49:08 +00:00Commented 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\$ScottM– ScottM2016年12月22日 16:15:38 +00:00Commented Dec 22, 2016 at 16:15
Explore related questions
See similar questions with these tags.