ActiveModel's `as_json` doesn't work as expected - ruby

I want to customize json output of my ActiveModel instance with as_json method. But though pass whitelist of attributes in only option, the result includes all attributes.
Here is example:
class Foo
include ActiveModel::Model
attr_accessor :a, :b
end
Foo.new(a: 1, b: 2).as_json
# {"a" => 1, "b" => 2}
Foo.new(a: 1, b: 2).as_json(only: [:a])
# {"a" => 1, "b" => 2}
# but I expect {"a" => 1}
Is it I doing something wrong?

I forgot include ActiveModel::Serializers::JSON module and attributes method. After this fix all work as expected:
class Foo
include ActiveModel::Model
include ActiveModel::Serializers::JSON
attr_accessor :a, :b
def attributes
{'a' => nil, 'b' => nil}
end
end
Foo.new(a: 1, b: 2).as_json
# {"a" => 1, "b" => 2}
Foo.new(a: 1, b: 2).as_json(only: [:a])
# {"a" => 1}

Related

setter method for hash with send()

Is it possible in Ruby to call setter method for hash with send method?
Just like I can do this:
h = {a: "a", b: "b"}
h.send(:[], :a)
and GET value "a" out of this hash, can I set value for key :a?
Yes. The method then is []=
Example:
hash = {:a => 1, :b => 2}
hash.send :[]=, :a, 3
hash # => {:a => 3, :b => 2}

||= with hash wrapped in a method

I put a hash in a method:
def example
#_hash ||= {:a => {}, :b => {}}
end
I call that hash from another method, and add key/value to the sub-hash like so:
example[:a][:c] = "test"
This will change the hash to:
{:a => {:c => "test"}, :b => {}}
I don't get why I can still update the :c hash by calling the method again like so:
example[:a][:c] = "test2" #=> {:a => {:c => "test2"}, :b => {}}
This is odd because the equals/or operator should not assign the values again if #_hash is not nil/false. So I assume we do example.[]= method to change the value of an individual hash key/value. If that's true, then why is it that, when I remove ||= from the hash method like so:
def example
#_hash = {:a => {}, :b => {}}
end
nothing is changed?
The reason for this is that, when you call the example method again, it correctly returns the following hash:
example #=> {:a => {:c => "test"}, :b => {}} # variable like syntax
example() #=> {:a => {:c => "test"}, :b => {}} # method like syntax
But, when you do this:
example[:a][:c] = "test2"
What you are really doing is:
hash = example() #=> {:a => {:c => "test"}, :b => {}}
hash[:a][:c] = "test2" #=> {:a => {:c => "test2"}, :b => {}}
And, therefore, the hash is updated.

Built-in way to flip ruby hash associations

Assuming I have a ruby has with one-to-one correspondence, is there some built-in method to reverse associations in a ruby hash? I would prefer doing this without explicitly looping through the keys.
For example, suppose I have:
a = {1 => "Foo", 2 => "Bar"}
a.reverse_association
a # ---> {"Foo" => 1, "Bar" => 2}
Yes, use Hash#invert:
h = {a: 1, b: 2}
h.invert #=> {1 => :a, 2 => :b}

How do I extract a sub-hash from a hash?

