Wednesday, October 31, 2007

Adding Mime Types to Merb

Adding a new mime type in Merb is really simple. It's just one line.

Lets say we want the following:

  • .pdf extension to be associated with the :pdf content_type

  • Want to call to_pdf on an object we give to render

  • Content-Type: application/pdf

  • Content-Encoding: gzip

For all this we just make a call to Merb.add_mime_type.
The arguments to Merb.add_mime_type are

  1. Symbol for content_type

  2. Symbol of method to call to convert an object to this format

  3. An array of possible content types that the browser might give the server for this format. The first one will be used as the outgoing Content-Type header

  4. A hash of response headers you want to include [optional]

To write add our pdf mime type we would put the following into merb_init.rb down the bottom.
Merb.add_mime_type(:pdf, :to_pdf, %w[application/pdf], "Content-Encoding" => "gzip")

That's it. Now in your controllers you can use
provides :pdf
# and then
render @obj
and to_pdf will be called on @obj.

This looks like a great opportunity to use a presenter object with a to_pdf method on it.

If there's a format that you never want to use a transform method on, since you'll always have templates for them, just use nil for the transform method.

Sunday, October 28, 2007

Goodbye respond_to, Hello provides

With the upcoming release of Merb 0.4.0 lets say goodbye to respond_to and hello to provides.

This isn't as bad as it might look at first glance. I remember when rails first brought out respond_to. I thought it was awesome, and it certainly has been a good, enabling method. Merb borrowed this along with a slew of other good ideas from rails, but today Merb transcends respond_to.

The provides method does a similar job to respond_to. It gives the user the format they want if it's available. It just does it a bit differently.

Lets have an example


class OldWay < Merb::Controller

def index
@items = Item.find(:all)
respond_to do |fmt|
fmt.html { render }
fmt.js { @items.to_json }
fmt.xml { render } # use a template
end
end

def show
@item = Item.find(:first)
respond_to do |fmt|
fmt.js { render } # use a template
fmt.xml{ @item.to_xml }
end
end

#...
end

I'm sure this kind of controller setup is reasonably familiar with anyone who is offering more than plain html to users.

Lets see this same example as it is with the new provides functionality.

class NewWay < Merb::Controller
provides :js, :xml

def index
@items = Item.find(:all)
render @items
end

def show
does_not_provide :html

@item = Item.find(:first)
render @item
end

#...
end

To my eye, that's much more concise.

This will respond to the user with the format they selected and the correct headers. But the good juju is not just that provides will select the format to return to the client. Render now accepts a model or ORM collection object as well. Say you call render with an object it will render a template if it finds one, otherwise it will attempt to call to_format method on the object.

What... No Blocks per Format

This behavior should cover a good proportion of cases without intervention, but if you need something more explicit you can fall back to good ol' ruby and use a case statement. The content_type method will give you the current format so you can be as fine grained as you like.

How to Control all this POWER?

There are a number of methods available.

provides :format1, :format2
only_provides :format1, :format3
does_not_provide :format6

These are available as both class an instance methods so you can combine them for very fine control.

Is it extensible?

What if you want to add your own format that a controller provides? Lets say you wanted to add an MS Word format for your controller. First add the mime type you want to merb specifying the format extension, transforming method, and possible accept headers.

Merb.add_mime_type(:doc, :to_doc, %w[application/vnd.ms-word])

This makes the doc format available for use in your controller

provides :doc

# ...

render @obj

to_doc will be called on @obj and the outgoing content-type header is set to application/vnd.ms-word. Using to_doc on a model is not the best idea, please remember I'm just using it as an example.

Thursday, October 11, 2007

Ruby: Default Argument Gotcha

Tonight I got very frustrated chasing a bug.

I was getting a method missing for a nil object with the + method.

def text_area_field(val="", attrs={})
add_label(attrs) do
open_tag("textarea", attrs) +
val +
"</textarea>"
end
end

I thought that if val was nil it would be set to a blank string. This is only the case when it's nil because it wasn't specified. I was sending nil to it from another method, and so it really did put a nil in there. Nasty.

I changed it to this and it worked as expected.

def text_area_field(val, attrs={})
val ||=""
add_label(attrs) do
open_tag("textarea", attrs) +
val +
"</textarea>"
end
end

Wednesday, October 10, 2007

Enabling Sessions on Merb

I just fired up merb for a project and i realised, I didn't know how to turn on sessions. It turns out its easy.

