8

I'm building a config file for one of our inline apps. Its essentially a json file. I'm having a lot of trouble getting puppet/ruby 1.8 to output the hash/json the same way each time.

I'm currently using

<%= require "json"; JSON.pretty_generate data %>

But while outputting human readable content, it doesn't guarantee the same order each time. Which means that puppet will send out change notifications often for the same data.

I've also tried

<%= require "json"; JSON.pretty_generate Hash[*data.sort.flatten] %>

Which will generate the same data/order each time. The problem comes when data has a nested array.

data => { beanstalkd => [ "server1", ] }

becomes

"beanstalkd": "server1",

instead of

"beanstalkd": ["server1"],

I've been fighting with this for a few days on and off now, so would like some help

Jason
  • 9,408
  • 5
  • 36
  • 36
Gavin Mogan
  • 1,497
  • 1
  • 14
  • 21

2 Answers2

3

Since hashes in Ruby are ordered, and the question is tagged with , here's a method that will sort a hash recursively (without affecting ordering of arrays):

def sort_hash(h)
  {}.tap do |h2|
    h.sort.each do |k,v|
      h2[k] = v.is_a?(Hash) ? sort_hash(v) : v
    end
  end
end

h = {a:9, d:[3,1,2], c:{b:17, a:42}, b:2 }
p sort_hash(h)
#=> {:a=>9, :b=>2, :c=>{:a=>42, :b=>17}, :d=>[3, 1, 2]}

require 'json'
puts sort_hash(h).to_json
#=> {"a":9,"b":2,"c":{"a":42,"b":17},"d":[3,1,2]}

Note that this will fail catastrophically if your hash has keys that cannot be compared. (If your data comes from JSON, this will not be the case, since all keys will be strings.)

Phrogz
  • 296,393
  • 112
  • 651
  • 745
0

Hash is an unordered data structure. In some languages (ruby, for example) there's an ordered version of hash, but in most cases in most languages you shouldn't rely on any specific order in a hash.

If order is important to you, you should use an array. So, your hash

{a: 1, b: 2}

becomes this

[{a: 1}, {b: 2}]

I think, it doesn't force too many changes in your code.

Workaround to your situation

Try this:

data = {beanstalkId: ['server1'], ccc: 2, aaa: 3}

data2 = data.keys.sort.map {|k| [k, data[k]]}

puts Hash[data2]
#=> {:aaa=>3, :beanstalkId=>["server1"], :ccc=>2}
Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
  • Order doesn't matter as long as its the same each time, so puppet doesn't think there is a config change every hour. Switching to an array isn't really something I can do without a lot of ugly hacking. – Gavin Mogan Mar 16 '12 at 08:14
  • It works for now, but nested hashes still have the same problem. Because time it takes to output isn't really an issue, I may just write a custom ruby function that outputs it by hand sorted. Thanks though. Still the closest thing to being right. – Gavin Mogan Mar 19 '12 at 20:50
  • It does work for my specific example, but not really what I was talking about. But it is right and so far the only answer, so I will, thanks again – Gavin Mogan Mar 19 '12 at 22:49
  • Under Ruby 1.9 you can just do `data2 = Hash[ data.sort ]` if you only need shallow sorting. – Phrogz Apr 13 '15 at 18:13