Specifying Merb Mailers

This helper is based on some other controller/view helpers I've been working on and planning on blogging soon, with a nod to the specs present in the merb-mailer library itself.

I'm still considering the idea of separate specs for UserMailer and its views, but I think the overhead is too much for mailers, compared to the benefits we get for regular controllers/views. I think this is a result of the way the send_mail helper functions.

in a controller

send_mail UserMailer, :hello, {
  :from => "greeter@example.com",
  :to => @person.email,
  :subject => "Greetings"
}, {
  :name => @person.name
}

The controller spec can simply stub/mock the send_mail call as appropriate.

spec/spec_helper.rb

Merb::Mailer.delivery_method = :test_send
def describe_mail(mailer, template, &block)
  describe "/#{mailer.to_s.downcase}/#{template}" do
    before :each do
      @mailer_class, @template = mailer, template
      @assigns = {}
    end

    def deliver(send_params={}, mail_params={})
      mail_params = {:from => "from@example.com", :to => "to@example.com", :subject => "Subject Line"}.merge(mail_params)
      @mailer_class.new(send_params).dispatch_and_deliver @template.to_sym, mail_params
      @mail = Merb::Mailer.deliveries.last
    end

    instance_eval &block
  end
end

spec/mailers/user_mailer_spec.rb

require File.join(File.dirname(__FILE__),'..','spec_helper')

describe_mail UserMailer, :hello do
  it "should say hello" do
    deliver :name => "Jamie"
    @mail.text.should == "Hello Jamie"
  end
end

I'm a big fan of custom rspec describers, as above. The fact that before and after blocks are transparently inherited is a huge win over test/unit, where you'd need to explicitly call super.

app/mailers/user_mailer.rb

class UserMailer < Merb::MailController
  def hello
    render_mail
  end  
end

app/mailers/views/user_mailer/hello.text.erb

Hello <%= params[:name] %>

Using Datamapper 0.9

So, DataMapper 0.3 was behaving weirdly for me, and I thought I'd try upgrading to 0.9 to see how things are there. Overall I'm quite liking it, but there's a few catches:

  • It's incompatible with Vlad for deployment, haven't looked into it but something in DM is making the 'repository' value unsettable, which Vlad uses to determine the checkout path.

  • Legacy connections beware, there doesn't seem to be a current alternative for settablename at the moment.

  • :memory: is no longer a good name for your test database when using sqlite. Use a fully-qualified connection string like sqlite://:memory: instead.

  • DataMapper::Persistence has been renamed DataMapper::Resource. Include it in models.

  • Validations are an add-on now, include DataMapper::Validate in your model (or even re-open DM:Resource and include it there).

  • Properties now take a class instead of a symbol (you can guess at all the main ones), and require the id to be specified like so: property :id, Fixnum, :serial => true

  • Associations are renamed, hasone is now onetoone, hasmany is oneto_many or manyto_many, etc. Haven't delved in deep yet though, so I'm not sure how to define who gets the foreign key.

  • New migration code is just now getting in to dm-core, and auto_migrate! is a thing of the past. Sucks for those of us using sqlite in-memory test databases that need a fresh migration every time.

My installation Rakefile follows, just stuff it in an empty directory and it'll do everything from there. Much thanks to Atmos for a good starting point and some setup help.

desc "Fetch and Install DM and Merb"
task :install_all do 
  config = CONFIG['dm']
  fetch config[:user], config[:repos]
  install config[:install]
  config = CONFIG['merb']
  fetch config[:user], config[:repos]
  install config[:install]
end

desc "Uninstall DM and Merb"
task :uninstall_all do
  uninstall CONFIG['dm'][:gems]  
  uninstall CONFIG['merb'][:gems]  
end

desc "Download latest sources for :project from git"
task :fetch, :project do |task, args|
  config = CONFIG[args[:project]]
  fetch config[:user], config[:repos]
end

desc "Install :project from git"
task :install, :project do |task, args|
  config = CONFIG[args[:project]]
  install config[:install]
end

desc "Uninstall :project"
task :uninstall, :project do |task, args|
  config = CONFIG[args[:project]]
  uninstall config[:gems]
end

def fetch(user, repos)
  base = File.expand_path(".")
  Dir.chdir base do
    repos.each do |repo|
      repo_dir = "#{base}/#{repo}"
      unless File.directory?(repo_dir)
        %x{git clone git://github.com/#{user}/#{repo}.git }
      end
      Dir.chdir(repo_dir) { %x{git pull} }
    end
  end
end

def install(modules)
  base = File.expand_path(".")
  modules.each do |lib|
    Dir.chdir("#{base}/#{lib}") do
      cmd = "sudo rake install 2>/dev/null |" +
            " grep -v '^(in' |" +
            " grep -v '^[0-9] gem' |" +
            " grep -v '^[IUc. ]'"
      puts %x{#{cmd}}
    end
  end
end

def uninstall(gems)
  gems.each do |name|
    puts %x{yes | sudo gem uninstall #{name} -aI}
  end
end

CONFIG = {
  'dm' => {
    :user => 'sam',
    :repos => %w(
      do
      dm-core
      dm-more),
    :install => %w(
      do/data_objects
      do/do_sqlite3
      do/do_mysql
      do/do_postgres
      dm-core
      dm-more/merb_datamapper
      dm-more/dm-migrations
      dm-more/dm-serializer
      dm-more/dm-validations
    ),
    :gems => %w(
      data_objects
      do_sqlite3
      do_mysql
      do_postgres
      dm-core
      merb_datamapper
      dm-migrations
      dm-serializer
      dm-validations
    )
  },
  'merb' => {
    :user => 'wycats',
    :repos => %w(
      merb-core
      merb-more
      merb-plugins
      merb-plugins/merb_param_protection),
    :install => %w(
      merb-core
      merb-more
      merb-plugins
    ),
    :gems => %w(
      merb
      merb-action-args
      merb-assets
      merb-builder
      merb-cache
      merb-core
      merb-gen
      merb-haml
      merb-mailer
      merb-more
      merb-parts
      merb_activerecord
      merb_datamapper
      merb_helpers
      merb_param_protection
      merb_rspec
      merb_sequel
      merb_stories
      merb_test_unit
    )
  }
}

Snippet: col

#!/usr/bin/env ruby
col = ARGV.pop.to_i-1
while line = gets
  puts line.chomp.split(/\s+/)[col]
end

For when you just want a list of filenames from version control, hg st | grep '?' | col 2

And because I can never remember the standard unix tool that does the same thing, and awk is awkward.

Now with comments

I've got comments done up now, and I'm mostly happy with them. They should show up on all the articles added this year.

For spam management purposes, I'm approving comments, but once you've got one approved comment it'll let your email address through immediately in the future.

Fish at Last!

Somehow, the last update of MacPorts has let me install Fish cleanly, rather than obtusely blowing up as it has in the past.

I highly recommend trying it out: port selfupdate followed by port install fish should do the trick.

If you want to use it as your default shell, you'll need to sudo echo /opt/local/bin/fish >> /etc/shells so chsh will treat it as an approved shell.