tags:

views:

196

answers:

2

Basically I have two modules: CoreExtensions::CamelcasedJsonString and …::CamelcasedJsonSymbol. The latter one overrides the Symbol#to_s, so that the method returns a String which is extended with the first module. I don't want every string to be a CamelcasedJsonString. This is the reason why I try to apply the extension instance specific.

My problem is, that Symbol#to_s seems to be overridden again after I included my module (the last spec fails):

require 'rubygems' if RUBY_VERSION < '1.9'
require 'spec'

module CoreExtensions

  module CamelcasedJsonString; end

  module CamelcasedJsonSymbol

    alias to_s_before_core_extension to_s
    def to_s(*args)
      to_s_before_core_extension(*args).extend(CamelcasedJsonString)
    end

  end
  ::Symbol.send :include, CamelcasedJsonSymbol

end

describe Symbol do

  subject { :chunky_bacon }

  it "should be a CamelcasedJsonSymbol" do
    subject.should be_a(CoreExtensions::CamelcasedJsonSymbol)
  end

  it "should respond to #to_s_before_core_extension" do
    subject.should respond_to(:to_s_before_core_extension)
  end

  specify "#to_s should return a CamelcasedJsonString" do
    subject.to_s.should be_a(CoreExtensions::CamelcasedJsonString)
  end

end

However the following example works:

require 'rubygems' if RUBY_VERSION < '1.9'
require 'spec'

module CoreExtensions
  module CamelcasedJsonString; end
end

class Symbol
  alias to_s_before_core_extension to_s
  def to_s(*args)
    to_s_before_core_extension(*args).extend(CoreExtensions::CamelcasedJsonString)
  end
end

describe Symbol do

  subject { :chunky_bacon }

  it "should respond to #to_s_before_core_extension" do
    subject.should respond_to(:to_s_before_core_extension)
  end

  specify "#to_s should return a CamelcasedJsonString" do
    subject.to_s.should be_a(CoreExtensions::CamelcasedJsonString)
  end

end

Update: Jan 24, 2010

The background of my problem is that I try to convert a huge nested hash structure into a JSON string. Each key in this hash is a Ruby Symbol in the typical underscore notation. The JavaScript library which consumes the JSON data expects the keys to be strings in camelcase notation. I thought that overriding the Symbol#to_json method might be the easiest way. But that didn't work out since Hash#to_json calls first #to_s and afterwards #to_json on each key. Therefore I thought it might be a solution to extend all Strings returnd by Symbol#to_s with a module which overrides the #to_json method of this specific string instance to return a string that has a #to_json method which returns itself in camelcase notation.

I'm not sure if there is an easy way to monkey patch Hash#to_json.

If someone wants to take a look into the JSON implementation I'm using, here is the link: http://github.com/flori/json/blob/master/lib/json/pure/generator.rb (lines 239 and following are of interest)

A: 

That looks kind of complicated. I probably don't understand what it is you're trying to achieve, but what about something like this?

#!/usr/bin/ruby1.8

class Symbol

  alias_method :old_to_s, :to_s
  def to_s(*args)
    if args == [:upcase]
      old_to_s.upcase
    else
      old_to_s(*args)
    end
  end

end

puts :foo                   # => foo
puts :foo.to_s(:upcase)     # => FOO

and a partial spec:

describe :Symbol do

  it "should return the symbol as a string when to_s is called" do
    :foo.to_s.should eql 'foo'
  end

  it "should delegate to the original Symbol.to_s method when to_s is called with unknown arguments" do
    # Yeah, wish I knew how to test that
  end

  it "should return the symbol name as uppercase when to_s(:upcase) is called" do
    :foo.to_s(:upcase).should eql "FOO"
  end

end
Wayne Conrad
+2  A: 

Your second monkeypatch works since you are re-opening the Symbol class.

The first one doesn't because all the include does is add the module in the list of included modules. These get called only if the class itself doesn't define a specific method, or if that method calls super. So your code never gets called.

If you want to use a module, you must use the included callback:

  module CamelcasedJsonSymbol
    def self.included(base)
      base.class_eval do
        alias_method_chain :to_s, :camelcase_json
      end
    end

    def to_s_with_camelcase_json(*args)
      to_s_without_camelcase_json(*args).extend(CamelcasedJsonString)
    end
  end

I've used active_record alias_method_chain, which you should always do when monkey patching. It encourages you to use the right names and thus avoid collisions, among other things.

That was the technical answer.

On a more pragmatic approach, you should rethink this. Repeatedly extending strings like this is not nice, will be a huge performance drain on most implementations (it clears the whole method cache on MRI, for instance) and is a big code smell.

I don't know enough about the problem to be sure, or suggest other solutions (maybe a Delegate class could be the right thing to return?) but I have a feeling this is not the right way to arrive to your goals.


Since you want to convert the keys of a hash, you could pass an option to #to_json and monkeypatch that instead of #to_s, like:

{ :chunky_bacon => "good" }.to_json(:camelize => true)

My first idea was to monkeypatch Symbol#to_json but that won't work as you point out because Hash will force the keys to strings before calling to_json, because javascript keys must be strings. So you can monkeypatch Hash instead:

module CamelizeKeys
  def self.included(base)
    base.class_eval do
      alias_method_chain :to_json, :camelize_option
    end
  end

  def to_json_with_camelize_option(*args)
    if args.empty? || !args.first[:camelize]
      to_json_without_camelize_option(*args)
    else
      pairs = map do |key, value|
        "#{key.to_s.camelize.to_json(*args)}: #{value.to_json(*args)}"
      end
      "{" << pairs.join(",\n") << "}"
    end
  end
end
Marc-André Lafortune
I've a huge nested hash structure. Each key is a ruby symbol in underscore notation. I try to convert each symbol into a string in camelcase notation, because the javascript library which consumes the JSON data expects keys in this notation. I thought my approach might be faster than traversing the whole hash.Example: { :chunky_bacon => "some value" }.to_json should be something like '{"chunkyBacon":"some value"}'
t6d
Thanks for the clarification. Don't hesitate to modify your question accordingly. See my updated answer
Marc-André Lafortune
Thanks a lot for your help so far. I updated my question. The problem is that Hash#to_json does not call sth. like Symbol#to_json but Symbol#to_s#to_json instead. I really don't now if there is an easy way to monkey patch Hash#to_json.
t6d
Ah, right. So indeed monkeypatching Hash#to_json is your best bet then. I've updated my answer.
Marc-André Lafortune