3
\$\begingroup\$

I have Ruby models which are populated from the responses of API calls in the following way:

  1. JSON.parse converts the response to a Hash
  2. the Hash is passed into the initialize method of a class
  3. the initialize method converts camelCase hash keys and assigns underscore_case instance variables
  4. controller code works with these instances and converts back to json to send to the browser

This works fine, but some of these response objects are large. Others are arrays of large objects.

Profiling shows that this process consumes a lot of CPU (and memory, but that is less of a concern) -- which makes sense given that I create hashes in order to create objects, and the back and forth between camelCase and underscore_case happens A LOT -- so what libraries or techniques have you come across which solve this problem?


Here is an oversimplified example:

JSON response from a third party API (unlikely to change):

"{\"abcDef\": 123, \"ghiJkl\": 456, \"mnoPqr\": 789}"

Class definition (attributes unlikely to change):

class Data
 attr_accessor :abc_def, :ghi_jkl, :mno_pqr
 def initialize(attributes = {})
 attributes.each do |key, val|
 send "#{key.underscore}=".to_sym, val
 end
 end
 def as_json
 instance_variables.reduce({}) do |hash, iv|
 iv_name = iv.to_s[1..-1]
 v = send(iv_name) if self.respond_to?(iv_name)
 hash[iv_name.camelize(:lower)] = (v.as_json(options) if v.respond_to?(:as_json)) || v
 hash
 end
 end
end

Controller:

get '/' do
 d = Data.new JSON.parse(api.get)
 # ... do some work ...
 content_type 'application/json'
 d.to_json
end
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Nov 14, 2014 at 15:02
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

TheJSON schema and data class attributes are unlikely to change. Consider implementing the class to encapsulate the JSON hash, adapting it to the ruby idioms and whatever other custom operations you've added. This should greatly decrease the cost and complexity of deserialization.

answered Nov 15, 2014 at 4:33
\$\endgroup\$
0
\$\begingroup\$

Your approach looks OK. It might consume a bunch of CPU power, but is that actually an issue? It's a little unclear from your question if you just noticed heavier CPU load, or if it's actually causing trouble.

If it is an issue, I guess you might use Data as a wrapper around the hash instead. The idea would be to avoid whole-sale conversion back and forth, and instead only do it when necessary.

class Data
 def initialize(json)
 @values = json
 end
 def method_missing(method, *args, &blk)
 # grab base property name, and, if the method is a setter,
 # the `=` at the end - it'll be nil for a reader method
 (/(?<name>.+?)(?<setter>=)?$/ =~ method.to_s) # thanks to Naklion
 property = name.camelize
 if @values.has_key?(property)
 setter ? @values[property] = args.first : @values[property]
 else
 super
 end
 end
 def as_json
 @values
 end
end

Alternatively, if method_missing is casting the net too wide, you can go the meta-programming route and do something like this, provided you declare a list of methods you want (like you do now with attr_accessor):

class Data
 def initialize(json)
 @values = json
 end
 # define getters/setters for the properties we want
 %{abc_def ghi_jkl mno_pqr}.each do |name|
 property = name.camelize
 define_method(name) do
 @values[property]
 end
 define_method("#{name}=") do |value|
 @values[property] = value
 end
 end
 def as_json
 @values
 end
end

Overall, though, it seems like a lot compared to just using the hash. From your question it's unclear why you even need the Data class. Yes, there's some syntactic sugar in being able to write data.abc_def instead of raw hash access, but if the cost is a performance hit, and said hit is too much, it would seem that the simplest solution is to just use the plain hash.

answered Nov 14, 2014 at 17:33
\$\endgroup\$
4
  • 1
    \$\begingroup\$ (/(?<name>.+?)(?<setter>=)?$/ =~ method.to_s) \$\endgroup\$ Commented Nov 16, 2014 at 5:57
  • \$\begingroup\$ @Nakilon Neat! I didn't actually know you could do that \$\endgroup\$ Commented Nov 17, 2014 at 1:27
  • \$\begingroup\$ stdlib, stdlib, stdlib, stdlib, stdlib, stdlib... \$\endgroup\$ Commented Nov 17, 2014 at 1:51
  • 1
    \$\begingroup\$ @Nakilon yes thank you, I figured. I apologize for not knowing (or remembering) everything about Ruby. \$\endgroup\$ Commented Nov 17, 2014 at 1:54

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.