How can I merge two params together from my permissions hash that share the same "school_id" and "plan_type'. Then delete the permission that was merged from the hash, just leaving one. There can also be more than two that match.
[{"school_id"=>"1",
"plan_type"=>"All",
"view"=>"true",
"create"=>"true",
"approve"=>"true",
"grant"=>"true",
"region_id"=>nil},
{"school_id"=>"1", "plan_type"=>"All", "edit"=>"true", "region_id"=>nil},
{"school_id"=>"2",
"plan_type"=>"All",
"edit"=>"true",
"grant"=>"true",
"region_id"=>nil}]
def create_permissions(user, params)
permissions = params[:permissions].values.map { |perm|
if perm[:plan_type] == "" || perm[:plan_type] == "All Plans"
perm[:plan_type] = "All"
end
#perm_type = get_permission_type(perm)
case
when 'school' then perm.merge(region_id: nil)
when 'region' then perm.merge(school_id: nil)
end
}.tap { |permissions|
new_permissions = []
permissions.each do |perm|
set_permissions = permissions.find {|x| (x != perm && x[:school_id] == perm[:school_id] && x[:plan_type] == perm[:plan_type]) }
end
params[:user][:region_ids] = permissions.map { |perm| perm[:region_id] }.compact
params[:user][:school_ids] = permissions.map { |perm| perm[:school_id] }.compact
}
end
Output:
[{"school_id"=>"1",
"plan_type"=>"All",
"view"=>"true",
"create"=>"true",
"approve"=>"true",
"grant"=>"true",
"region_id"=>nil},
"edit"=>"true"
{"school_id"=>"2",
"plan_type"=>"All",
"edit"=>"true",
"grant"=>"true",
"region_id"=>nil}]
Group by school_id and then reduce by merging hashes:
input.group_by { |e| e['school_id'] }
.values
.map { |v| p v.reduce(&:merge) }
To group by many fields, one might use an array of desired fields, a concatenated string, whatever:
input.group_by { |e| [e['school_id'], e['plan_type']] }
.values
.map { |v| p v.reduce(&:merge) }
or, to keep nifty captions:
input.group_by { |e| "School: #{e['school_id']}, Plan: #{e['plan_type']}" }
.map { |k,v| [k, v.reduce(&:merge)] }
.to_h
#⇒ {
# "School: 1, Plan: All" => {
# "approve" => "true",
# "create" => "true",
# "edit" => "true",
# "grant" => "true",
# "plan_type" => "All",
# "region_id" => nil,
# "school_id" => "1",
# "view" => "true"
# },
# "School: 2, Plan: All" => {
# "edit" => "true",
# "grant" => "true",
# "plan_type" => "All",
# "region_id" => nil,
# "school_id" => "2"
# }
#}
arr1 = arr.group_by { |e| [e["school_id"],e["plan_type"]] }.values
=> {["1", "All"]=>[{"school_id"=>"1", "plan_type"=>"All", "view"=>"true", "create"=>"true", "approve"=>"true", "grant"=>"true", "region_id"=>nil}, {"school_id"=>"1", "plan_type"=>"All", "edit"=>"true", "region_id"=>nil}], ["2", "All"]=>[{"school_id"=>"2", "plan_type"=>"All", "edit"=>"true", "grant"=>"true", "region_id"=>nil}]}
arr1.map{ |i| i.inject({}) { |sum, e| sum.merge e}}
=> [{"school_id"=>"1", "plan_type"=>"All", "view"=>"true", "create"=>"true", "approve"=>"true", "grant"=>"true", "region_id"=>nil, "edit"=>"true"}, {"school_id"=>"2", "plan_type"=>"All", "edit"=>"true", "grant"=>"true", "region_id"=>nil}]
Related
Say I have the below ruby hash nested
hash_or_array = [{
"book1" => "buyer1",
"book2" => {
"book21" => "buyer21", "book22" => ["buyer23", "buyer24", true]
},
"book3" => {
"0" => "buyer31", "1" => "buyer32", "2" => "buyer33",
"3" => [{
"4" => "buyer34",
"5" => [10, 11],
"6" => [{
"7" => "buyer35"
}]
}]
},
"book4" => ["buyer41", "buyer42", "buyer43"],
"book5" => {
"book5,1" => "buyer5"
}
}]
And I want to search for a string that matches buyer35. On match, I want it to return the following result
"book3" => {
"3" => [{
"6" => [{
"7" => "buyer35"
}]
}]
}]
All, other non matching keys,values, arrays should be omitted. I have the following example, but it doesn't quite work
def search(hash)
hash.each_with_object({}) do |(key, value), obj|
if value.is_a?(Hash)
returned_hash = search(value)
obj[key] = returned_hash unless returned_hash.empty?
elsif value.is_a?(Array)
obj[key] = value if value.any? { |v| matches(v) }
elsif matches(key) || matches(value)
obj[key] = value
end
end
end
def matches(str)
match_criteria = /#{Regexp.escape("buyer35")}/i
(str.is_a?(String) || str == true || str == false) && str.to_s.match?(match_criteria)
end
....
=> search(hash_or_array)
Any help is appreciated. I realize, I need to use recursion, but can't quite figure how to build/keep track of the matched node from the parent node.
You can use the following recursive method.
def recurse(obj, target)
case obj
when Array
obj.each do |e|
case e
when Array, Hash
rv = recurse(e, target)
return [rv] unless rv.nil?
when target
return e
end
end
when Hash
obj.each do |k,v|
case v
when Array, Hash
rv = recurse(v, target)
return {k=>rv} unless rv.nil?
when target
return {k=>v}
end
end
end
nil
end
recurse(hash_or_array, "buyer35")
#=> [{"book3"=>{"3"=>[{"6"=>[{"7"=>"buyer35"}]}]}}]
recurse(hash_or_array, "buyer24")
#=>[{"book2"=>{"book22"=>"buyer24"}}]
recurse(hash_or_array, "buyer33")
#=> [{"book3"=>{"2"=>"buyer33"}}]
recurse(hash_or_array, 11)
#=> [{"book3"=>{"3"=>[{"5"=>11}]}}]
recurse(hash_or_array, "buyer5")
#=>[{"book5"=>{"book5,1"=>"buyer5"}}]
If desired, one may write, for example,
recurse(hash_or_array, "buyer35").first
#=> {"book3"=>{"3"=>[{"6"=>[{"7"=>"buyer35"}]}]}}
Say I have the below ruby hash nested
hash_or_array = [{
"book1" => "buyer1",
"book2" => {
"book21" => "buyer21", "book22" => ["buyer23", "buyer24", true]
},
"book3" => {
"0" => "buyer31", "1" => "buyer32", "2" => "buyer33",
"3" => [{
"4" => "buyer34",
"5" => [10, 11],
"6" => [{
"7" => "buyer35"
}]
}]
},
"book4" => ["buyer41", "buyer42", "buyer43", "buyer35"],
"book5" => {
"book5,1" => "buyer5"
}
}]
And I want to look for the string buyer35. Upon match, it should return the following
[
{
"book3" => {
"0" => "buyer31", "1" => "buyer32", "2" => "buyer33",
"3" => [{
"4" => "buyer34",
"5" => [10, 11],
"6" => [{
"7" => "buyer35"
}]
}]
}
},
{
"book4" => ["buyer41", "buyer42", "buyer43", "buyer35"]
}
]
The solution below (from another SO question, link below), returns the first match, but I am trying to figure out how to return multiple matches
def recurse(obj, target)
case obj
when Array
obj.each do |e|
case e
when Array, Hash
rv = recurse(e, target)
return [rv] unless rv.nil?
when target
return e
end
end
when Hash
obj.each do |k,v|
case v
when Array, Hash
rv = recurse(v, target)
return {k=>rv} unless rv.nil?
when target
return {k=>v}
end
end
end
nil
end
This is the original question and answer: How to search nested hash of arrays and arrays of hash and only return matching object from the parent node?
UPDATE: The correct return format should be
[
{
"book3" => {
"3" => [{
"6" => [{
"7" => "buyer35"
}]
}]
}
},
{
"book4" => ["buyer41", "buyer42", "buyer43", "buyer35"]
}
]
Here is a function that recursively searches for the target value in any nested arrays or hashes. The function is then used to select the entries in your top level hash_or_array for entries that contain the target and adds them to an array.
require 'pp'
def find_value(obj, target, found = false)
found = true if obj == target
return found unless obj.is_a?(Array) || obj.is_a?(Hash)
case obj
when Hash
obj.each { |k, v| found = find_value(v, target, found) }
when Array
obj.each { |v| found = find_value(v, target, found) }
end
found
end
found_entries = hash_or_array.inject([]) {|entries, obj| entries << obj.select { |k, v| find_value({ k => v }, "buyer35") }}
pp found_entries
=>
[{"book3"=>
{"0"=>"buyer31",
"1"=>"buyer32",
"2"=>"buyer33",
"3"=>[{"4"=>"buyer34", "5"=>[10, 11], "6"=>[{"7"=>"buyer35"}]}]},
"book4"=>["buyer41", "buyer42", "buyer43", "buyer35"]}]
Here is a recursive solution to your "real" question. Since it mutates the original object, I used a "trick" to make a deep copy first. The keep_entries_with produces an object with the original shape as your input, but since your output shape is different the second step just transforms the filtered result into the shape of your desired output.
deep_copy = Marshal.load(Marshal.dump(hash_or_array))
def keep_entries_with(target, obj)
return unless obj.is_a?(Hash) || obj.is_a?(Array)
case obj
when Hash
keep_entries_with(target, obj.values)
obj.keep_if do |k, v|
v == target ||
v.is_a?(Hash) && v.values.any? { _1 == target || _1.is_a?(Hash) || _1.is_a?(Array) } ||
v.is_a?(Array) && v.any? { _1 == target || _1.is_a?(Hash) || _1.is_a?(Array) }
end
when Array
obj.each do |v|
keep_entries_with(target, v)
end
end
end
filtered = keep_entries_with("buyer35", deep_copy)
final_result = filtered.first.map { |k, v| { k => v } }
pp final_result
which produces:
[{"book3"=>{"3"=>[{"6"=>[{"7"=>"buyer35"}]}]}},
{"book4"=>["buyer41", "buyer42", "buyer43", "buyer35"]}]
The code below appears to generate the desired output for this specific case:
hash_or_array.inject([]) do |result, x|
x.keep_if { |k, v| v.to_s =~ /buyer35/ }
result << x
end
I have a string of field names like this "name,address.postal_code" that acts as a whitelist for a hash that looks like this:
{
name: "Test",
email: "test#test.com",
address: {
postal_code: "12345",
street: "Teststreet"
}
}
So now what I want to to is convert the whitelist string into a format that is accepted by ActionController::Parameters.permit: [:name, address: [:postal_code]]
What would be the best way to do this? I've tried group_by and some other things but it always turned out way more complicated that I think it needs to be.
This uses Array#group_by to parse the nested keys recursively:
module WhitelistParser
# Parses a comma delimed string into an array that can be passed as arguments
# to the rails params whitelist
def self.parse(string)
ary = string.split(',').map do |key|
if key.include?('.')
key.split('.').map(&:intern)
else
key.intern
end
end
make_argument_list(ary)
end
private
def self.make_argument_list(ary)
nested, flat = ary.partition { |a| a.is_a?(Array) }
if flat.any?
flat.tap do |a|
a.push(make_hash(nested)) if nested.any?
end
elsif nested.any?
make_hash(nested)
end
end
def self.make_hash(nested)
nested.group_by(&:first).transform_values do |value|
make_argument_list(value.map { |f, *r| r.length > 1 ? r : r.first })
end
end
end
Usage:
irb(main):004:0> params = ActionController::Parameters.new(foo: { a: 1, b: 2, bar: { baz: 3} })
irb(main):005:0> whitelist = WhitelistParser.parse('a,b,bar.baz')
irb(main):006:0> params.require(:foo).permit(*whitelist)
=> <ActionController::Parameters {"a"=>1, "b"=>2, "bar"=><ActionController::Parameters {"baz"=>3} permitted: true>} permitted: true>
Spec:
require 'spec_helper'
RSpec.describe WhitelistParser do
describe ".parse" do
let(:string) { "name,address.postal_code,foo.bar,foo.bar.baz" }
it "handles flat arguments" do
expect(WhitelistParser.parse(string)).to include :name
end
it "handles hash arguments" do
expect(WhitelistParser.parse(string).last).to include(
{ address: [:postal_code] }
)
end
it "handles nested hash arguments" do
expect(WhitelistParser.parse(string).last[:foo]).to include(
{ bar: [:baz] }
)
end
end
end
It's not ideal, but it should work:
def to_strong_parameters_compatible(fields)
fields # fields="name,address.postal_code,address.street"
.split(',') # ["name", "address.postal_code", "address.street"]
.group_by { |val| val.split('.').first } # { "name" => ["name"], "address" => ["address.postal_code", "address.street"]}
.inject([]) do |params, hash|
k = hash.first
v = hash.last
puts "k: #{k}"
puts "v: #{v}"
# 1st: k="name", v=["name"]
# 2nd: k="address", v=["address.postal_code", "address.street"]
if v.length == 1
params << k
# 1st: params=["name"]
else
params << { k => v.map { |chain| chain.split('.').last } }
# 2nd: params=["name", { "address" => ["postal_code", "street"]}]
end
params
end
end
fields = "name,address.postal_code,address.street"
# How you should use it:
params.permit(*to_strong_parameters_compatible(fields))
Note: it won't work for fields like a.b.c... you would have to make this algo recursive (it should not be hard)
EDIT: and here is the recursive version
def to_strong_parameters_compatible(fields)
fields
.split(',')
.group_by { |val| val.split('.').first }
.inject([]) do |params, hash|
k = hash.first
v = hash.last
if v.length == 1
params << k
else
params << {
k => to_strong_parameters_compatible(
v
.map { |chain| chain.split('.').drop(1).join('.') }
.join(',')
)
}
end
params
end
end
fields = "name,address.postal_code,address.street,address.country.code,address.country.name"
to_strong_parameters_compatible(fields)
# ["name", {"address"=>["postal_code", "street", {"country"=>["code", "name"]}]}]
# How you should use it:
params.permit(*to_strong_parameters_compatible(fields))
I have a nested hash:
given = {
"AA" => {
:GE => nil,
"GE" => "successful",
:GR => nil,
:ZG => nil,
"ZG" => "successful",
},
"BB" => {
:MM => nil,
"MM" => "successful",
:GR => nil,
:ZZ => nil,
"ZZ" => "successful",
}
}
and my goal is to transform it into a new hash without the duplicates eg. :GE/"GE" and :ZG/"ZG" and so on.
goal = {
"AA" => {
:GE => "successful",
:GR => nil,
:ZG => "successful",
},
"BB" => {
:MM => "successful",
:GR => nil,
:ZZ => "successful",
}
}
My attempt is with Rails method index_by
given.map do |key, value|
value.index_by {|r| value[r]}
end
Or with
given.each { |key,value| temp_hash = {} (value.each { |va| va[0].each { |k,v| temp_hash|key| << val }}) given_hash[k.to_sym] = temp_hash if given.has_key?(k.to_sym)}
But I'm kind of stuck. Any help is greatly appreciated!
I'm assuming that in the case of { :GE => nil, "GE" => "successful" }, you want to use the first truthy value ("successful") and make sure the key is a symbol:
result = given.transform_values do |inner_hsh|
inner_hsh.group_by do |k,v|
k.to_sym
end.transform_values do |key_vals|
key_vals.to_h.values.find(&:itself)
end
end
Assuming that #max's assumption is correct, and also that the order of the keys in inner hashes is not important, here is another way to produce the desired return value.
given.each_with_object({}) do |(k,v),h|
h[k] = v.sort_by { |_,w| w.nil? ? 1 : 0 }.uniq { |m,_| m.to_s }.to_h
end
#=> {"AA"=>{"GE"=>"successful", "ZG"=>"successful", :GR=>nil},
# "BB"=>{"MM"=>"successful", "ZZ"=>"successful", :GR=>nil}}
See Array#uniq, particularly the sentence, "self is traversed in order, and the first occurrence is kept.".
For
k = "AA"
v = { :GE=>nil, "GE"=>"successful", :GR=>nil, :ZG=>nil, "ZG"=>"successful" }
we compute
a = v.sort_by { |_,w| w.nil? ? 1 : 0 }
#=> [["GE", "successful"], ["ZG", "successful"],
# [:GE, nil], [:GR, nil], [:ZG, nil]]
b = a.uniq { |m,_| m.to_s }
#=> [["GE", "successful"], ["ZG", "successful"], [:GR, nil]]
because "GE" precedes :GE and "ZG" precedes :ZG. Lastly,
h[k] = b.to_h
#=> {"GE"=>"successful", "ZG"=>"successful", :GR=>nil}
I'm trying to create a Ruby template on the fly with Chef attributes but I can't figure out how to map the attributes to output the way I need.
Example Hash:
a = {
"route" => {
"allocation" => {
"recovery" => {
"speed" => 5,
"timeout" => "30s"
},
"converge" => {
"timeout" => "1m"
}
}
}
}
Would turn into:
route.allocation.recovery.speed: 5
route.allocation.recovery.timeout: 30s
route.allocation.converge.timeout: 1m
Thanks for the help.
You can use recursion if your hash is not large enough to throw stack overflow exception. I don't know what are you trying to achieve, but this is example of how you can do it:
a = {
"route" => {
"allocation" => {
"recovery" => {
"speed" => 5,
"timeout" => "30s"
},
"converge" => {
"timeout" => "1m"
}
}
}
}
def show hash, current_path = ''
hash.each do |k,v|
if v.respond_to?(:each)
current_path += "#{k}."
show v, current_path
else
puts "#{current_path}#{k} : #{v}"
end
end
end
show a
Output:
route.allocation.recovery.speed : 5
route.allocation.recovery.timeout : 30s
route.allocation.recovery.converge.timeout : 1m
I don't know Rails but I'm guessing that the following requires only a small tweak to give the result you want:
#result = []
def arrayify(obj, so_far=[])
if obj.is_a? Hash
obj.each { |k,v| arrayify(v, so_far+[k]) }
else
#result << (so_far+[obj])
end
end
arrayify(a)
#result
#=> [["route", "allocation", "recovery", "speed", 5],
# ["route", "allocation", "recovery", "timeout", "30s"],
# ["route", "allocation", "converge", "timeout", "1m"]]
EDIT: Did I completely misread your question - is the desired output a string? Oh dear.
I think this is a really good use case for OpenStruct:
require 'ostruct'
def build_structs(a)
struct = OpenStruct.new
a.each do |k, v|
if v.is_a? Hash
struct[k] = build_structs(v)
else
return OpenStruct.new(a)
end
end
struct
end
structs = build_structs(a)
output:
[2] pry(main)> structs.route.allocation.recovery.speed
=> 5
For anyone wanting to convert an entire hash with multi levels then here is the code I ended up using:
confHash = {
'elasticsearch' => {
'config' => {
'discovery' => {
'zen' => {
'ping' => {
'multicast' => {
'enabled' => false
},
'unicast' => {
'hosts' => ['127.0.0.1']
}
}
}
}
}
}
}
def generate_config( hash, path = [], config = [] )
hash.each do |k, v|
if v.is_a? Hash
path << k
generate_config( v, path, config )
else
path << k
if v.is_a? String
v = "\"#{v}\""
end
config << "#{path.join('.')}: #{v}"
end
path.pop
end
return config
end
puts generate_config(confHash['elasticsearch']['config'])
# discovery.zen.ping.multicast.enabled: false
# discovery.zen.ping.unicast.hosts: ["127.0.0.1"]