I have a hash:
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
What is the best way to extract a sub-hash like this?
h1.extract_subhash(:b, :d, :e, :f) # => {:b => :B, :d => :D}
h1 #=> {:a => :A, :c => :C}
ActiveSupport, at least since 2.3.8, provides four convenient methods: #slice, #except and their destructive counterparts: #slice! and #except!. They were mentioned in other answers, but to sum them in one place:
x = {a: 1, b: 2, c: 3, d: 4}
# => {:a=>1, :b=>2, :c=>3, :d=>4}
x.slice(:a, :b)
# => {:a=>1, :b=>2}
x
# => {:a=>1, :b=>2, :c=>3, :d=>4}
x.except(:a, :b)
# => {:c=>3, :d=>4}
x
# => {:a=>1, :b=>2, :c=>3, :d=>4}
Note the return values of the bang methods. They will not only tailor existing hash but also return removed (not kept) entries. The Hash#except! suits best the example given in the question:
x = {a: 1, b: 2, c: 3, d: 4}
# => {:a=>1, :b=>2, :c=>3, :d=>4}
x.except!(:c, :d)
# => {:a=>1, :b=>2}
x
# => {:a=>1, :b=>2}
ActiveSupport does not require whole Rails, is pretty lightweight. In fact, a lot of non-rails gems depend on it, so most probably you already have it in Gemfile.lock. No need to extend Hash class on your own.
If you specifically want the method to return the extracted elements but h1 to remain the same:
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.select {|key, value| [:b, :d, :e, :f].include?(key) } # => {:b=>:B, :d=>:D}
h1 = Hash[h1.to_a - h2.to_a] # => {:a=>:A, :c=>:C}
And if you want to patch that into the Hash class:
class Hash
def extract_subhash(*extract)
h2 = self.select{|key, value| extract.include?(key) }
self.delete_if {|key, value| extract.include?(key) }
h2
end
end
If you just want to remove the specified elements from the hash, that is much easier using delete_if.
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h1.delete_if {|key, value| [:b, :d, :e, :f].include?(key) } # => {:a=>:A, :c=>:C}
h1 # => {:a=>:A, :c=>:C}
Ruby 2.5 added Hash#slice:
h = { a: 100, b: 200, c: 300 }
h.slice(:a) #=> {:a=>100}
h.slice(:b, :c, :d) #=> {:b=>200, :c=>300}
If you use rails, Hash#slice is the way to go.
{:a => :A, :b => :B, :c => :C, :d => :D}.slice(:a, :c)
# => {:a => :A, :c => :C}
If you don't use rails, Hash#values_at will return the values in the same order as you asked them so you can do this:
def slice(hash, *keys)
Hash[ [keys, hash.values_at(*keys)].transpose]
end
def except(hash, *keys)
desired_keys = hash.keys - keys
Hash[ [desired_keys, hash.values_at(*desired_keys)].transpose]
end
ex:
slice({foo: 'bar', 'bar' => 'foo', 2 => 'two'}, 'bar', 2)
# => {'bar' => 'foo', 2 => 'two'}
except({foo: 'bar', 'bar' => 'foo', 2 => 'two'}, 'bar', 2)
# => {:foo => 'bar'}
Explanation:
Out of {:a => 1, :b => 2, :c => 3} we want {:a => 1, :b => 2}
hash = {:a => 1, :b => 2, :c => 3}
keys = [:a, :b]
values = hash.values_at(*keys) #=> [1, 2]
transposed_matrix =[keys, values].transpose #=> [[:a, 1], [:b, 2]]
Hash[transposed_matrix] #=> {:a => 1, :b => 2}
If you feels like monkey patching is the way to go, following is what you want:
module MyExtension
module Hash
def slice(*keys)
::Hash[[keys, self.values_at(*keys)].transpose]
end
def except(*keys)
desired_keys = self.keys - keys
::Hash[[desired_keys, self.values_at(*desired_keys)].transpose]
end
end
end
Hash.include MyExtension::Hash
You can use slice!(*keys) which is available in the core extensions of ActiveSupport
initial_hash = {:a => 1, :b => 2, :c => 3, :d => 4}
extracted_slice = initial_hash.slice!(:a, :c)
initial_hash would now be
{:b => 2, :d =>4}
extracted_slide would now be
{:a => 1, :c =>3}
You can look at slice.rb in ActiveSupport 3.1.3
module HashExtensions
def subhash(*keys)
keys = keys.select { |k| key?(k) }
Hash[keys.zip(values_at(*keys))]
end
end
Hash.send(:include, HashExtensions)
{:a => :A, :b => :B, :c => :C, :d => :D}.subhash(:a) # => {:a => :A}
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
keys = [:b, :d, :e, :f]
h2 = (h1.keys & keys).each_with_object({}) { |k,h| h.update(k=>h1.delete(k)) }
#=> {:b => :B, :d => :D}
h1
#=> {:a => :A, :c => :C}
if you use rails, it may be convenient to use Hash.except
h = {a:1, b:2}
h1 = h.except(:a) # {b:2}
Both delete_if and keep_if are part of Ruby core. Here you can achieve what you would like to without patching the Hash type.
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.clone
p h1.keep_if { |key| [:b, :d, :e, :f].include?(key) } # => {:b => :B, :d => :D}
p h2.delete_if { |key, value| [:b, :d, :e, :f].include?(key) } #=> {:a => :A, :c => :C}
For futher info, check the links below from the documentation:
delete_if
keep_if
As others have mentioned, Ruby 2.5 added the Hash#slice method.
Rails 5.2.0beta1 also added it's own version of Hash#slice to shim the functionality for users of the framework that are using an earlier version of Ruby.
https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8
If looking to implement your own for whatever reason, it's a nice one liner as well:
def slice(*keys)
keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
end unless method_defined?(:slice)
if you want to extract from data base record also it is better to use slice
hash = { a: 1, b: 2, c: 3, d: 4 }
hash.slice!(:a, :b) # => {:c=>3, :d=>4}
hash # => {:a=>1, :b=>2}
https://api.rubyonrails.org/classes/Hash.html#method-i-slice-21
class Hash
def extract(*keys)
key_index = Hash[keys.map{ |k| [k, true] }] # depends on the size of keys
partition{ |k, v| key_index.has_key?(k) }.map{ |group| Hash[group] }
end
end
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2, h1 = h1.extract(:b, :d, :e, :f)
Here is a quick performance comparison of the suggested methods, #select seems to be the fastest
k = 1_000_000
Benchmark.bmbm do |x|
x.report('select') { k.times { {a: 1, b: 2, c: 3}.select { |k, _v| [:a, :b].include?(k) } } }
x.report('hash transpose') { k.times { Hash[ [[:a, :b], {a: 1, b: 2, c: 3}.fetch_values(:a, :b)].transpose ] } }
x.report('slice') { k.times { {a: 1, b: 2, c: 3}.slice(:a, :b) } }
end
Rehearsal --------------------------------------------------
select 1.640000 0.010000 1.650000 ( 1.651426)
hash transpose 1.720000 0.010000 1.730000 ( 1.729950)
slice 1.740000 0.010000 1.750000 ( 1.748204)
----------------------------------------- total: 5.130000sec
user system total real
select 1.670000 0.010000 1.680000 ( 1.683415)
hash transpose 1.680000 0.010000 1.690000 ( 1.688110)
slice 1.800000 0.010000 1.810000 ( 1.816215)
The refinement will look like this:
module CoreExtensions
module Extractable
refine Hash do
def extract(*keys)
select { |k, _v| keys.include?(k) }
end
end
end
end
And to use it:
using ::CoreExtensions::Extractable
{ a: 1, b: 2, c: 3 }.extract(:a, :b)
This code injects the functionality you're asking for into the Hash class:
class Hash
def extract_subhash! *keys
to_keep = self.keys.to_a - keys
to_delete = Hash[self.select{|k,v| !to_keep.include? k}]
self.delete_if {|k,v| !to_keep.include? k}
to_delete
end
end
and produces the results you provided:
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
p h1.extract_subhash!(:b, :d, :e, :f) # => {b => :B, :d => :D}
p h1 #=> {:a => :A, :c => :C}
Note: this method actually returns the extracted keys/values.
Here's a functional solution that can be useful if you're not running on Ruby 2.5 and in the case that you don't wan't to pollute your Hash class by adding a new method:
slice_hash = -> keys, hash { hash.select { |k, _v| keys.include?(k) } }.curry
Then you can apply it even on nested hashes:
my_hash = [{name: "Joe", age: 34}, {name: "Amy", age: 55}]
my_hash.map(&slice_hash.([:name]))
# => [{:name=>"Joe"}, {:name=>"Amy"}]
Just an addition to slice method, if the subhash keys which you want to separate from original hash is going to be dynamic you can do like,
slice(*dynamic_keys) # dynamic_keys should be an array type
We can do it by looping on keys only we want to extract and just checking the key is exist and then extract it.
class Hash
def extract(*keys)
extracted_hash = {}
keys.each{|key| extracted_hash[key] = self.delete(key) if self.has_key?(key)}
extracted_hash
end
end
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.extract(:b, :d, :e, :f)

