I'm trying to access a hash by using an regular class method like this:
aHash.key
But I get this error:
undefined method `key1' for {:key1=>"value1", :key2=>"value2"}:Hash
I defined a 'method_missing' in a Hash class as suggested here:
class Hash
def method_missing(method, *params)
method = method.to_sym
return self[method] if self.keys.collect(&:to_sym).include?(method)
super
end
end
I know for sure, that when I call 'aHash.key' it does not use the Hash I defined. If I look at one on my gems, it shows this:
require "activerecord-postgres-hstore/hash"
So I checked this file, and indeed they have implemented another hash. I believe I should add 'method_missing' in there but can it be done without modified the gem?
Maybe I'm not getting how class overloading works with ruby, so the answer might be something else.
Like SporkInventor said, what you are doing is very dangerous and bad practice, you shouldn't patch Hash. Instead, like he said, use OpenStruct from Ruby's standard library. If you need, change your code to wrap the Hash into an OpenStruct when returning it.
require 'ostruct'
result = OpenStruct.new({ :a => 1, :b => 2 })
result.a # => 1
So if you really need to monkey-patch, rather monkey patch the method that is returning the hash to wrap the result in an OpenStruct, instead of patching Hash. Or if you need it to really be a Hash (obj.is_a? Hash #=> true) then create a subclass of hash with your method_missing implementation and then monkey-patch the method returning the Hash to return your OpenHash (or whatever you call it).
class OpenHash < Hash
def self.from_hash(hash)
h = OpenHash.new
h.merge!(hash)
h
end
def method_missing(method, *params)
method = method.to_sym
return self[method] if self.keys.collect(&:to_sym).include?(method)
super
end
end
For (made up) example you have a Hstore class which has a method return_hash which returns the hash. Then patch it like this:
class Hstore
def return_hash_with_ostruct
OpenStruct.new(return_hash_without_ostruct)
end
alias_method_chain :return_hash, :ostruct
end
Hstore.new.return_hash.class # => OpenStruct
Monkey-patching standard library is very bad practice!
Related
For example I have class with two methods:
class Example < ActiveRecord::Base
def method_one(value)
end
def method_two
end
end
and method in controller where I call them:
def example
ex = Example.find(params[:id])
ex.send(params[:method], params[:value]) if ex.respond_to?(params[:method])
end
But the problem comes when I try to call method_two
ArgumentError (wrong number of arguments (1 for 0))
It happens because params[:value] returns nil.
The easiest solution is:
def example
ex = Example.find(params[:id])
if ex.respond_to?(params[:method])
if params[:value].present?
ex.send(params[:method], params[:value])
else
ex.send(params[:method])
end
end
end
I wonder if there is any better workaround to do not pass argument if it's null.
What you are trying to do can be really dangerous, so I recommend you filter the params[:method] before.
allowed_methods = {
method_one: ->(ex){ex.method_one(params[:value])}
method_two: ->(ex){ex.method_two}
}
allowed_methods[params[:method]]&.call(ex)
I defined an Hash mapping the methods name to a lambda calling the method, which handles arguments and any special case you want.
I only get a lambda if params[:method] is in the allowed_methods hash as a key.
The &. syntax is the new safe navigation operator in ruby 2.3, and - for short - executes the following method if the receiver is not nil (i.e. the result of allowed_methods[params[:method]])
If you're not using ruby >= 2.3, you can use try instead, which have a similar behavior in this case :
allowed_methods[params[:method]].try(:call, ex)
If you don't filter the value of params[:method], then a user can just pass :destroy for example to delete your entry, which is certainly not what you want.
Also, by calling ex.send ..., you bypass the object's encapsulation, which you usually shouldn't. To use only the public interface, prefer using public_send.
Another point on the big security flaw of you code:
eval is a private method defined on Object (actually inherited from Kernel), so you can use it this way on any object :
object = Object.new
object.send(:eval, '1+1') #=> 2
Now, with your code, imagine the user puts eval as the value of params[:method] and an arbitrary ruby code in params[:value], he can actually do whatever he wants inside your application.
If you understand what you are doing, there are easier workarounds:
def method_two _ = nil
end
or
def method_two *
end
It works as well the other way round:
def method_one *args
end
def method_two *
end
and:
ex.public_send(params[:method], *[params[:value]]) \
if ex.respond_to?(params[:method])
Sidenote: prefer public_send over send unless you are explicitly calling private method.
Using splatted params without modifying the methods signatures:
ex.public_send(*[params[:method], params[:value]].compact)
Is there a way to specify a method to use for serialization when using Oj.dump rather than having it just default to to_json or as_json?
Oj doesn't have the option to set a default method to call, but it'll happily serialize the result of calling that method yourself.
Oj.dump(data.special_as_json)
This is also a good place for a presenter object that implements the method you want as to_json.
class Glove
private attr_reader :obj
def initialize(obj)
#obj = obj
end
def as_json(opts = {})
{
# ...
}
end
end
Oj.dump(Glove.new(data))
Since it seems like you're trying to avoid typing anything beyond Oj.dump, you can write a wrapper around Oj that does the work.
module Ok
def self.dump(obj)
Oj.dump(obj.special_as_json)
end
end
Ok.dump(data)
And of course, monkeypatching Oj is always an option, but not a good one.
Try out
Oj.generate({a: 'test'})
my_hash = {a: 'test'}
Oj.dump(my_hash, mode: :compat)
I have a method like this:
class Foo < ActiveRecord::Base
def load_data(data)
self.foo = data[:foo] if data.has_key?(:foo)
self.bar = data[:bar] if data.has_key?(:bar)
self.moo = data[:moo] if data.has_key?(:moo)
self.save
end
end
I want to write the method like this:
[:foo, :bar, :moo].each do |sym|
# need some trick here
self.sym = data[sym] if data.has_key?(sym)
end
Of course this method doesn't work, how can I assign a value to a Model column by using a symbol?
vee's answer is correct for the general case, but since this is Rails and ActiveRecord, you can take some nice shortcuts:
def load_data(data)
update_attributes data.slice(:foo, :bar:, :moo)
end
This works because data.slice filters your data hash to just the given keys, and then update_attributes will set those values in your model and invoke #save. When the keys aren't present, they aren't written, so you don't need to check and assign each key separately.
If you don't care about filtering the inbound data and simply assigning the keys given to the model, then just update_attributes data and you're done.
You can use send:
[:foo, :bar, :moo].each do |sym|
# need some trick here
send "#{sym}=", data[sym] if data.has_key?(sym)
end
In Python, you can write a decorator for memoizing a function's response.
Is there something similar for Ruby on Rails? I have a model's method that makes a query, which I would like to cache.
I know I can do something inside the method, like:
def foo(param)
if self.cache[param].nil?
self.cache[param] = self.get_query_result(param)
else
self.cache[param]
end
end
However, given that I would do this often, I'd prefer a decorator syntax. It is clearer and better IMO.
Is there something like this for Ruby on Rails?
I usually do this using custom accessors, instance variables, and the ||= operator:
def foo
#foo ||= something_or_other
end
something_or_other could be a private method on the same class that returns the object that foo should be.
EDIT
Here's a slightly more complicated solution that lets you cache any method based on the arguments used to call them.
class MyClass
attr_reader :cache
def initialize
#cache = {}
end
class << self
def cacheable(symbol)
alias_method :"_#{symbol}_uncached", symbol
define_method(symbol) do |*args|
cache[[symbol, *args]] ||= (send :"_#{symbol}_uncached", *args)
end
end
end
end
How this works:
class MyClass
def foo(a, b)
a + b
end
cacheable :foo
end
First, the method is defined normally. Then the class method cacheable is called, which aliases the original method to a new name, then redefines it under the original name to be executed only if it's not already cached. It first checks the cache for anything using the same method and arguments, returns the value if present, and executes the original method if not.
http://martinfowler.com/bliki/TwoHardThings.html:
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
Rails has a lot of built in caching(including query caching). You might not need to do anything:
http://guides.rubyonrails.org/caching_with_rails.html
Here is a recent blog post about problems with roll your own caching:
http://cmme.org/tdumitrescu/blog/2014/01/careful-what-you-memoize/
When I load up the Rails console in my project, I can do this:
{}.to_json
But I can't do this:
"{}".from_json
Even when I require the json gem first, it doesn't work. Does a from_json method even exist?
No. from_json does not exist. If you want to get a Ruby hash from a JSON string, you can use the JSON#parse method. Example below:
json = JSON.parse("{\"hello\": \"world\"}")
the above returns {"hello"=>"world"}
Your {}.to_json assumption is correct. However when we're taking JSON data (or any textual data for that matter) and converting it to some native structure we call that process parsing. An instance method of a class that parsed some textual data and initialized its attributes with that data would be odd - a bit out of place. Instead what you typically see are static factory methods (known as class methods in Ruby), like:
JSON.parse "{}"
This will return a hash in Ruby.
No, as mentiond below and above, a .from_json method does not exist. But its kinda simple to implement on you own:
require 'json'
class String
def from_json
JSON.load(self) rescue false
end
end
Just happened across this. from_json is now in the framework.
From the docs...
class Person
include ActiveModel::Model
include ActiveModel::AttributeMethods
include ActiveModel::Serializers::JSON
attr_accessor :name, :age, :awesome
def attributes=(hash)
hash.each do |key, value|
send("#{key}=", value)
end
end
def attributes
instance_values
end
end
I generally use it to serialize JSON to ActiveModel objects...
json.each do |object|
object = object.to_json
myModel = MyModel.new
myModel.from_json(object)
end
The error reporting is kinda bad with the "from_json" method, if I was dealing with malformed JSON I would get the undefined method error.
Years later, HTH!