Objectify Ruby Hashes from/to JSON API

2019-07-29 04:46发布

问题:

I just released a ruby gem to use some JSON over HTTP API:

https://github.com/solyaris/blomming_api

My naif ruby code just convert complex/nested JSON data structures returned by API endpoints (json_data) to ruby Hashes ( hash_data), in a flat one-to-one transaltion (JSON to ruby hash and viceversa). Tat's fine, but...

I would like a programming interface more "high level". Maybe instatiating a class Resource for every endpoint, but I'm confused about a smart implementation.

Let me explain with an abstract code.

Let say I have a complex/nested JSON received by an API, usually an Array of Hashes, recursively nested as here below (imagination example):

json_data = '[{
    "commute": {
        "minutes": 0,
        "startTime": "Wed May 06 22:14:12 EDT 2014",
        "locations": [
            {
                "latitude": "40.4220061",
                "longitude": "40.4220061"
            },
            {
                "latitude": "40.4989909",
                "longitude": "40.48989805"
            },            
            {
                "latitude": "40.4111169",
                "longitude": "40.42222869"
            }            
        ]
    }
},
{
    "commute": {
        "minutes": 2,
        "startTime": "Wed May 28 20:14:12 EDT 2014",
        "locations": [
            {
                "latitude": "43.4220063",
                "longitude": "43.4220063"
            }
        ]
    }
}]'

At the moment what I do, when I receive a similar JSON form an API is just:

# from JSON to hash 
hash_data = JSON.load json_data

# and to assign values:
coords = hash_data.first["commute"]["locations"].last
coords["longitude"] = "40.00" # was "40.4111169"
coords["latitude"] = "41.00" # was "40.42222869"

that's ok, but with awfull/confusing syntax. Instead, I probably would enjoy something like:

# create object Resource from hash
res = Resource.create( hash_data )

# ... some processing

# assign a "nested" variables: longitude, latitude of object: res
coords = res.first.commute.locations.last
coords.longitude = "40.00" # was "40.4111169"
coords.latitude = "41.00" # was "40.42222869"

# ... some processing

# convert modified object: res into an hash again:
modified_hash = res.save

# and probably at least I'll recover to to JSON:
modified_json = JSON.dump modified_hash

I read intresting posts: http://pullmonkey.com/2008/01/06/convert-a-ruby-hash-into-a-class-object/ http://www.goodercode.com/wp/convert-your-hash-keys-to-object-properties-in-ruby/

and copying Kerry Wilson' code, I sketched the implementation here below:

class Resource

  def self.create (hash)
    new ( hash)
  end

  def initialize ( hash)
    hash.to_obj
  end

  def save
    # or to_hash() 
    # todo! HELP! (see later)
  end

end


class ::Hash
  # add keys to hash
  def to_obj
    self.each do |k,v|

      v.to_obj if v.kind_of? Hash
      v.to_obj if v.kind_of? Array

      k=k.gsub(/\.|\s|-|\/|\'/, '_').downcase.to_sym

      ## create and initialize an instance variable for this key/value pair
      self.instance_variable_set("@#{k}", v)

      ## create the getter that returns the instance variable
      self.class.send(:define_method, k, proc{self.instance_variable_get("@#{k}")})

      ## create the setter that sets the instance variable
      self.class.send(:define_method, "#{k}=", proc{|v| self.instance_variable_set("@#{k}", v)})
    end
    return self
  end
end

class ::Array
  def to_obj
    self.map { |v| v.to_obj }
  end 
end
#------------------------------------------------------------

BTW, I studied a bit ActiveResource project (was part of Rails if I well understood). ARes could be great for my scope but the problem is ARes have a bit too "strict" presumption of full REST APIs... In my case server API are not completely RESTfull in the way ARes would expect... All in all I would do a lot of work to subclass / modify ARes behaviours and at the moment I discarded the idea to use ActiveResource

QUESTIONS:

  1. someone could help me to realize the save() method on the above code (I'm really bad with recursive methods... :-( ) ?
  2. Does exist some gem that to the above sketched hash_to_object() and object_to_hash() translation ?

  3. What do you think about that "automatic" objectifying of an "arbitrary" hash coming froma JSON over http APIs ? I mean: I see the great pro that I do not need to client-side static-wire data structures, allowing to be flexible to possible server side variations. But on the other hand, doing this automatic objectify, there is a possible cons of a side effect to allow security issues ... like malicious JSON injection (possible untrasted communication net ...)

What do you think about all this ? Any suggestion is welcome! Sorry for my long post and my ruby language metaprogramming azards :-)

giorgio

UPDATE 2: I'm still interested reading opinions about question point 3: Pros/Cons to create Resource class for every received JSON Pros/Cons to create static (preemptive attributes) / automatich/dynamic nested objects

UPDATE 1: long reply to Simone: thanks, you are right Mash have a sweet .to_hash() method:

require 'json'
require 'hashie'

json_data = '{
    "commute": {
        "minutes": 0,
        "startTime": "Wed May 06 22:14:12 EDT 2014",
        "locations": [
            {
                "latitude": "40.4220061",
                "longitude": "40.4220061"
            },
            {
                "latitude": "40.4989909",
                "longitude": "40.48989805"
            },            
            {
                "latitude": "40.4111169",
                "longitude": "40.42222869"
            }            
        ]
    }
}'

# trasforma in hash
hash = JSON.load json_data

puts hash

res = Hashie::Mash.new hash

# assign a "nested" variables: longitude, latitude of object: res
coords = res.commute.locations.last
coords.longitude = "40.00" # was "40.4111169"
coords.latitude = "41.00" # was "40.42222869"

puts; puts "longitude: #{res.commute.locations.last.longitude}"
puts "latitude: #{res.commute.locations.last.latitude}"

modified_hash = res.to_hash
puts; puts modified_hash

回答1:

This feature is provided by a few gem. One of the most known is Hashie, specifically the class Hashie::Mash.

Mash is an extended Hash that gives simple pseudo-object functionality that can be built from hashes and easily extended. It is designed to be used in RESTful API libraries to provide easy object-like access to JSON and XML parsed hashes.

Mash also supports multi-level objects.



回答2:

Depending on your needs and level of nesting, you may get away with an OpenStruct.

I was working with a simple test stub. Hashie would have worked well, but was a bigger tool than I needed (and added dependency).