Programming with Soulcutter

Write code like you mean it

Default Hash Values (the RIGHT Way)

In my recent post about default hash values I was posting something I thought was pretty basic, and embarassingly I missed what should have been obvious. Right there in the documentation for Hash is the best way to initialize Hash keys on-demand:

1
2
3
4
options = Hash.new {|hash, key| hash[key] = [] } # => {}
options[:key] # => []
options[:key] << 'thing' # => ["thing"]
options # => {:key=>["thing"]}

I know I must've seen this before, but I guess it never stuck with me. Thanks to @alindeman, @pete_higgins, and @samphippen for straightening me out.

Is it better to be thought a fool than to open your mouth and remove all doubt? I dunno, but this fool learned the RIGHT answer to the problem as well as a dose of humility. I suppose that's worth something!

Default Hash Values

UPDATE: Most of the advice in this post is superceded by the followup which reflects the best solution for default hash values.

If you ever find yourself writing initialization for ruby hash values that looks something like this:

1
2
3
4
5
options = {}
options[:key] ||= []
options[:key] << 'thing'
options[:key] << 'other thing'
options # => {:key=>["thing", "other thing"]}

You can save yourself the ||= statement by initializing your hash with a default value.

1
2
3
4
options = Hash.new []
options[:key] += ['thing']
options[:key] += ['other thing']
options # => {:key=>["thing", "other thing"]}

One small caveat is that accessing a hash at an unknown key will return the exact instance that you gave to your Hash initializer. If it's a mutable object such as the Array in this example, then mutating the object for an unknown key will change the value for ALL unknown keys.

1
2
3
4
5
options = Hash.new []
options[:key] << 'thing'
options[:key]     # => ['thing'] great!
options[:unknown] # => ['thing'] umm, this is unexpected
options           # => {} we never assigned a value to our hash key at all!

So go forth and use default hash values! Just be mindful to avoid changes to the default value object.

Addendum

I was reminded by @samphippen and @pete_higgins that there is a way to prevent mutations from affecting the default value, and that is to initialize your hash with a block:

1
2
3
4
5
6
7
8
9
10
options = Hash.new { [] }

options[:key] << 'thing'
options[:key] # => []
options[:unknown] # => []
options # => {}

options[:key] += ['thing']
options[:key] += ['other thing']
options # => {:key=>["thing", "other thing"]}

As you can see there is still a potential bug lurking above where we never actually assign a value to the hash key, however instead of returning a mutated instance for missing values what you get is the result of evaluating the block that is passed into the hash initializer.

In the scheme of things this is a better solution for initializing a hash with a default value since the default will never get polluted by accidental mutations on the default object.

Speeding Up Tests That Interact With Dragonfly

I noticed that my rspec tests which interacted with Dragonfly attachments on my models were slowing down my test suite. Without even profiling I could see in my terminal menu bar that running these specs was resulting in system calls to ImageMagick convert and identify commands.

This seemed entirely unnecessary to me in a test environment, so I came up with this small monkeypatch to bypass Dragonfly's typical calls out to ImageMagick:

spec/support/dragonfly.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module Dragonfly
  module ImageMagick
    module Utils

      private

      # does not run actual ImageMagick conversion, just returns the same temp object
      def convert(temp_object=nil, *_)
        temp_object
      end

      def identify(*_)
        # this is totally arbitrary
        { format: :png, width: 300, height: 250, depth: 8 }
      end
    end
  end
end

YMMV, but this resulted in a pretty significant speed increase in my image-centric application - on the order of 2x faster! Good times.

How to Turn Off Dragonfly’s Default Verbose Rack-cache

It was a minor annoyance that my feature specs were incredibly verbose, filling my console with rack-cache trace debugging information like so:

1
2
3
cache: [GET /assets/logo.jpg] miss, store
cache: [GET /assets/bg-nav.jpg] miss, store
cache: [GET /assets/bg-box.jpg] miss, store

I tracked it down to the fact that Dragonfly's default configuration adds rack-cache to your middleware stack configured with verbose logging enabled.

My solution was to add a block in my Dragonfly initializer to remove that noisy rack-cache and insert my own quiet version.

config/initializers/dragonfly.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require 'dragonfly/rails/images'

app = Dragonfly[:images]

# some storage settings here...

# shuts down verbose cache logging
if %w(development test).include? Rails.env
  Rails.application.middleware.delete(Rack::Cache)

  Rails.application.middleware.insert 0, Rack::Cache, {
    :verbose     => false, # this is set to true in dragonfly/rails/images
    :metastore   => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/meta"), # URI encoded in case of spaces
    :entitystore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/body")
  }
end

Now my testing serenity is unperturbed by extra logging noise.

Click Events in Poltergeist vs Capybara-webkit

Due to constraints from a continuous integration service that I am using, I found myself converting from capybara-webkit to poltergeist for my headless javascriptable feature testing needs. To capybara's credit, the driver change is as simple as Capybara.javascript_driver = :poltergeist

Unfortunately I discovered that after this simple substitution some of my features involving asynchronous requests began to fail intermittently. By taking screenshots of the points of failure I discovered that calls to find(selector).click did not always appear to trigger JavaScript click events.

I cannot say whether the root cause has to do with element visibility, CSS animations, or the JavaScript callbacks not being registered in time for the clicks to trigger them, however I -did- find that switching to find(selector).trigger('click') solved my problem.

Somewhat annoying considering this behavior "just worked" in capybara-webkit, but I suppose I'll have to live with it for now.

For posterity here are the versions I hit this issue with:

  • capybara: 2.0.2
  • poltergeist: 1.1.0

Localization of Nested Attributes in Rails 3.2

When you refer to one shared model from a handful of other models, sometimes you need to give the shared model's attributes different user-facing names since they are being used in a different context. Unfortunately the documentation for using Rails' built-in localization does not indicate how to accomplish this feat.

Fear not, there is a way!

If you can look past a contrived example, let's say you've got models for Organization and Customer, and each of them refer to a ContactInfo. The unnested way to use Rails i18n would be something like this:

config/locales/en.yml
1
2
3
4
5
en:
  activerecord:
    attributes:
      contact_info:
        phone: Phone number
1
ContactInfo.human_attribute_name("phone") # => "Phone number"

To show different strings for Organization and Customer, the yaml syntax has a slight twist - rather than nesting contact_info inside of organization or naming they key organization.contact_info the nested attribute should be separated with a / character.

config/locales/en.yml
1
2
3
4
5
6
7
en:
  activerecord:
    attributes:
      organization/contact_info:
        phone: Customer service number
      customer/contact_info:
        phone: Phone number

Despite the yaml syntax, the new call for a nested attribute with human_attribute_name is dot-separated. Feels a little weird since the yaml uses slashes, but there you have it.

1
2
Organization.human_attribute_name("contact_info.phone") # => Customer service number
Customer.human_attribute_name("contact_info.phone") # => Phone number

I had a hard time tracking down this syntax through the documentation or google, and it appears that this worked differently pre-3.2. This seems like it would be a good candidate for a Rails documentation contribution.