Hash Defaults and Nested OrderedHash in Rails
Thanks to ruby_switcher.sh I now have Ruby 1.9 running on my machine. Subsequently, I’ve just run into my first Ruby 1.9 induced bug: I relied upon 1.9’s Hash ordering. Fortunately, Rails’ ActiveSupport has it’s own undocumented OrderedHash implementation for Ruby 1.8. Unfortunately you can’t create an OrderdHash using the hash literal notation (i.e { :a => 1, :b => 2 }) so making a nested OrderedHash could be painful.
Consider the following code:
hash = ActiveSupport::OrderedHash.new
hash["2"] = ActiveSupport::OrderedHash.new
hash["2"]["b"] = "2.b"
hash["2"]["a"] = "2.a"
hash["1"] = ActiveSupport::OrderedHash.new
hash["1"]["b"] = "1.b"
hash["1"]["a"] = "1.a"This however seems unnecessarily verbose. Thankfully Ruby’s Hash defaults come to the rescue.
hash = ActiveSupport::OrderedHash.new{|h,k| h[k] = ActiveSupport::OrderedHash.new(&h.default_proc) }
hash["2"]["b"] = "2.b"
hash["2"]["a"] = "2.a"
hash["1"]["b"] = "1.b"
hash["1"]["a"] = "1.a"All the magic here happens in the somewhat arcane first line. Hash.new (and OrderedHash which is one of its subclasses) allows us to pass a block that defines the default behavior for undefined keys. The second bit of magic is the “h.default_proc” bit. This means that any automatically created OrderedHashes will have the same default behavior as their parent. As a result, we now have an OrderedHash that automatically creates OrderedHashes which in turn create OrderedHashes and so on.
The big benefit of this is that we don’t have to explicitly implement any of the child hashes. There is one gotcha however. You have to be careful with how you check for the existence of a key. While normally you could use “if hash[:key]” this will initialize an OrderedHash and will evaluate to true not to false. Instead, you should use “hash.has_key?(:key)”.
This is not a solution for all cases, but in some instances it can save you a few lines of clutter.
ActiveRecord #find_by_id with Conditions and NotFound Exception
Every so often I’ve seen code (maybe even written by yours truly) that looked something like this:
post = Post.first(:conditions => { :id => 1, :published => true })However, this won’t raise an error like a #find(id) would. So then the following line is added:
post = Post.first(:conditions => { :id => 1, :published => true })
raise ActiveRecord::RecordNotFound unless postBut this is getting a bit messy and seems overly complicated. Fortunately, we have a better solution.
Ironically, this solution is actually well documented in the Rails docs. Any call to ActiveRecord::Base#find accepts an options hash. This means we can do things like:
post = Post.first(1, :conditions => { :published => true })Now that looks much better.
Coloration in Autotest with TestUnit 2.0.3 1
I recently discovered that even though TestUnit 2.0.3 has support for for coloration, coloration does not show in Autotest. Previously, the redgreen gem had taken care of this, but now that TestUnit itself supports coloration the redgreen gem is no longer necessary (not to mention the fact that it’s broken with Ruby 1.9 and TestUnit 2.0.3). TestUnit supports a –use-color flag, but because of the unique way in which Autotest loads the tests I was unable to figure out how I could pass this flag. Furthermore, –use-color defaults to false when the command is passed to a pipe, which Autotest also does.
The easy solution I figured was to then override the method that sets the –use-color defaults. The best place I found to do this is in the Rails test_helper.rb file itself. I know this isn’t the most flexible location, but I couldn’t find anywhere else to inject it, so for now it will have to do. All you need to do is add the following code:
require 'test/unit/ui/console/testrunner'
module Test
module Unit
module UI
module Console
class TestRunner
def guess_color_availability; true; end
end
end
end
end
endIf anyone comes up with a more seamless way to do this, please let me know.
ActsAsNestedController with infinitely more documentation!
A while back I wrote a plugin to help with managing the logic of controllers with nested routes. Unfortunately, I was lazy and never documented it. I finally got around to refactoring it and adding proper documentation. You can find it at: http://github.com/rxcfc/acts_as_nested_controller
:use_route param for Rails URL helper
Imagine the following case. You have two landing pages, one generic one, and an account specific one. The urls are as follows:
map.landing 'landing', :controller => 'landing', :action => 'index'
map.account_landing 'accounts/:account_id/landing', :controller => 'landing', :action => 'index'Now imagine you want a path to the landing page, using the most specific route possible. If you have an account_id, use it, if not, skip it.
You could do:
url_for(:controller => 'landing', :action => 'index', :account_id => current_account)If current_account is set you’ll get “/accounts/:account_id/landing” if not, you’ll get “/landing”. However, that just looks ugly.
Enter :use_route => nil.
landing_path(:account_id => nil) # => '/landing'
landing_path(:account_id => 1) # => '/landing?account_id=1'
landing_path(:account_id => nil, :use_route => nil) # => '/landing'
landing_path(:account_id => 1, :use_route => nil) # => '/accounts/1/landing'Setting :use_route to nil, is equivalent to the earlier #url_for example.
Another use for :use_route is to force a route helper when using :url_for.
For instance, you can do:
url_for(:controller => 'landing', :action => 'index', :account_id => 1, :use_route => :landing)
# => '/landing?account_id=1'Though it’s not likely you would want that specific outcome, if you need to force #url_for to use a specific route form, it can be done.
This is likely to be most of use when working with a plugin, such as will_paginate, that calls #url_for internally.
Thanks to matbrown for the second tip.
Path Expander
UPDATE: This is not necessary. See this post for details.
From time to time, I’ve been working on a Rails app with nested routes and ran into the situation where I want to nest the url only if I have the necessary information. Consider the following example to see what I mean:
user_projects_path(current_user) #=> /users/:id/projectsNow this will work perfectly if current_user is defined, but what if current_user is nil? Unfortunately, I’ll end up with the less than desirable “/users//projects”. To avoid this I could use an if statement along the lines of:
current_user ? user_projects_path(current_user) : projects_path
#=> /users/:id/projects or /projectsThis will work of course, but it isn’t all that elegant. Another option would be something along the lines of:
projects_path(:user_id => current_user.id)This solution will also work and has the benefit of being concise. Unfortunately we lose the nice route formatting. Instead of “/users/:id/projects”, we get “/projects?user_id=:id”. Not the prettiest result.
So I started thinking, wouldn’t it be great if Rails would automatically expand that path into “/users/:id/projects” if possible and if not just fall back to “/projects”. I started hacking around a bit and this is what I came up with:
The biggest downside to this is the call to #recognize_path. From my understanding, this can be a bit computationally intensive, so if you’re worried about top notch performance, this solution may be a bit risky.
So have you dealt with this before? If so, have you been able to come up with a better solution than this?
Don't Just Ignore schema.rb!
It’s a long standing practice among many Rails developers to have their version control ignore schema.rb. However this practice is contrary to that recommended by the Rails source itself! As you can see from this commit, DHH himself highly encourages that you check in into version control. This alone should be good reason to pause and consider if we should keep ignoring schema.rb.
Now, I know that some developers prefer to use migrations as their main reference. Though migrations can be unstable, they argue that properly maintained migrations won’t have issues. And that’s fine. If you have a good reason to go against Rails conventions then I won’t stop you, every developer should know what works best for their own projects and development style. However, I’ve heard just as many developers argue that using schema.rb causes problems with their version control or breaks things in some other way. If this is the case then the answer is not to just ignore it. If schema.rb doesn’t work the way it should, then it’s a bug and should be reported as such! Ignoring it is just a stopgap measure at best and should not be considered a long term solution when things are actually broken.
Stop Hash.from_xml from Killing XML Attributes 1
UPDATE: I’ve submitted a patch for this to core. Check it out here.
Before I get into the solution, I should probably begin by explaining the problem. Consider the following line of code:
Hash.from_xml('<variable type="integer">5</variable>')
#=> { "variable" => 5 }There are two important things to notice here. First, the 5 is parsed as an integer, second, we lose the “type” attribute. What’s going on behind the scenes is that Rails is looking for a "type" attribute to determine how to typecast the element contents. Now under most circumstances, this is all well and good, but what if you run into a situation where the “type” attribute doesn’t match up with ones known to Rails?
Hash.from_xml('<variable type="product_code">5</variable>')
#=> { "variable" => 5 }Now we not only lose the benefit of typecasting since Rails doesn’t know what to do with type “product_code”, we also lose any information telling us that the type was “product_code”. Unfortunately it gets worse:
Hash.from_xml('<variable type="product_code" subtype="upc">5</variable>')
#=> { "variable" => 5 }We also lose any other attributes on this element! This is true of all elements that are childless but not empty.
Since a project I’m working on requires me to be able to get the attributes in these cases, I had to do a little hacking in the Hash class. My results are as follows:
class Hash
def self.from_xml(xml, preserve_attributes = false)
# TODO: Refactor this into something much cleaner that doesn't rely on XmlSimple
typecast_xml_value(undasherize_keys(XmlSimple.xml_in_string(xml,
'forcearray' => false,
'forcecontent' => true,
'keeproot' => true,
'contentkey' => '__content__')
), preserve_attributes)
end
private
def self.typecast_xml_value(value, preserve_attributes = false)
case value.class.to_s
when 'Hash'
if value['type'] == 'array'
child_key, entries = value.detect { |k,v| k != 'type' } # child_key is throwaway
if entries.nil? || (c = value['__content__'] && c.blank?)
[]
else
case entries.class.to_s # something weird with classes not matching here. maybe singleton methods breaking is_a?
when "Array"
entries.collect { |v| typecast_xml_value(v, preserve_attributes) }
when "Hash"
[typecast_xml_value(entries, preserve_attributes)]
else
raise "can't typecast #{entries.inspect}"
end
end
elsif value.has_key?("__content__")
content = value["__content__"]
if parser = XML_PARSING[value["type"]]
if parser.arity == 2
XML_PARSING[value["type"]].call(content, value)
else
XML_PARSING[value["type"]].call(content)
end
elsif preserve_attributes && value.keys.size > 1
value["content"] = value.delete("__content__")
value
else
content
end
elsif value['type'] == 'string' && value['nil'] != 'true'
""
# blank or nil parsed values are represented by nil
elsif value.blank? || value['nil'] == 'true'
nil
# If the type is the only element which makes it then
# this still makes the value nil, except if type is
# a XML node(where type['value'] is a Hash)
elsif value['type'] && value.size == 1 && !value['type'].is_a?(::Hash)
nil
else
xml_value = value.inject({}) do |h,(k,v)|
h[k] = typecast_xml_value(v, preserve_attributes)
h
end
# Turn { :files => { :file => #<StringIO> } into { :files => #<StringIO> } so it is compatible with
# how multipart uploaded files from HTML appear
xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value
end
when 'Array'
value.map! { |i| typecast_xml_value(i, preserve_attributes) }
case value.length
when 0 then nil
when 1 then value.first
else value
end
when 'String'
value
else
raise "can't typecast #{value.class.name} - #{value.inspect}"
end
end
endNow that’s a bit lengthy, but all you have to worry about is the new parameter for #from_xml called “preserve_type”. Setting this to true will provide slightly less elegant, but much more useful results in the above cases. Instead you get something like this:
Hash.from_xml('<variable type="product_code" subtype="upc">5</variable>', true)
# => {"variable"=>{"type"=>"product_code", "content"=>5, "subtype"=>"upc"}}The only downside to this solution is that our variable value is now stored in “content”. It is a touch less elegant, but I find it a worthwhile trade off for not losing any data.
As an added bonus, it’s not too hard to make use of this with ActiveResource, which is actually where I ran into the problem originally. All we have to do is add this Format to ActiveResource:
module ActiveResource
module Formats
module AttributePreservingXmlFormat
extend XmlFormat
def self.decode(xml)
from_xml_data(Hash.from_xml(xml, true))
end
end
end
endAnd then in your model:
class SomeResource < ActiveResource::Base
self.format = :attribute_preserving_xml
endWe’ve now prevented Hash.from_xml from killing our attributes.