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.
- A helper method inside the class so you can overwrite
options = reformat_hash(options)
A singleton on that specific variable
def options.clean_up! # see internals below end
Or open up
Hash
and do the cleaning from the classclass 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?
2 Answers 2
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
-
\$\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– jstim2012年07月22日 08:33:38 +00:00Commented Jul 22, 2012 at 8:33
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