Merge branch 'master' into condition-blocks

Conflicts:
	lib/rabl-rails.rb
	test/renderers/json_renderer_test.rb
This commit is contained in:
ccocchi 2012-10-05 14:08:45 +02:00
commit 842f63b080
18 changed files with 351 additions and 59 deletions

View File

@ -1,5 +1,22 @@
# CHANGELOG # CHANGELOG
## 0.2.1
* Avoid useless render on POST request with custom responder
* Custom responder now fallback to Rails default in case the template is not found
## 0.2.0
* Add `root` in DSL to set root without changing the data source
* Add XML renderer
* Use MultiJson's preferred JSON engine as default (shmeltex)
* Default template to render with responder can be set per controller
* Reponder works out of the box with devise
* object or collection can be skipped if use with `respond_to` blocks
## 0.1.3
* Render correctly when variables are not passed via the assigns ivar but as helper methods
(decent_exposure, focused_controller)
* Add custom Responder
## 0.1.2 ## 0.1.2
* Add RablRails#render method (see README or source code) * Add RablRails#render method (see README or source code)
* Fix fail when JSON engine is not found. Now fallback to MultiJson.default_adapter * Fix fail when JSON engine is not found. Now fallback to MultiJson.default_adapter
@ -8,7 +25,7 @@
## 0.1.1 ## 0.1.1
* Add CHANGELOG * Add CHANGELOG
* Remove unnused test in loop * Remove unused test in loop
* Speed up rendering by not double copying variable from context * Speed up rendering by not double copying variable from context
* Rename private variable to avoid name conflict * Rename private variable to avoid name conflict
* Remove sqlite3 development dependency * Remove sqlite3 development dependency

View File

@ -2,7 +2,8 @@ source "http://rubygems.org"
gemspec gemspec
gem 'yajl-ruby' gem 'oj'
gem 'libxml-ruby'
group :test do group :test do
gem 'rspec-mocks' gem 'rspec-mocks'

View File

