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"]
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))
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}]
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'm building an app in Rails 4 using the Magento SOAP API v1 and Savon gem. Right now I am trying to get all orders with a status of pending. To hook into the API I am using this code:
class MagentoAPI
def self.call method, options={}
response = ##soap_client.request :call do
if options.empty?
soap.body = { :session => ##soap_session, :method => method }
elsif options[:string]
soap.body = { :session => ##soap_session, :method => method, :arguments => [options[:string]] }
else
puts options
soap.body = { :session => ##soap_session, :method => method, :arguments => options }
end
end
if response.success?
# listing found products
final = []
call_return = response[:call_response][:call_return]
return [] if call_return[:item].nil?
raw = call_return[:item]
if raw.is_a? Hash # this is a list of one item
final << raw[:item].inject({}){|x,y| x.merge(y[:key]=>y[:value])}
else
if raw[0][:item].nil? # this is a product info
return raw.inject({}){|x,y| x.merge(y[:key]=>y[:value])}
else # this is a list of many items
raw.each{|result| final << result[:item].inject({}){|x,y| x.merge(y[:key]=>y[:value])}}
end
end
final
end
end
end
And then this:
class Order
def self.get_all_active
activeOrders = MagentoAPI.call 'order.list', :filter => {:status => 'pending'}
end
end
This just returns Savon::HTTP::Error so I'm thinking I'm not formatting the request properly. Does anybody have any experience or insight on this?
Hope this isn't too late (assume it might be), but I created a gem for this with some rudimentary documentation. I'm hoping to finish it up this weekend or next week, but you can take a look at the code and see how I'm creating the filters for Magento. To install, just run:
gem install magento_api_wrapper
To summarize, if you want to use one of the Magento SOAP API simple filters, you can pass a hash with a key and value:
api = MagentoApiWrapper::Sales.new(magento_url: "yourmagentostore.com/index.php", magento_username: "soap_api_username", magento_api_key: "userkey123")
api.order_list(simple_filters: [{key: "status", value: "processing"}, {key: created_at, value: "12/10/2013 12:00" }])
And to use a complex filter, pass a hash with key, operator, and value:
api.order_list(complex_filters: [{key: "status", operator: "eq", value: ["processing", "completed"]}, {key: created_at, operator: "from", value: "12/10/2013" }])
This returns an array of hashes with all your Magento orders.
Specifically, check out the request code: https://github.com/harrisjb/magento_api_wrapper/blob/master/lib/magento_api_wrapper/requests/sales_order_list.rb
While it will be easier to just use the gem, here's how I'm formatting the request prior to passing it to the SavonClient, which finishes the formatting for Magento's SOAP API:
def body
merge_filters!(sales_order_list_hash)
end
def attributes
{ session_id: { "xsi:type" => "xsd:string" },
filters: { "xsi:type" => "ns1:filters" },
}
end
def sales_order_list_hash
{
session_id: self.session_id
}
end
def merge_filters!(sales_order_list_hash)
if !filters_array.empty?
sales_order_list_filters = {
filters: filters_array,
}
sales_order_list_hash.merge!(sales_order_list_filters)
else
sales_order_list_hash
end
end
def filters_array
custom_filters = {}
custom_filters.compare_by_identity
if !simple_filters.nil?
add_simple_filters(custom_filters)
end
if !complex_filters.nil?
add_complex_filters(custom_filters)
end
custom_filters
end
def add_simple_filters(custom_filters)
simple_filters.each do |sfilter|
custom_filters[:attributes!] = {
"filter" => {
"SOAP-ENC:arrayType" => "ns1:associativeEntity[2]",
"xsi:type" => "ns1:associativeArray"
}
}
custom_filters["filter"] = {
item: {
key: sfilter[:key],
value: sfilter[:value], #formatted_timestamp(created_at)
:attributes! => {
key: { "xsi:type" => "xsd:string" },
value: { "xsi:type" => "xsd:string" }
},
},
:attributes! => {
item: { "xsi:type" => "ns1:associativeEntity" },
},
}
end
custom_filters
end
def add_complex_filters(custom_filters)
complex_filters.each do |cfilter|
custom_filters[:attributes!] = {
"complex_filter" => {
"SOAP-ENC:arrayType" => "ns1:complexFilter[2]",
"xsi:type" => "ns1:complexFilterArray"
}
}
custom_filters["complex_filter"] = {
item: {
key: cfilter[:key],
value: {
key: cfilter[:operator],
value: cfilter[:value]
},
:attributes! => {
key: { "xsi:type" => "xsd:string" },
value: { "xsi:type" => "xsd:associativeEntity" }
},
},
:attributes! => {
item: { "xsi:type" => "ns1:complexFilter" },
},
}
end
custom_filters
end
def formatted_timestamp(timestamp)
begin
Time.parse(timestamp).strftime("%Y-%m-%d %H:%M:%S")
rescue MagentoApiWrapper::BadRequest => e
raise "Did you pass date in format YYYY-MM-DD? Error: #{e}"
end
end
def status_array
data[:status_array]
end
def created_at_from
data[:created_at_from]
end
def created_at_to
data[:created_at_to]
end
def last_modified
data[:last_modified]
end
def session_id
data[:session_id]
end
def simple_filters
data[:simple_filters]
end
def complex_filters
data[:complex_filters]
end
I've also got a SavonClient that does some of the configuration for the specific API, here's most of that:
def call
client.call(#request.call_name, message: message_with_attributes, response_parser: :nokogiri)
end
#message_with_attributes are required for some specific formatting when updating Magento via the SOAP API
def message_with_attributes
#request.body.merge!(:attributes! => #request.attributes) unless #request.attributes.empty?
puts "REQUEST: #{#request.inspect}"
return #request.body
end
#configuration of the client is mostly mandatory, however some of these options (like timeout) will be made configurable in the future
#TODO: make timeout configurable
def client
Savon::Client.new do |savon|
savon.ssl_verify_mode :none
savon.wsdl base_url
savon.namespaces namespaces
savon.env_namespace 'SOAP-ENV'
savon.raise_errors false
#savon.namespace_identifier #none
savon.convert_request_keys_to :lower_camelcase
savon.strip_namespaces true
savon.pretty_print_xml true
savon.log log_env
savon.open_timeout 10 #seconds
savon.read_timeout 45 #seconds
end
end
#TODO: make configurable
def log_env
true
end
#correctly format MagentoApiWrapper::Request call_names for SOAP v2
def response_tag_format_lambda
lambda { |key| key.snakecase.downcase }
end
def namespaces
{
'xmlns:SOAP-ENV' => 'http://schemas.xmlsoap.org/soap/envelope/',
'xmlns:ns1' => 'urn:Magento',
'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
'xmlns:SOAP-ENC' => 'http://schemas.xmlsoap.org/soap/encoding/',
'SOAP-ENV:encodingStyle' => 'http://schemas.xmlsoap.org/soap/encoding/'
}
end
#Use MagentoApiWrapper::Api magento_url as endpoint
def base_url
"#{#magento_url}/api/v2_soap?wsdl=1"
end
end
Like I said, it's a work in progress, but I should have pretty good coverage of the Magento API complete in the next couple of weeks. Hope this helps you out! Good luck!