How do I recursively define a Hash in Ruby from supplied arguments?

This snippet of code populates an #options hash. values is an Array which contains zero or more heterogeneous items. If you invoke populate with arguments that are Hash entries, it uses the value you specify for each entry to assume a default value.
def populate(*args)
args.each do |a|
values = nil
if (a.kind_of? Hash)
# Converts {:k => "v"} to `a = :k, values = "v"`
a, values = a.to_a.first
end
#options[:"#{a}"] ||= values ||= {}
end
end
What I'd like to do is change populate such that it recursively populates #options. There is a special case: if the values it's about to populate a key with are an Array consisting entirely of (1) Symbols or (2) Hashes whose keys are Symbols (or some combination of the two), then they should be treated as subkeys rather than the values associated with that key, and the same logic used to evaluate the original populate arguments should be recursively re-applied.
That was a little hard to put into words, so I've written some test cases. Here are some test cases and the expected value of #options afterwards:
populate :a
=> #options is {:a => {}}
populate :a => 42
=> #options is {:a => 42}
populate :a, :b, :c
=> #options is {:a => {}, :b => {}, :c => {}}
populate :a, :b => "apples", :c
=> #options is {:a => {}, :b => "apples", :c => {}}
populate :a => :b
=> #options is {:a => :b}
# Because [:b] is an Array consisting entirely of Symbols or
# Hashes whose keys are Symbols, we assume that :b is a subkey
# of #options[:a], rather than the value for #options[:a].
populate :a => [:b]
=> #options is {:a => {:b => {}}}
populate :a => [:b, :c => :d]
=> #options is {:a => {:b => {}, :c => :d}}
populate :a => [:a, :b, :c]
=> #options is {:a => {:a => {}, :b => {}, :c => {}}}
populate :a => [:a, :b, "c"]
=> #options is {:a => [:a, :b, "c"]}
populate :a => [:one], :b => [:two, :three => "four"]
=> #options is {:a => :one, :b => {:two => {}, :three => "four"}}
populate :a => [:one], :b => [:two => {:four => :five}, :three => "four"]
=> #options is {:a => :one,
:b => {
:two => {
:four => :five
}
},
:three => "four"
}
}
It is acceptable if the signature of populate needs to change to accommodate some kind of recursive version. There is no limit to the amount of nesting that could theoretically happen.
Any thoughts on how I might pull this off?
So here's some simple code that works.
def to_value args
ret = {}
# make sure we were given an array
raise unless args.class == Array
args.each do |arg|
case arg
when Symbol
ret[arg] = {}
when Hash
arg.each do |k,v|
# make sure that all the hash keys are symbols
raise unless k.class == Symbol
ret[k] = to_value v
end
else
# make sure all the array elements are symbols or symbol-keyed hashes
raise
end
end
ret
rescue
args
end
def populate *args
#options ||= {}
value = to_value(args)
if value.class == Hash
#options.merge! value
end
end
It does deviate from your test cases:
test case populate :a, :b => "apples", :c is a ruby syntax error. Ruby will assume the final argument to a method is a hash (when not given braces), but not a non-final one, as you assume here. The given code is a syntax error (no matter the definition of populate) since it assumes :c is a hash key, and finds an end of line when it's looking for :c's value. populate :a, {:b => "apples"}, :c works as expected
test case populate :a => [:one], :b => [:two, :three => "four"] returns {:a=>{:one=>{}}, :b=>{:two=>{}, :three=>"four"}}. This is consistent with the test case populate :a => [:b].
Ruby isn't Perl, => works only inside real Hash definition or as final argument in method call. Most things you want will result in a syntax error.
Are you sure that populate limited to cases supported by Ruby syntax is worth it?

Resources