@ -2,9 +2,9 @@
RABL (Ruby API Builder Language) is a ruby templating system for rendering resources in different format (JSON, XML, BSON, ...). You can find documentation [here](http://github.com/nesquena/rabl). RABL (Ruby API Builder Language) is a ruby templating system for rendering resources in different format (JSON, XML, BSON, ...). You can find documentation [here](http://github.com/nesquena/rabl).
RABL-rails only target Rails 3+ application because Rails 2 applications are becoming less and less present and will be obsolete with Rails 4. So let's look to the future ! rabl-rails is **faster** and uses **less memory** than the standard rabl gem while letting you access the same features. There are some slight changes to do on your templates to get this gem to work but it should't take you more than 5 minutes.
So now you ask why used `rabl-rails` if `rabl` already exists and supports Rails. Rabl-rails is **faster** and uses **less memory** than standard rabl gem while letting you access same features. Of course, there are some slight changes to do on your templates to get this gem to work but it should't take you more than 5 minutes. rabl-rails only target **Rails 3+ application**.
## Installation ## Installation
@ -24,7 +24,7 @@ And that's it !
## Overview ## Overview
Once you have installed RABL, you can directly used RABL templates to render your resources without changing anything to you controller. As example, Once you have installed rabl-rails, you can directly used RABL-rails templates to render your resources without changing anything to you controller. As example,
assuming you have a `Post` model filled with blog posts, and a `PostController` that look like this : assuming you have a `Post` model filled with blog posts, and a `PostController` that look like this :
```ruby ```ruby
@ -83,16 +83,19 @@ After the template is compiled into a hash, Rabl-rails will use a renderer to do
## Configuration ## Configuration
RablRails works out of the box, with default options and fastest engine available (yajl, libxml). But depending on your needs, you might want to change that or how your output looks like. You can set global configuration in your application: RablRails works out of the box, with default options and fastest engine available (oj, libxml). But depending on your needs, you might want to change that or how your output looks like. You can set global configuration in your application:
```ruby ```ruby
# config/initializers/rabl_rails.rb # config/initializers/rabl_rails.rb
RablRails.configure do |config| RablRails.configure do |config|
# These are the default # These are the default
# config.cache_templates = true # config.cache_templates = true
# config.include_json_root = true # config.include_json_root = true
# config.json_engine = :yajl # config.json_engine = :oj
end # config.xml_engine = 'LibXML'
# config.use_custom_responder = false
# config.default_responder_template = 'show'
end
``` ```
## Usage ## Usage
@ -127,6 +130,24 @@ node(:some_count) { |_| @user.posts.count }
child(:@user) { attribute :name } child(:@user) { attribute :name }
``` ```
If you use gem like *decent_exposure* or *focused_controller*, you can use your variable directly without the leading `@`
```ruby
object :object_exposed
```
You can even skip data declaration at all. If you used `respond_with`, rabl-rails will render the data you passed to it.
As there is no name, you can set a root via the `root` macro. This allow you to use your template without caring about variables passed to it.
```ruby
# in controller
respond_with(@post)
# in rabl-rails template
root :article
attribute :title
```
### Attributes / Methods ### Attributes / Methods
Basic usage is to declared attributes to include in the response. These can be database attributes or any instance method. Basic usage is to declared attributes to include in the response. These can be database attributes or any instance method.
@ -232,12 +253,6 @@ child :posts do
end end
``` ```
### Caching
Caching is not a part of Rabl-rails. It is already in Rails itself, because caching all view output is the same as action caching (Rails caching is even better because it will also not run your queries).
Moreover caching each object in a collection can be really not effective with big collections or simple objects. This is also a nightmare with cache expiration.
### Render object directly ### Render object directly
There are cases when you want to render object outside Rails view context. For instance to render objects in the console or to create message queue payloads. For these situations, you can use `RablRails.render` as show below: There are cases when you want to render object outside Rails view context. For instance to render objects in the console or to create message queue payloads. For these situations, you can use `RablRails.render` as show below:
@ -248,11 +263,15 @@ Rabl.render(object, template, :view_path => 'app/views', :format => :json) #=> "
You can find more informations about how to use this method in the [wiki](http://github.com/ccocchi/rabl-rails/wiki/Render-object-directly) You can find more informations about how to use this method in the [wiki](http://github.com/ccocchi/rabl-rails/wiki/Render-object-directly)
### Other features
You can find more informations about other features (caching, custom_responder, ...) in the [WIKI](https://github.com/ccocchi/rabl-rails/wiki)
## Performance ## Performance
Benchmarks have been made using this [application](http://github.com/ccocchi/rabl-benchmark), with rabl 0.6.14 and rabl-rails 0.1.0 Benchmarks have been made using this [application](http://github.com/ccocchi/rabl-benchmark), with rabl 0.6.14 and rabl-rails 0.1.0
Overall, Rabl-rails is **20% faster and use 10% less memory**. Overall, Rabl-rails is **20% faster and use 10% less memory**, even **twice faster** when rendering collections with extends.
You can see full tests on test application repository. You can see full tests on test application repository.

View File

@ -19,24 +19,44 @@ require 'multi_json'
module RablRails module RablRails
extend Renderer extend Renderer
autoload :Responder, 'rabl-rails/responder'
mattr_accessor :cache_templates mattr_accessor :cache_templates
@@cache_templates = true @@cache_templates = true
mattr_accessor :include_json_root mattr_accessor :include_json_root
@@include_json_root = true @@include_json_root = true
mattr_reader :json_engine mattr_accessor :use_custom_responder
@@json_engine = :yajl @@use_custom_responder = false
mattr_accessor :responder_default_template
@@responder_default_template = 'show'
def self.configure def self.configure
yield self yield self
ActionController::Base.responder = Responder if self.use_custom_responder
end end
def self.json_engine=(name) def self.json_engine=(name)
MultiJson.engine = name MultiJson.engine = name
@@json_engine = name
rescue LoadError rescue LoadError
Rails.logger.warn %Q(WARNING: rabl-rails could not load "#{self.json_engine}" as JSON engine, fallback to default) Rails.logger.warn %Q(WARNING: rabl-rails could not load "#{name}" as JSON engine, fallback to default)
end
def self.json_engine
MultiJson.engine
end
def self.xml_engine=(name)
ActiveSupport::XmlMini.backend = name
rescue LoadError, NameError
Rails.logger.warn %Q(WARNING: rabl-rails could not load "#{name}" as XML engine, fallback to default)
end
def self.xml_engine
ActiveSupport::XmlMini.backend
end end
def self.cache_templates? def self.cache_templates?
@ -44,6 +64,7 @@ module RablRails
end end
def self.load_default_engines! def self.load_default_engines!
self.json_engine = :yajl self.json_engine = MultiJson.default_engine
self.xml_engine = 'LibXML' if defined?(LibXML)
end end
end end

View File

@ -30,6 +30,10 @@ module RablRails
end end
alias_method :collection, :object alias_method :collection, :object
def root(name)
@template.root_name = name
end
# #
# Includes the attribute or method in the output # Includes the attribute or method in the output
# Example: # Example:

View File

@ -7,7 +7,7 @@ module RablRails
def self.call(template) def self.call(template)
%{ %{
RablRails::Library.instance. RablRails::Library.instance.
get_rendered_template(#{template.source.inspect}, self) get_rendered_template(#{template.source.inspect}, self, local_assigns)
} }
end end
end end

View File

@ -8,14 +8,14 @@ module RablRails
@cached_templates = {} @cached_templates = {}
end end
def get_rendered_template(source, context) def get_rendered_template(source, context, locals = nil)
path = context.instance_variable_get(:@virtual_path) path = context.instance_variable_get(:@virtual_path)
@lookup_context = context.lookup_context @lookup_context = context.lookup_context
compiled_template = compile_template_from_source(source, path) compiled_template = compile_template_from_source(source, path)
format = context.params[:format] || 'json' format = context.params[:format] || 'json'
Renderers.const_get(format.upcase!).new(context).render(compiled_template) Renderers.const_get(format.upcase!).new(context, locals).render(compiled_template)
end end
def compile_template_from_source(source, path = nil) def compile_template_from_source(source, path = nil)

View File

@ -1,5 +1,6 @@
require 'rabl-rails/renderers/base' require 'rabl-rails/renderers/base'
require 'rabl-rails/renderers/json' require 'rabl-rails/renderers/json'
require 'rabl-rails/renderers/xml'
module RablRails module RablRails
module Renderer module Renderer
@ -33,7 +34,6 @@ module RablRails
# #
class Context class Context
attr_reader :format attr_reader :format
attr_accessor :target_object
def initialize(path, options) def initialize(path, options)
@virtual_path = path @virtual_path = path
@ -80,12 +80,11 @@ module RablRails
object = options[:locals].delete(:object) if !object && options[:locals] object = options[:locals].delete(:object) if !object && options[:locals]
c = Context.new(template, options) c = Context.new(template, options)
c.target_object = object
t = c.lookup_context.find_template(template, [], false) t = c.lookup_context.find_template(template, [], false)
raise TemplateNotFound unless t raise TemplateNotFound unless t
Library.instance.get_rendered_template(t.source, c) Library.instance.get_rendered_template(t.source, c, resource: object)
end end
end end
end end

View File

@ -5,9 +5,10 @@ module RablRails
class Base class Base
attr_accessor :_options attr_accessor :_options
def initialize(context) # :nodoc: def initialize(context, locals = nil) # :nodoc:
@_context = context @_context = context
@_options = {} @_options = {}
@_resource = locals[:resource] if locals
setup_render_context setup_render_context
end end
@ -18,8 +19,14 @@ module RablRails
# method defined by the renderer. # method defined by the renderer.
# #
def render(template) def render(template)
collection_or_resource = instance_variable_get(template.data) if template.data collection_or_resource = if template.data
collection_or_resource = @_context.target_object unless collection_or_resource || template.data == false || !@_context.respond_to?(:target_object) if @_context.respond_to?(template.data)
@_context.send(template.data)
else
instance_variable_get(template.data)
end
end
collection_or_resource ||= @_resource
output_hash = collection_or_resource.respond_to?(:each) ? render_collection(collection_or_resource, template.source) : output_hash = collection_or_resource.respond_to?(:each) ? render_collection(collection_or_resource, template.source) :
render_resource(collection_or_resource, template.source) render_resource(collection_or_resource, template.source)
_options[:root_name] = template.root_name _options[:root_name] = template.root_name
@ -55,7 +62,13 @@ module RablRails
when Hash when Hash
current_value = value.dup current_value = value.dup
data_symbol = current_value.delete(:_data) data_symbol = current_value.delete(:_data)
object = data_symbol.nil? ? data : data_symbol.to_s.start_with?('@') ? instance_variable_get(data_symbol) : data.send(data_symbol) object = if data_symbol == nil
data
else
data_symbol.to_s.start_with?('@') ? instance_variable_get(data_symbol)
: data.respond_to?(data_symbol) ? data.send(data_symbol)
: send(data_symbol)
end
if key.to_s.start_with?('_') # glue if key.to_s.start_with?('_') # glue
current_value.each_pair { |k, v| current_value.each_pair { |k, v|

View File

@ -0,0 +1,14 @@
require 'active_support/core_ext/hash/conversions'
module RablRails
module Renderers
class XML < Base
DEFAULT_OPTIONS = { dasherize: true, skip_types: false }
def format_output(hash)
xml_options = { root: _options[:root_name] }.merge!(DEFAULT_OPTIONS)
hash.to_xml(xml_options)
end
end
end
end

View File

@ -0,0 +1,46 @@
module RablRails
#
# Override default responder's api behavior to not
# user to_format methods on a resource as a default
# representation but instead use a rabl template
#
class Responder < ActionController::Responder
def initialize(controller, resources, options = {})
super
if options[:locals]
options[:locals][:resource] = resource
else
options[:locals] = { resource: resource }
end
end
def to_format
if get? || response_overridden?
default_render
elsif has_errors?
display_errors
else
api_behavior(nil)
end
end
protected
def api_behavior(error)
if post?
template = if @controller.respond_to?(:responder_default_template, true)
controller.send(:responder_default_template)
else
RablRails.responder_default_template
end
options[:template] ||= "#{@controller.controller_name}/#{template}"
controller.default_render options.merge(status: :created, location: api_location)
else
head :no_content
end
rescue ActionView::MissingTemplate => e
super(e)
end
end
end

View File

@ -1,3 +1,3 @@
module RablRails module RablRails
VERSION = '0.1.2' VERSION = '0.2.1'
end end

View File

@ -24,6 +24,16 @@ class CompilerTest < ActiveSupport::TestCase
assert_equal({}, t.source) assert_equal({}, t.source)
end end
test "root can be defined via keyword" do
t = @compiler.compile_source(%{ root :author })
assert_equal :author, t.root_name
end
test "root keyword override object root" do
t = @compiler.compile_source(%{ object :@user ; root :author })
assert_equal :author, t.root_name
end
test "collection set the data for the template" do test "collection set the data for the template" do
t = @compiler.compile_source(%{ collection :@user }) t = @compiler.compile_source(%{ collection :@user })
assert_equal :@user, t.data assert_equal :@user, t.data

View File

@ -19,7 +19,6 @@ class DeepNestingTest < ActiveSupport::TestCase
@post = Post.new(42, 'I rock !') @post = Post.new(42, 'I rock !')
@user = User.new(1, 'foobar', 'male') @user = User.new(1, 'foobar', 'male')
@user.stub(:posts).and_return([@post]) @user.stub(:posts).and_return([@post])
@user.stub(:respond_to?).with(:each).and_return(false)
@context = Context.new @context = Context.new
@context.assigns['user'] = @user @context.assigns['user'] = @user

View File

@ -6,7 +6,6 @@ class RenderTest < ActiveSupport::TestCase
setup do setup do
@user = User.new(1, 'Marty') @user = User.new(1, 'Marty')
@user.stub(:respond_to?).with(:each).and_return(false)
@tmp_path = Pathname.new(Dir.mktmpdir) @tmp_path = Pathname.new(Dir.mktmpdir)
end end

View File

@ -4,7 +4,6 @@ class TestJsonRenderer < ActiveSupport::TestCase
setup do setup do
@data = User.new(1, 'foobar', 'male') @data = User.new(1, 'foobar', 'male')
@data.stub(:respond_to?).with(:each).and_return(false)
@context = Context.new @context = Context.new
@context.assigns['data'] = @data @context.assigns['data'] = @data
@ -28,6 +27,13 @@ class TestJsonRenderer < ActiveSupport::TestCase
assert_equal %q([{}]), render_json_output assert_equal %q([{}]), render_json_output
end end
test "render object with local methods (used by decent_exposure)" do
@context.stub(:user).and_return(@data)
@template.data = :user
@template.source = { :id => :id }
assert_equal %q({"id":1}), render_json_output
end
test "render single object attributes" do test "render single object attributes" do
@template.source = { :id => :id, :name => :name } @template.source = { :id => :id, :name => :name }
assert_equal %q({"id":1,"name":"foobar"}), render_json_output assert_equal %q({"id":1,"name":"foobar"}), render_json_output
@ -44,6 +50,12 @@ class TestJsonRenderer < ActiveSupport::TestCase
assert_equal %q({"author":{"name":"foobar"}}), render_json_output assert_equal %q({"author":{"name":"foobar"}}), render_json_output
end end
test "render child with local methods (used by decent_exposure)" do
@context.stub(:user).and_return(@data)
@template.source = { :author => { :_data => :user, :name => :name } }
assert_equal %q({"author":{"name":"foobar"}}), render_json_output
end
test "render glued attributes from single object" do test "render glued attributes from single object" do
@template.source = { :_glue0 => { :_data => :@data, :name => :name } } @template.source = { :_glue0 => { :_data => :@data, :name => :name } }
assert_equal %q({"name":"foobar"}), render_json_output assert_equal %q({"name":"foobar"}), render_json_output
@ -77,6 +89,7 @@ class TestJsonRenderer < ActiveSupport::TestCase
end end
test "node with context method call" do test "node with context method call" do
@context.stub(:respond_to?).with(:@data).and_return(false)
@context.stub(:respond_to?).with(:context_method).and_return(true) @context.stub(:respond_to?).with(:context_method).and_return(true)
@context.stub(:context_method).and_return('marty') @context.stub(:context_method).and_return('marty')
proc = lambda { |object| context_method } proc = lambda { |object| context_method }
@ -86,7 +99,6 @@ class TestJsonRenderer < ActiveSupport::TestCase
test "partial should be evaluated at rendering time" do test "partial should be evaluated at rendering time" do
# Set assigns # Set assigns
@data.stub(:respond_to?).with(:empty?).and_return(false)
@context.assigns['user'] = @data @context.assigns['user'] = @data
# Stub Library#get # Stub Library#get

View File

@ -0,0 +1,131 @@
require 'test_helper'
class TestXmlRenderer < ActiveSupport::TestCase
INDENT_REGEXP = /\n(\s)*/
HEADER_REGEXP = /<[^>]+>/
setup do
@data = User.new(1, 'foobar', 'male')
@context = Context.new
@context.assigns['data'] = @data
@template = RablRails::CompiledTemplate.new
@template.data = :@data
@template.root_name = :user
end
def render_xml_output
RablRails::Renderers::XML.new(@context).render(@template).to_s.gsub!(INDENT_REGEXP, '').sub!(HEADER_REGEXP, '')
end
test "render object simple object" do
@template.source = {}
assert_equal %q(<user></user>), render_xml_output
end
test "render collection with empty template" do
@context.assigns['data'] = [@data]
@template.source = {}
@template.root_name = :users
assert_equal %q(<users type="array"><user></user></users>), render_xml_output
end
test "render object with local methods (used by decent_exposure)" do
@context.stub(:user).and_return(@data)
@template.source = { :id => :id }
assert_equal %q(<user><id type="integer">1</id></user>), render_xml_output
end
test "render single object attributes" do
@template.source = { :name => :name }
assert_equal %q(<user><name>foobar</name></user>), render_xml_output
end
test "render child with arbitrary data source" do
@template.source = { :author => { :_data => :@data, :name => :name } }
@template.root_name = :post
assert_equal %q(<post><author><name>foobar</name></author></post>), render_xml_output
end
test "render child with local methods (used by decent_exposure)" do
@context.stub(:user).and_return(@data)
@template.source = { :author => { :_data => :user, :name => :name } }
@template.root_name = :post
assert_equal %q(<post><author><name>foobar</name></author></post>), render_xml_output
end
test "render glued attributes from single object" do
@template.source = { :_glue0 => { :_data => :@data, :name => :name } }
assert_equal %q(<user><name>foobar</name></user>), render_xml_output
end
test "render collection with attributes" do
@data = [User.new(1, 'foo', 'male'), User.new(2, 'bar', 'female')]
@context.assigns['data'] = @data
@template.root_name = :users
@template.source = { :uid => :id, :name => :name }
assert_equal %q(<users type="array"><user><uid type="integer">1</uid><name>foo</name></user><user><uid type="integer">2</uid><name>bar</name></user></users>), render_xml_output
end
test "render node property" do
proc = lambda { |object| object.name }
@template.source = { :name => proc }
assert_equal %q(<user><name>foobar</name></user>), render_xml_output
end
test "render node property with true condition" do
condition = lambda { |u| true }
proc = lambda { |object| object.name }
@template.source = { :name => [condition, proc] }
assert_equal %q(<user><name>foobar</name></user>), render_xml_output
end
test "render node property with false condition" do
condition = lambda { |u| false }
proc = lambda { |object| object.name }
@template.source = { :name => [condition, proc] }
assert_equal %q(<user></user>), render_xml_output
end
test "node with context method call" do
@context.stub(:respond_to?).with(:@data).and_return(false)
@context.stub(:respond_to?).with(:context_method).and_return(true)
@context.stub(:context_method).and_return('marty')
proc = lambda { |object| context_method }
@template.source = { :name => proc }
assert_equal %q(<user><name>marty</name></user>), render_xml_output
end
test "partial should be evaluated at rendering time" do
# Set assigns
@context.assigns['user'] = @data
# Stub Library#get
t = RablRails::CompiledTemplate.new
t.source = { :name => :name }
RablRails::Library.reset_instance
RablRails::Library.instance.should_receive(:compile_template_from_path).with('users/base').and_return(t)
@template.data = false
@template.root_name = :post
@template.source = { :user => ->(s) { partial('users/base', :object => @user) } }
assert_equal %q(<post><user><name>foobar</name></user></post>), render_xml_output
end
test "partial with no values should raise an error" do
@template.data = false
@template.source = { :user => ->(s) { partial('users/base') } }
assert_raises(RablRails::Renderers::PartialError) { render_xml_output }
end
test "partial with empty values should not raise an error" do
@template.data = false
@template.root_name = :list
@template.source = { :users => ->(s) { partial('users/base', :object => []) } }
assert_equal %q(<list><users type="array"/></list>), render_xml_output
end
end

View File

@ -47,4 +47,11 @@ class Context
end end
end end
User = Struct.new(:id, :name, :sex) class User
attr_accessor :id, :name, :sex
def initialize(id=nil, name=nil, sex=nil)
@id = id
@name = name
@sex = sex
end
end