Posted by Charlie
Fri, 31 Mar 2006 07:41:00 GMT
Let's revisit the
subject of content negotiation in Rails. Now that I've upgraded to Rails
1.1 I've had a chance to play with the new responds_to functionality. To
get some feedback, I also posted a comment to Jamis' blog and a message to
the REST microformat list.
My conclusion is that adding content negotiation to Rails would
be valuable. The new responds_to functionality is useful when
you want to override the default behavior. Having content negotiation
would be useful for making the default behavior more intelligent.
Templates already work in a similar manner. For example, say you write
this code:
class MyController < ApplicationController
def list
end
end
Rails will assume that you want to render a template called list.rhtml. However,
you can override this behavior as needed like this:
class MyController < ApplicationController
def list
render(:template => 'not_the_default_template')
end
end
Content negotiation would work in a similar manner. It would load the appropriate
template based on a client's request as specified in the HTTP accept header.You
can then override that behavior when needed using responds_to.
The way I propose this works is that
Content Types are mapped to templates via their file extensions. For instance:
| Content Type |
Template Name |
| text/html |
list.rhtml |
| application/xhtml+xml |
list.rxhtml |
| text/javascript, application/javascript |
list.rjs |
| application/atom+xml |
list.ratom |
| application/atom+rss |
list.rss |
| application/x-yaml |
list.ryaml |
One key thing to understand - the template extension does not dictate the
template type. Thus, list.rxhtml could be an ERB template or a Builder template
or any other sort of template.
Right now Rails doesn't support such a scheme because template extensions
are hard-coded to template types. This is done in ActionView::Base::create_template_source:
def create_template_source(extension, template, render_symbol, locals)
if template_requires_setup?(extension)
body = case extension.to_sym
when :rxml
"xml = Builder::XmlMarkup.new(:indent => 2)\n" +
"@controller.headers['Content-Type'] ||= 'application/xml'\n" +
template
when :rjs
"@controller.headers['Content-Type'] ||= 'text/javascript'\n" +
"update_page do |page|\n#{template}\nend"
end
else
body = ERB.new(template, nil, @@erb_trim_mode).src
end
@@template_args[render_symbol] ||= {}
locals_keys = @@template_args[render_symbol].keys | locals
@@template_args[render_symbol] = locals_keys.inject(
{}) { |h, k| h[k] = true; h }
locals_code= ""
locals_keys.each do |key|
locals_code << "#{key} = local_assigns[:#{key}]
if local_assigns.has_key?(:#{key})\n"
end
"def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend"
end
It would be easy to work around this code since the template parameter includes
the full source of the template. With a little regular expression magic,
you could figure out what type of template has been loaded and then run the
appropriate code.
However - what if we did something more clever? What if every template started
with an comment that specified its type - similar to a Unix shebang or
XML processing instruction.
Maybe something like this:
For ERB:
<!-- template_type :erb -->
For RJS:
# template_type :rjs
For Builder:
<!-- template_type :erb -->
It would then be easy to extract out the template type when it was loaded. If
the template comment did not exist (i.e., for all existing templates) then you
could revert to regular expression magic.
This solution has one very nice property - it would make it really easy to add
additional template types to Rails (not that its that hard now).
To recap, the idea is:
- Implement default content negotiation by mapping content types to template names
via file extensions (.rhtml, .rxthml, .rjs., .ratom, etc.)
- Add code to Rails so that it determine a template's type when the template is
loaded
- Use responds_to to override the default behavior as needed.
What do people think of this idea? If there is interest, I'll update my content
negotiation plugin and add this functionality to Rails 1.1.
Posted in HTTP, Rails | 8 comments | no trackbacks
Posted by Charlie
Fri, 31 Mar 2006 07:41:00 GMT
Let's revisit the
subject of content negotiation in Rails. Now that I've upgraded to Rails
1.1 I've had a chance to play with the new responds_to functionality. To
get some feedback, I also posted a comment to Jamis' blog and a message to
the REST microformat list.
My conclusion is that adding content negotiation to Rails would
be valuable. The new responds_to functionality is useful when
you want to override the default behavior. Having content negotiation
would be useful for making the default behavior more intelligent.
Templates already work in a similar manner. For example, say you write
this code:
class MyController < ApplicationController
def list
end
end
Rails will assume that you want to render a template called list.rhtml. However,
you can override this behavior as needed like this:
class MyController < ApplicationController
def list
render(:template => 'not_the_default_template')
end
end
Content negotiation would work in a similar manner. It would load the appropriate
template based on a client's request as specified in the HTTP accept header.You
can then override that behavior when needed using responds_to.
The way I propose this works is that
Content Types are mapped to templates via their file extensions. For instance:
| Content Type |
Template Name |
| text/html |
list.rhtml |
| application/xhtml+xml |
list.rxhtml |
| text/javascript, application/javascript |
list.rjs |
| application/atom+xml |
list.ratom |
| application/atom+rss |
list.rss |
| application/x-yaml |
list.ryaml |
One key thing to understand - the template extension does not dictate the
template type. Thus, list.rxhtml could be an ERB template or a Builder template
or any other sort of template.
Right now Rails doesn't support such a scheme because template extensions
are hard-coded to template types. This is done in ActionView::Base::create_template_source:
def create_template_source(extension, template, render_symbol, locals)
if template_requires_setup?(extension)
body = case extension.to_sym
when :rxml
"xml = Builder::XmlMarkup.new(:indent => 2)\n" +
"@controller.headers['Content-Type'] ||= 'application/xml'\n" +
template
when :rjs
"@controller.headers['Content-Type'] ||= 'text/javascript'\n" +
"update_page do |page|\n#{template}\nend"
end
else
body = ERB.new(template, nil, @@erb_trim_mode).src
end
@@template_args[render_symbol] ||= {}
locals_keys = @@template_args[render_symbol].keys | locals
@@template_args[render_symbol] = locals_keys.inject(
{}) { |h, k| h[k] = true; h }
locals_code= ""
locals_keys.each do |key|
locals_code << "#{key} = local_assigns[:#{key}]
if local_assigns.has_key?(:#{key})\n"
end
"def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend"
end
It would be easy to work around this code since the template parameter includes
the full source of the template. With a little regular expression magic,
you could figure out what type of template has been loaded and then run the
appropriate code.
However - what if we did something more clever? What if every template started
with an comment that specified its type - similar to a Unix shebang or
XML processing instruction.
Maybe something like this:
For ERB:
<!-- template_type :erb -->
For RJS:
# template_type :rjs
For Builder:
<!-- template_type :erb -->
It would then be easy to extract out the template type when it was loaded. If
the template comment did not exist (i.e., for all existing templates) then you
could revert to regular expression magic.
This solution has one very nice property - it would make it really easy to add
additional template types to Rails (not that its that hard now).
To recap, the idea is:
- Implement default content negotiation by mapping content types to template names
via file extensions (.rhtml, .rxthml, .rjs., .ratom, etc.)
- Add code to Rails so that it determine a template's type when the template is
loaded
- Use responds_to to override the default behavior as needed.
What do people think of this idea? If there is interest, I'll update my content
negotiation plugin and add this functionality to Rails 1.1.
Posted in HTTP, Rails | 8 comments | no trackbacks