In merb.yml, to use the data_mapper sessions support include the line

:session_store: data_mapper

Alternatively you can use memory, cookie, memcache or a custom one from an ORM plugin.

Usually when using an ORM plugin a migration is required. To find out what's available use

rake -T

At the moment the datamapper plugins rake tasks are broken and don't generate migrations. You can still use the datamapper sessions, just start merb and if the table is missing, it will be created.

Easy :)

Friday, October 5, 2007

Using Rubigen With Scope for Merb Plugins

I like to help where I can with contributing to merb. I tried to use the merb_sequel plugin the other day and found that there are no generators. It seems I've been spoiled by the generators in rails getting the basic empty model, migration and test stubs are great when I want to write a model. The only generator for merb_sequel was a migration one but I wanted more. That was ok though, I wanted to learn about generators anyway.

Merb has the rubigen gem in it to generate the application structure, so it's only natural to use it. Rubigen was extracted from rails by DrNic and I was lucky enough for him to explain the general workings of it to me. The major idea that I really liked was the ability to use scope in your gem generators.

What does using scope in your gem generators mean? Well, there is a line in a merb applications script/generate file


RubiGen::Base.use_component_sources! [:merb, :rspec]

What this line does is when you call script/generate it goes through all your gems and loads any generators found in the folders merb_generators and rspec_generators. [:merb, :rspec] are the generator scopes that merb has by default. All gems with merb or rspec scoped generators are loaded and available when script/generate is called. Newgem has some great examples of this in use. How do you scope these in your gems? For this example, include them in the following directories in your gem.

merb_genereators
rspec_generators

So if you do

ruby scirpt/generate

A list of all the generators of all the gems in those scopes. OK but what good is that?

In the merb_sequel plugin this allowed me to cater for both rspec and test unit stubs when generating a model. I just create the same named generator in a different scope and then whichever scope the user has selected is available. I used this when generating models to include the stubs. The directories I used were

merb_generators
sequel_migration
sequel_model
rspec_generators
sequel_model_test
test_unit_generators
sequel_model_test

Then in the model generator

record do
...
m.dependency "sequel_model_test", [@name]
end

This is where drnic's magic happens. The scope, set in script/generate, will only pick up the sequel_model_test generator from the rspec_generators directory and not the test_unit one. So when the dependency gets called it's the one defined in rspec_generators that it finds. Of course, if the scope is set to :test_unit

RubiGen::Base.use_component_sources! [:merb, :test_unit]

the sequel_model generator, when the dependency is called will find the seuqel_model_test defined in the test_unit_generators directory instead and use that.

This is great in that it allows you to select the flavor of testing that you like, and the generators will give it to you. That is providing that the gem has both rspec and test_unit generators defined.

By the way, if you call a dependency and there is neither scope is selected then an error will be raised. This is because there is no dependency of that name loaded to find.

In the merb_sequel plugin I've hacked a bit into it to check that an :rspec or :test_unit scope has been set. If a scope has not been set the test stub won't be generated and a message will show up.

I haven't found a way to dynamically set and then unset the scope which I'm guessing would be good for default scopes. That would be better in this case than the hack to not generate the test stubs.

Tuesday, October 2, 2007

What's Happening

I'm really bad at writing blog articles so I've got a plan. I'm going to write a merb app, something I've been meaning to do for a while, and try and document it as I go.

Well, lets see how I go :)

Monday, October 1, 2007

Mysql Gem on OS X 10.4

I'm starting to work with merb and I thought I'd give the sequel plugin a try.

I hoped it would be sweet but I had issues. The mysql gem needed to be installed. Apparently the AR gem has the mysql adapter coded into it and so it doesn't need the mysql gem, but I want to try sequel so I decided to get the gem installed.

By default it didn't work. The following is what I did to get it working. This is based on information I found here but it didn't work correctly for me.

According to the article at http://blog.maxdunn.com the gcc needs to be set to 3.3 I left mine at 4.0.1 and it worked fine.

First try and install the gem. This will setup the directories and get all the files.

sudo gem install mysql -- --with-mysql-dir=/usr/local/mysql
cd /usr/local/ruby/gems/1.8/gems/mysql-2.7

Edit mysql.c.in and add the following line at the top.

#define ulong unsigned long

Then

sudo ruby extconf.rb --with-mysql-dir=/usr/local/mysql
sudo make
sudo make install
sudo make clean

I can't promise this will work for anyone else but it worked for me.