HashWithIndifferentAccess doesn't work with % format operator in Rails 5

Viewed 403

Ruby string's method % allows to substitute name placeholders with the values of the given hash: "Hello %{name}" % {name: "World"} will result with "Hello World".

The problem is that it doesn't work with HashWithIndifferentAccess arguments. In Rails 5 this code would raise an exception:

2.3.8 :001 > "%{a}" % {"a" => 1}.with_indifferent_access
KeyError: key{a} not found
    from (irb):1:in `%'
    from (irb):1

It's also interesting that the same code works fine in Rails 4 but not in Rails 5

Is there a way to fix it (maybe some monkey-patching of ActiveSupport::HashWithIndifferentAccess) and understand why the behavior of % (and sprintf too) has changed?

1 Answers

This is mostly because of the changes introduced to ActiveSupport::HashWithIndifferentAccess through the different releases as long as Rails has been evolving and to the default method (don't confuse with Hash#default).

If you see in the Rails 4 version of ActiveSupport::HashWithIndifferentAccess the default method looks like:

def default(key = nil)
  if key.is_a?(Symbol) && include?(key = key.to_s)
    self[key]
  else
    super
  end
end

Which is clearly receiving a key when is invoked, checking its type and if is included within the hash (self) as one of its keys, returns its value, otherwise invokes super.

And in newer versions (newer because your example doesn't work in Rails 4+ versions) it looks like:

def default(*args)
  super(*args.map { |arg| convert_key(arg) })
end

def convert_key(key) # :doc:
  key.kind_of?(Symbol) ? key.to_s : key
end

Which attempts to return every element from the arguments passed to default when being invoked to String if it's a symbol or return the same element.

This in your example would return nil for default, which explains the KeyError error you're getting.

The error or difference in the behavior isn't % but in the changes introduced.

Related