Cjmarkham
Cjmarkham

Reputation: 9700

Process nested hash to convert all values to strings

I have the following code which takes a hash and turns all the values in to strings.

def stringify_values obj
  @values ||= obj.clone

  obj.each do |k, v|
    if v.is_a?(Hash)
      @values[k] = stringify_values(v)
    else
      @values[k] = v.to_s
    end
  end

  return @values
end

So given the following hash:

{
  post: {
    id: 123,
    text: 'foobar',
  }
}

I get following YAML output

--- &1
:post: *1
:id: '123'
:text: 'foobar'

When I want this output

--- 
:post: 
  :id: '123'
  :text: 'foobar'

It looks like the object has been flattened and then been given a reference to itself, which causes Stack level errors in my specs.

How do I get the desired output?

Upvotes: 4

Views: 5098

Answers (4)

Gordon Seidoh Worley
Gordon Seidoh Worley

Reputation: 8088

If you want a simple solution without need of ActiveSupport, you can do this in one line using each_with_object:

obj.each_with_object({}) { |(k,v),m| m[k] = v.to_s }

If you want to modify obj in place pass obj as the argument to each_with_object; the above version returns a new object.

Upvotes: 0

Wand Maker
Wand Maker

Reputation: 18772

A simpler implementation of stringify_values can be - assuming that it is always a Hash. This function makes use of Hash#deep_merge method added by Active Support Core Extensions - we merge the hash with itself, so that in the block we get to inspect each value and call to_s on it.

def stringify_values obj
  obj.deep_merge(obj) {|_,_,v| v.to_s}
end

Complete working sample:

require "yaml"
require "active_support/core_ext/hash"

def stringify_values obj
  obj.deep_merge(obj) {|_,_,v| v.to_s}
end

class Foo
    def to_s
        "I am Foo"
    end
end

h = {
  post: {
    id: 123,
    arr: [1,2,3],
    text: 'foobar',
    obj: { me: Foo.new}
  }
}

puts YAML.dump (stringify_values h)
#=> 
---
:post:
  :id: '123'
  :arr: "[1, 2, 3]"
  :text: foobar
  :obj:
    :me: I am Foo

Not sure what is the expectation when value is an array, as Array#to_s will give you array as a string as well, whether that is desirable or not, you can decide and tweak the solution a bit.

Upvotes: 8

Rustam Gasanov
Rustam Gasanov

Reputation: 15791

There are two issues. First: the @values after the first call would always contain an object which you cloned in the first call, so in the end you will always receive a cloned @values object, no matter what you do with the obj variable(it's because of ||= operator in your call). Second: if you remove it and will do @values = obj.clone - it would still return incorrect result(deepest hash), because you are overriding existing variable call after call.

require 'yaml'

def stringify_values(obj)
  temp = {}
  obj.each do |k, v|
    if v.is_a?(Hash)
      temp[k] = stringify_values(v)
    else
      temp[k] = v.to_s
    end
  end
  temp
end

hash = {
  post: {
    id: 123,
    text: 'foobar',
  }
}

puts stringify_values(hash).to_yaml

#=>
---
:post:
  :id: '123'
  :text: foobar

Upvotes: 4

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121020

If you are as aware of converting values to strings, I would go with monkeypatching Hash class:

class Hash
  def stringify_values
    map { |k, v| [k, Hash === v ? v.stringify_values : v.to_s] }.to_h
  end
end

Now you will be able to:

require 'yaml'
{
  post: {
    id: 123,
    text: 'foobar'
  },
  arr: [1, 2, 3]
}.stringify_values.to_yaml
#⇒ ---
#  :post:
#    :id: '123'
#    :text: foobar
#  :arr: "[1, 2, 3]"

In fact, I wonder whether you really want to scramble Arrays?

Upvotes: -1

Related Questions