Ruby on Rails Json Serialization – To Infinity and beyond!

Wel the title says it all :)

There's a subtile, but very important difference between Ruby on Rails 3 and 4 with json serialization.

Rails 3

(1..10).as_json => [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Rails 4

(1..10).as_json => "1..10"

I had a piece of Ruby code that modeled the business rules of the application like this:

 
{
  brackets: [
    { income: 0..19_645,               perc: 37 },
    { income: 19_646..55_991,          perc: 42 },
    { income: 55_992..Float::INFINITY, perc: 52 }
  ]
}

Try serializing this to the browser with to_json in Rails 3 and prepare for a long wait ;)

The workaround I used to this was creating an initalizer "/config/initializer/range_to_json_monkey_patch.rb"

#
# This initializer requires some explanation
# 
# In the ruby version installed (Rails 3.2.13)  the json encoding method works like this:
#   ActiveSupport::JSON::encode(1..10)  => [1,2,3,4,5,6,7,8,9,10]
#
# Doing this with a large range  is not nice!!
#
# Ruby on rails 4.0.0.1 works like this:
#   ActiveSupport::JSON::encode(1..10)  => "1..10"
#
# This initializer modifies the Range#as_json method so it works like rails 4
# this method first checks if this adjustment is required
#
if (1..4).as_json.kind_of?(Array)

  class Range
    def as_json( options=nil )
      self.to_s
    end
  end

end

Now the json-serialization of Ranges will behave like the one in Rails 4.

A ruby idea

In ruby you can check ranges via de 'range' language construct. For example: (12..23).include?( value).
But how about chaining '<', '<=', '=>' and '>' operators.

For example. currently in Ruby I must write the following

if 10 < x && x < 15
  # code
end

Why isn't it possible to write

if 10 < x < 15
  # code
end

Well in fact it is possible with some hacks :)
The example below is only valid for Fixnum, but it describes the possibilities:

The hack is to just simply return the right hand. In Ruby this shouldn't be a problem, because every object/values is true except false and nil.

class Fixnum
  def <(val)
    super ? val : false
  end
  def <=(val)
    super ? val : false
  end
  def >(val)
    super ? val : false
  end
  def >=(val)
    super ? val : false
  end
end

To make this work the FalseClass should also support these operators, and simply return false to make the complete expression return false if one of them fails:


class FalseClass
  def <(val)
    false
  end
  alias :<= :<
  alias :> :<
  alias :>= :< 
end

So 10 < x just returns 'x' on succes and returns false on error. [code language="ruby"] if 10 < x < 15 # code end [/code] I'm wondering what could/would be the 'problem' by using this construct? Is there a specific reason this has not been implemented this way? BTW: just found a 'nice' implementation for this construct on: http://refactormycode.com/codes/1284-chained-comparisons-in-ruby

[:<, :>, :<=, :>=].each do |operator|
  [Float, Fixnum, Comparable].each do |klass| 
    klass.class_eval {
      alias_method("__#{operator}__", operator)
      define_method(operator) do |operand|
        send("__#{operator}__", operand) and operand
      end 
    }
  end
  FalseClass.send(:define_method, operator) { false }
end

My ruby external-encoding hack

Reading (text)-files from disk is very easy in Ruby.

content = IO.read( "filename.txt" )

When you are trying to do something with this content you can get in trouble.

content.split(",")  # => invalid byte sequence in UTF-8

I've setup my environment very nicely, so Ruby treats external files as UTF-8. The trouble begins when you are trying to handle files that are encoded in the ISO-8859-1 or CP-1252 format and Ruby thinks they are UTF-8.

To accept both UTF-8 and ISO-8858-? formats I've implemented the following hack:

  def convert_to_utf8(content)
    if content.valid_encoding?
      content
    else
      content.force_encoding("ISO-8859-1").encode("UTF-8")
    end    
  end

  # reading the content:
  content = convert_to_utf8 IO.read( "filename.txt" ) 

This hack works for me because the text-files I use are in one of those formats.

has_many tricky replace method

Today I discovered a small bug / feature of rails 3.1.3.

Having the following structure:

class Item < ActiveRecord::Base
  has_many :tags
end

class Tag < ActiveRecord::Base
  # Tag has a boolean flag 'enabled'
  belongs_to :item
end

It's possible to completely replace the has_many collection
with a new collection. Make this collection:

item = Item.find(1)
tags = [ 
  item.tags.create( :enabled => true, :name => "tag1" )
]

All fine for the moment. As expected the enabled flag of the first item is set:

tags[0].name      # => "tag1"
tags[0].enabled?  # => true

Now replacing the existing tags:

item.tags.replace( tags )
item.tags[0].name        # => "tag1"
item.tags[0].enabled?    # => false 

What?? It just forgot the enabled flag!!

The way to replace the items is by assigning:

item.tags = tags
item.tags[0].name        # => "tag1"
item.tags[0].enabled?    # => true

So remember when replacing collections of has_many do not replace them but assign them....

Ruby on Rails / ChiliProject encoding issues

This week I've decided to exchange Redmine for the ChiliProject. The reason for this is the support for Ruby 1.9. My Apache Passenger server runs Ruby 1.9 so for Redmine I needed a seperate webserver.

When I tried to access the "My Account" page I recieved the following error:

ArgumentError (invalid byte sequence in US-ASCII):
  <internal:prelude>:10:in `synchronize'
  passenger (3.0.7) lib/phusion_passenger/rack/request_handler.rb:96:in `process_request'
  passenger (3.0.7) lib/phusion_passenger/abstract_request_handler.rb:513:in `accept_and_process_next_request'
  passenger (3.0.7) lib/phusion_passenger/abstract_request_handler.rb:274:in `main_loop'
  passenger (3.0.7) ...
`handle_spawn_application'
  passenger (3.0.7) lib/phusion_passenger/abstract_server.rb:357:in `server_main_loop'
  passenger (3.0.7) lib/phusion_passenger/abstract_server.rb:206:in `start_synchronously'
  passenger (3.0.7) helper-scripts/passenger-spawn-server:99:in `<main>'

Rendering /data/www/rails/chili/public/500.html (500 Internal Server Error)

Solution

How should I solve this? The chiliproject has an issue related to this: https://www.chiliproject.org/issues/591.

The following Apache configuration fixed the issue: (The sample is on a FreeBSD system)

I added the following code to a file in the /usr/local/apache22/envvars.d/environment.env

export LC_CTYPE="en_US.UTF-8"

Problems I ruled out or fixed

While trying I also made sure the following things were configured:

I made sure the database is UTF-8. I re-created the database
an ran the migrations again.

create database chiliproject character set utf8;

I used the mysql2 connector instead of the mysql connector in database.yml