I use Rails 4.2.0 with ActiveResource to implement a webclient for server API.
The problem I've faced is that server resources contains '#' and '##' in attribute names. So these attributes can't be processed correctly by ActiveResource while creating ARes object.
Example: server returns JSON data:
{"##artist"=>
{"#id"=>"some_text_id",
"#name"=>"Carman",
"#publisher"=>"Carmen radio",
"#category"=>"Music",
"#automaticallyGenerated"=>true,
"##options"=>{"#public"=>true, "#enabled"=>false},
"_extended"=>{"#container"=>"Music box", "region"=>"Europe"}}}
The variant to rename attributes before ARes object is created: (remove '#' from name or replace it for another symbol). In that case I need to do back rename attributes before send POST, PUT requests (call resource.save for example) E.g. add '#' or '##' in the beginning of the names.
Could you suggest more flexible and pretty variant?
I rly dont know what ist your output, but you can use something like this.
This is just example.
input = {
"##artist"=>
{"#id"=>"some_text_id",
"#name"=>"Carman",
"#publisher"=>"Carmen radio",
"#category"=>"Music",
"#automaticallyGenerated"=>true,
"##options"=>{"#public"=>true, "#enabled"=>false},
"_extended"=>{"#container"=>"Music box", "region"=>"Europe"}
}
}
def transcode(hash)
result = {}
hash.each do |k, v|
result[k.gsub(/#*/, "")] = ( Hash === v ? transcode(v) : v )
end
result
end
output = transcode(input)
#{
# "artist" => {
# "id" => "some_text_id",
# "name" => "Carman",
# "publisher" => "Carmen radio",
# "category" => "Music",
# "automaticallyGenerated" => true,
# "options" => {
# "public" => true,
# "enabled" => false
# },
# "_extended" => {
# "container" => "Music box",
# "region" => "Europe"
# }
# }
#}
BACK
input = {
"##artist"=>
{"#id"=>"some_text_id",
"#name"=>"Carman",
"#publisher"=>"Carmen radio",
"#category"=>"Music",
"#automaticallyGenerated"=>true,
"##options"=>{"#public"=>true, "#enabled"=>false},
"_extended"=>{"#container"=>"Music box", "region"=>"Europe"}
}
}
def transcode(hash)
result = {}
hash.each do |k, v|
result[k.gsub(/#*/, "")] = ( Hash === v ? transcode(v) : v )
end
result
end
def transcode_back(hash, output)
result = {}
hash.each do |k, v|
value = output[k.gsub(/#*/, "")]
result[k] = ( Hash === value ? transcode_back(v, value) : value )
end
result
end
output = transcode(input)
# you can modify values
#output["artist"]["name"] = "CarmanNew"
result = transcode_back(input, output)
output == result # is same ?
# true
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"}]}]}}
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'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"]
I want to "flatten" (not in the classical sense of .flatten) down a hash with varying levels of depth, like this:
{
:foo => "bar",
:hello => {
:world => "Hello World",
:bro => "What's up dude?",
},
:a => {
:b => {
:c => "d"
}
}
}
down into a hash with one single level, and all the nested keys merged into one string, so it would become this:
{
:foo => "bar",
:"hello.world" => "Hello World",
:"hello.bro" => "What's up dude?",
:"a.b.c" => "d"
}
but I can't think of a good way to do it. It's a bit like the deep_ helper functions that Rails adds to Hashes, but not quite the same. I know recursion would be the way to go here, but I've never written a recursive function in Ruby.
You could do this:
def flatten_hash(hash)
hash.each_with_object({}) do |(k, v), h|
if v.is_a? Hash
flatten_hash(v).map do |h_k, h_v|
h["#{k}.#{h_k}".to_sym] = h_v
end
else
h[k] = v
end
end
end
flatten_hash(:foo => "bar",
:hello => {
:world => "Hello World",
:bro => "What's up dude?",
},
:a => {
:b => {
:c => "d"
}
})
# => {:foo=>"bar",
# => :"hello.world"=>"Hello World",
# => :"hello.bro"=>"What's up dude?",
# => :"a.b.c"=>"d"}
Because I love Enumerable#reduce and hate lines apparently:
def flatten_hash(param, prefix=nil)
param.each_pair.reduce({}) do |a, (k, v)|
v.is_a?(Hash) ? a.merge(flatten_hash(v, "#{prefix}#{k}.")) : a.merge("#{prefix}#{k}".to_sym => v)
end
end
irb(main):118:0> flatten_hash(hash)
=> {:foo=>"bar", :"hello.world"=>"Hello World", :"hello.bro"=>"What's up dude?", :"a.b.c"=>"d"}
The top voted answer here will not flatten the object all the way, it does not flatten arrays. I've corrected this below and have offered a comparison:
x = { x: 0, y: { x: 1 }, z: [ { y: 0, x: 2 }, 4 ] }
def top_voter_function ( hash )
hash.each_with_object( {} ) do |( k, v ), h|
if v.is_a? Hash
top_voter_function( v ).map do |h_k, h_v|
h[ "#{k}.#{h_k}".to_sym ] = h_v
end
else
h[k] = v
end
end
end
def better_function ( a_el, a_k = nil )
result = {}
a_el = a_el.as_json
a_el.map do |k, v|
k = "#{a_k}.#{k}" if a_k.present?
result.merge!( [Hash, Array].include?( v.class ) ? better_function( v, k ) : ( { k => v } ) )
end if a_el.is_a?( Hash )
a_el.uniq.each_with_index do |o, i|
i = "#{a_k}.#{i}" if a_k.present?
result.merge!( [Hash, Array].include?( o.class ) ? better_function( o, i ) : ( { i => o } ) )
end if a_el.is_a?( Array )
result
end
top_voter_function( x ) #=> {:x=>0, :"y.x"=>1, :z=>[{:y=>0, :x=>2}, 4]}
better_function( x ) #=> {"x"=>0, "y.x"=>1, "z.0.y"=>0, "z.0.x"=>2, "z.1"=>4}
I appreciate that this question is a little old, I went looking online for a comparison of my code above and this is what I found. It works really well when used with events for an analytics service like Mixpanel.
Or if you want a monkey-patched version or Uri's answer to go your_hash.flatten_to_root:
class Hash
def flatten_to_root
self.each_with_object({}) do |(k, v), h|
if v.is_a? Hash
v.flatten_to_root.map do |h_k, h_v|
h["#{k}.#{h_k}".to_sym] = h_v
end
else
h[k] = v
end
end
end
end
In my case I was working with the Parameters class so none of the above solutions worked for me. What I did to resolve the problem was to create the following function:
def flatten_params(param, extracted = {})
param.each do |key, value|
if value.is_a? ActionController::Parameters
flatten_params(value, extracted)
else
extracted.merge!("#{key}": value)
end
end
extracted
end
Then you can use it like flatten_parameters = flatten_params(params). Hope this helps.
Just in case, that you want to keep their parent
def flatten_hash(param)
param.each_pair.reduce({}) do |a, (k, v)|
v.is_a?(Hash) ? a.merge({ k.to_sym => '' }, flatten_hash(v)) : a.merge(k.to_sym => v)
end
end
hash = {:foo=>"bar", :hello=>{:world=>"Hello World", :bro=>"What's up dude?"}, :a=>{:b=>{:c=>"d"}}}
flatten_hash(hash)
# {:foo=>"bar", :hello=>"", :world=>"Hello World", :bro=>"What's up dude?", :a=>"", :b=>"", :c=>"d"}
I have a hash that I'm getting from using JSON.parse. I am inserting into db at that point. The data structure uses acts as tree so I can have a hash like this:
category_attributes
name: "Gardening"
items_attributes:
item 1: "backhoe"
item 2: "whellbarrel"
children_attributes
name: "seeds"
items_attributes
item 3: "various flower seeds"
item 4: "various tree seeds"
children_attributes
name: "agricultural seeds"
items_attributes
item 5: "corn"
item 6: "wheat"
For this hash, I'd like to return an array of all the items_attributes. I have seen this question Traversing a Hash Recursively in Ruby but it seems different. Is there a way to recursively search a hash and return all those elements? Ideally, empty items_attributes should come back with nothing rather than nil.
thx
Try this:
def extract_list(hash, collect = false)
hash.map do |k, v|
v.is_a?(Hash) ? extract_list(v, (k == "items_attributes")) :
(collect ? v : nil)
end.compact.flatten
end
Now let us test the function:
>> input = {
'category_attributes' => {
'name' => "Gardening",
'items_attributes' => {
'item 1' => "backhoe",
'item 2' => "whellbarrel",
'children_attributes' => {
'name' => "seeds",
'items_attributes' => {
'item 3' => "various flower seeds",
'item 4' => "various tree seeds"
},
'children_attributes' => {
'name' => "agricultural seeds",
'items_attributes' => {
'item 5' => "corn",
'item 6' => "wheat"
}
}
}
}
}
}
>> extract_list(input)
=> ["various flower seeds", "various tree seeds", "wheat", "corn",
"backhoe", "whellbarrel"]
You can do something like this:
def collect_item_attributes h
result = {}
h.each do |k, v|
if k == 'items_attributes'
h[k].each {|k, v| result[k] = v } # <= tweak here
elsif v.is_a? Hash
collect_item_attributes(h[k]).each do |k, v|
result[k] = v
end
end
end
result
end
puts collect_item_attributes(h)
# => {"item 1"=>"backhoe",
# "item 2"=>"whellbarrel",
# "item 3"=>"various flower seeds",
# "item 4"=>"various tree seeds",
# "item 5"=>"corn",
# "item 6"=>"wheat"}