Porting an application to Ruby 1.9
We recently ported our application to Ruby 1.9, here at escolinhas. After seeing the benefits of using Ruby 1.9.1 instead of 1.8.7, we just couldn’t resist (check before, after or just read the entire blog post).
We had to sort a few things out in order to make our application 100% compatible with Ruby 1.9. I’ll cover the issues that most people will probably have to face.
Encoding issues in Rails
Ruby 1.9 has a much more powerful encoding engine. Unfortunately, the developer needs to put some extra effort to be able to cope with this. Check this great blog post for an in depth analysis. I’ll assume that you are using UTF-8 on your project. If you aren’t, you really should.
First of all, here’s some code that you should place inside your config/initializers:
# coding: UTF-8
# TODO: Most of these issues are not present in Rails 3. Remove this when updating.
# Force mysql rows to be UTF-8 (see rails.lighthouseapp.com/projects/8994/tickets/2476)
require 'mysql'
class Mysql::Result
def encode(value, encoding = "utf-8")
String === value ? value.force_encoding(encoding) : value
end
def each_utf8(&block)
each_orig do |row|
yield row.map {|col| encode(col) }
end
end
alias each_orig each
alias each each_utf8
def each_hash_utf8(&block)
each_hash_orig do |row|
row.each {|k, v| row[k] = encode(v) }
yield(row)
end
end
alias each_hash_orig each_hash
alias each_hash each_hash_utf8
end
# fix template rendering
module ActionView
# NOTE: The template that this mixin is being included into is frozen
# so you cannot set or modify any instance variables
module Renderable #:nodoc:
extend ActiveSupport::Memoizable
private
def compile!(render_symbol, local_assigns)
locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join
source = <<-end_src
def #{render_symbol}(local_assigns)
old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
ensure
self.output_buffer = old_output_buffer
end
end_src
# Workaround for erb
source.force_encoding('utf-8') if '1.9'.respond_to?(:force_encoding)
begin
ActionView::Base::CompiledTemplates.module_eval(source, filename, 0)
rescue Errno::ENOENT => e
raise e # Missing template file, re-raise for Base to rescue
rescue Exception => e # errors from template code
if logger = defined?(ActionController) && Base.logger
logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
logger.debug "Function body: #{source}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end
raise ActionView::TemplateError.new(self, {}, e)
end
end
end
end
# the previous fix causes issues in uploaded files encoding, fixed here
module ActionController
class Request
private
# Convert nested Hashs to HashWithIndifferentAccess and replace
# file upload hashs with UploadedFile objects
def normalize_parameters(value)
case value
when Hash
if value.has_key?(:tempfile)
upload = value[:tempfile]
upload.extend(UploadedFile)
upload.original_path = value[:filename]
upload.content_type = value[:type]
upload
else
h = {}
value.each { |k, v| h[k] = normalize_parameters(v) }
h.with_indifferent_access
end
when Array
value.map { |e| normalize_parameters(e) }
else
value.force_encoding(Encoding::UTF_8) if value.respond_to?(:force_encoding)
value
end
end
end
end
So, what are we doing?
- Forcing databse results to be UTF-8 (in this case, mysql);
- Forcing ERB to use UTF-8 as well when compiling;
- The last change introduces a bug when uploading files, so we fix this specific case.
It’s not a good idea to be overriding certain Rails parts. You won’t need to do this when using Rails 2.3.6 (from what I’ve heard) and neither when using Rails 3 but for the present, where most of us use Rails 2.3.5, this is the way to go.
Setting the proper encoding in Ruby source files
Did you notice the following header on the intializer above?
# coding: UTF-8
I hope you did since you’ll be seeing it a lot unless you don’t use non-ascii in your Ruby files at all. If you do, you’ll need to specify its encoding. In many countries (like Portugal, Germany or China), the written language uses them all the time. To sort this out in our entire project without going mad, I created a simple task which handles this for us:
desc "Manage the encoding header of Ruby files"
task :check_encoding_headers => :environment do
files = Array.new
["*.rb", "*.rake"].each do |extension|
files.concat(Dir[ File.join(Dir.getwd.split(/\\/), "**", extension) ])
end
files.each do |file|
content = File.read(file)
next if content[0..16] == "# coding: UTF-8\n\n"
["\n\n", "\n"].each do |file_end|
content = content.gsub(/(# encoding: UTF-8#{file_end})|(# coding: UTF-8#{file_end})|(# -*- coding: UTF-8 -*-#{file_end})/i, "")
end
new_file = File.open(file, "w")
new_file.write("# coding: UTF-8\n\n"+content)
new_file.close
end
end
We run it once in a while, keeping our files clean and making sure that magical header is there. Not to worry if someone adds the header manually - the task is supposed to handle it, if necessary.
Minor issues
A few other minor issues also came up. I’ll just list some of them:
- Object.type is now Object.class.name
- No more normalize for your strings. I ported my permalink_fu fork without it. If you want to know how I did it, take a look at the code.
- You can no longer use “:” on your case statements. Stick with the most common syntax usages.
Final thoughts
As you’ve seen, most of the issues you’ll encounter when porting an application to Ruby 1.9 are encoding-related. We wouldn’t probably have these issues if UTF-8 was the default encoding for Ruby, but for now it’s ASCII-8BIT.
Luckily, the Rails guys are trying to overcome these issues, along with the Ruby team. It’s nice to see a whole community working together.