2
\$\begingroup\$

I am writing an API wrapper and the endpoint takes dates in a very specific format.

The user of the API can pass in the parameters in whatever format they prefer, but regardless of what they pass in, I want to be able to clean up their input prior to submitting their query.

My question centers around the best way to update the options hash in place, and I have thought of a few possible ways to implement.

  1. A helper method inside the class so you can overwrite options = reformat_hash(options)
  2. A singleton on that specific variable

    def options.clean_up!
      # see internals below
    end
    
  3. Or open up Hash and do the cleaning from the class

    class Hash
      def clean_hash!
        self.each { |key, value|
          if value.is_a? Date
            self[key] = value.strftime('%Y-%m-%d %H:%M:%S')
          else
            self[key] = value.to_s
          end
        }
      end
    end
    

    so that I can just call it on whatever the variable may be named like:

    def api_request(options={})
      options.clean_hash!
      # the options variable is now clean and I can pass it to the api
      HHTParty.get(path, :query => options).parsed_response
    end
    

Is there a best practice for modifying or formatting hashes after they're passed into a method?

I feel like #3 is the neatest, but should I be worried about opening up Hash to do this?

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Here, while it may look nice, the method clean_hash is not general enough to be valid across all Hashes. So adding a method such as clean_hash to all Hashes would only serve to increase the coupling which is bad. A second problem is that you are mutating your method argument which is almost never advisable.

The solution is to define the clean method outside, perhaps as a part of your internal API object and call HHTParty.get(path, :query => clean(options)).parsed_response.

I would also define the clean method this way

def clean(opt)
  Hash[opt.collect{|k,v| [k,v.is_a? Date : v.strftime('%Y-%m-%d %H:%m:%S'):v.to_s ]}]
end
\$\endgroup\$
1
  • \$\begingroup\$ thanks blufox. I think I will include it in the get query like that, although I may use a different enumerable based on this gist.github.com/3158814. \$\endgroup\$
    – jstim
    Jul 22, 2012 at 8:33
2
\$\begingroup\$

Rather than extend the hash class with something that "isn't general enough to be valid across all classes" (blufox: well said), make a subclass of Hash named CleanHash.

I added || value.is_a?(Time) so you could format Time in addition to Date.

class CleanHash < Hash
  def self.[](opts)
    super(opts).clean!
  end

  def []=(key,value)
    super(key,clean(value))
  end

  def clean(value)
    if value.is_a?(Date) || value.is_a?(Time)
      value.strftime('%Y-%m-%d %H:%M:%S')
    else
      value.to_s
    end
  end

  def clean!
    self.each { |key, value|
      self[key] = value # don't clean(value) or it will clean twice
    }
  end
end

Examples below.

# example 1
h = CleanHash[:s=>"x",:n=>9,:d=>Date.today,:t=>Time.now]
# => {:s=>"x", :t=>"2012-07-20 09:21:18", :d=>"2012-07-20 00:00:00", :n=>"9"}
h[:d2] = Date.yesterday
# => {:d2=>"2012-07-19 00:00:00", :s=>"x", :t=>"2012-07-20 09:21:18", :d=>"2012-07-20 00:00:00", :n=>"9"}

# example 2
def api_request(options={})
  clean_options = CleanHash[options]
  # the options variable is now clean and I can pass it to the api
  HHTParty.get(path, :query => clean_options).parsed_response
end

It's still a Hash so you can pass it around as if it were a Hash.

h = CleanHash[:foo=>Date.today]
#=> {:foo=>"2012-07-20 00:00:00"}
h.class
#=> CleanHash
h.is_a?(Hash)
#=> true
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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