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
## 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
* Add RablRails#render method (see README or source code)
* Fix fail when JSON engine is not found. Now fallback to MultiJson.default_adapter
@ -8,7 +25,7 @@
## 0.1.1
* Add CHANGELOG
* Remove unnused test in loop
* Remove unused test in loop
* Speed up rendering by not double copying variable from context
* 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
gem 'yajl-ruby'
gem 'oj'
gem 'libxml-ruby'
group :test do
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-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
@ -24,7 +24,7 @@ And that's it !
## 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 :
```ruby
@ -32,7 +32,7 @@ class PostController < ApplicationController
respond_to :html, :json, :xml
def index
@posts = Post.order('created_at DESC')
@posts = Post.order('created_at DESC')
respond_with(@posts)
end
end
@ -71,9 +71,9 @@ The only places where you can actually used instance variables are into Proc (o
```ruby
# We reference the @posts varibles that will be used at rendering time
collection :@posts
# Here you can use directly the instance variable because it
# will be evaluated when rendering the object
# will be evaluated when rendering the object
node(:read) { |post| post.read_by?(@user) }
```
@ -83,16 +83,19 @@ After the template is compiled into a hash, Rabl-rails will use a renderer to do
## 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
# config/initializers/rabl_rails.rb
RablRails.configure do |config|
# These are the default
# config.cache_templates = true
# config.include_json_root = true
# config.json_engine = :yajl
end
# config/initializers/rabl_rails.rb
RablRails.configure do |config|
# These are the default
# config.cache_templates = true
# config.include_json_root = true
# config.json_engine = :oj
# config.xml_engine = 'LibXML'
# config.use_custom_responder = false
# config.default_responder_template = 'show'
end
```
## Usage
@ -127,6 +130,24 @@ node(:some_count) { |_| @user.posts.count }
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
Basic usage is to declared attributes to include in the response. These can be database attributes or any instance method.
@ -139,7 +160,7 @@ You can aliases these attributes in your response
```ruby
attributes title: :foo, to_s: :bar
# => { "foo" : <title value>, "bar" : <to_s value> }
# => { "foo" : <title value>, "bar" : <to_s value> }
```
### Child nodes
@ -175,7 +196,7 @@ node(:full_name) { |u| u.first_name + " " + u.last_name }
You can add the node only if a condition is true
```ruby
node(:email, if: -> { |u| u.valid_email? }) do |u|
node(:email, if: -> { |u| u.valid_email? }) do |u|
u.email
end
```
@ -232,12 +253,6 @@ child :posts do
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
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:
@ -246,13 +261,17 @@ There are cases when you want to render object outside Rails view context. For i
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
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.

View File

@ -19,24 +19,44 @@ require 'multi_json'
module RablRails
extend Renderer
autoload :Responder, 'rabl-rails/responder'
mattr_accessor :cache_templates
@@cache_templates = true
mattr_accessor :include_json_root
@@include_json_root = true
mattr_reader :json_engine
@@json_engine = :yajl
mattr_accessor :use_custom_responder
@@use_custom_responder = false
mattr_accessor :responder_default_template
@@responder_default_template = 'show'
def self.configure
yield self
ActionController::Base.responder = Responder if self.use_custom_responder
end
def self.json_engine=(name)
MultiJson.engine = name
@@json_engine = name
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
def self.cache_templates?
@ -44,6 +64,7 @@ module RablRails
end
def self.load_default_engines!
self.json_engine = :yajl
self.json_engine = MultiJson.default_engine
self.xml_engine = 'LibXML' if defined?(LibXML)
end
end

View File

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

View File

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

View File

@ -8,14 +8,14 @@ module RablRails
@cached_templates = {}
end
def get_rendered_template(source, context)
def get_rendered_template(source, context, locals = nil)
path = context.instance_variable_get(:@virtual_path)
@lookup_context = context.lookup_context
compiled_template = compile_template_from_source(source, path)
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
def compile_template_from_source(source, path = nil)

View File

@ -1,10 +1,11 @@
require 'rabl-rails/renderers/base'
require 'rabl-rails/renderers/json'
require 'rabl-rails/renderers/xml'
module RablRails
module Renderer
class TemplateNotFound < StandardError; end
mattr_reader :view_path
@@view_path = 'app/views'
@ -33,7 +34,6 @@ module RablRails
#
class Context
attr_reader :format
attr_accessor :target_object
def initialize(path, options)
@virtual_path = path
@ -56,17 +56,17 @@ module RablRails
@lookup_context ||= LookupContext.new(@options[:view_path], format)
end
end
#
# Renders object with the given rabl template.
#
#
# Object can also be passed as an option :
# { locals: { object: obj_to_render } }
#
# Default render format is JSON, but can be changed via
# an option: { format: 'xml' }
#
# If template includes uses of instance variables (usually
# If template includes uses of instance variables (usually
# defined in the controller), you can passed them as locals
# options.
# For example, if you have this template:
@ -80,12 +80,11 @@ module RablRails
object = options[:locals].delete(:object) if !object && options[:locals]
c = Context.new(template, options)
c.target_object = object
t = c.lookup_context.find_template(template, [], false)
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

View File

@ -5,9 +5,10 @@ module RablRails
class Base
attr_accessor :_options
def initialize(context) # :nodoc:
def initialize(context, locals = nil) # :nodoc:
@_context = context
@_options = {}
@_resource = locals[:resource] if locals
setup_render_context
end
@ -18,8 +19,14 @@ module RablRails
# method defined by the renderer.
#
def render(template)
collection_or_resource = instance_variable_get(template.data) if template.data
collection_or_resource = @_context.target_object unless collection_or_resource || template.data == false || !@_context.respond_to?(:target_object)
collection_or_resource = if template.data
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) :
render_resource(collection_or_resource, template.source)
_options[:root_name] = template.root_name
@ -55,7 +62,13 @@ module RablRails
when Hash
current_value = value.dup
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
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
VERSION = '0.1.2'
VERSION = '0.2.1'
end

View File

@ -24,6 +24,16 @@ class CompilerTest < ActiveSupport::TestCase
assert_equal({}, t.source)
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
t = @compiler.compile_source(%{ collection :@user })
assert_equal :@user, t.data

View File

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

View File

@ -6,7 +6,6 @@ class RenderTest < ActiveSupport::TestCase
setup do
@user = User.new(1, 'Marty')
@user.stub(:respond_to?).with(:each).and_return(false)
@tmp_path = Pathname.new(Dir.mktmpdir)
end
@ -29,16 +28,16 @@ class RenderTest < ActiveSupport::TestCase
end
assert_equal %q({"user":{"id":1,"name":"Marty"}}), RablRails.render(@user, 'show', view_path: @tmp_path)
end
test "raise error if template is not found" do
assert_raises(RablRails::Renderer::TemplateNotFound) { RablRails.render(@user, 'not_found') }
end
test "instance variables can be passed via options[:locals]" do
File.open(@tmp_path + "instance.json.rabl", "w") do |f|
f.puts %q{
object false
node(:username) { |_| @user.name }
node(:username) { |_| @user.name }
}
end
assert_equal %q({"username":"Marty"}), RablRails.render(nil, 'instance', view_path: @tmp_path, locals: { user: @user })
@ -51,7 +50,7 @@ class RenderTest < ActiveSupport::TestCase
extends 'base'
}
end
File.open(@tmp_path + "base.json.rabl", "w") do |f|
f.puts %q{
attribute :name, as: :extended_name

View File

@ -4,7 +4,6 @@ class TestJsonRenderer < ActiveSupport::TestCase
setup do
@data = User.new(1, 'foobar', 'male')
@data.stub(:respond_to?).with(:each).and_return(false)
@context = Context.new
@context.assigns['data'] = @data
@ -28,6 +27,13 @@ class TestJsonRenderer < ActiveSupport::TestCase
assert_equal %q([{}]), render_json_output
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
@template.source = { :id => :id, :name => :name }
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
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
@template.source = { :_glue0 => { :_data => :@data, :name => :name } }
assert_equal %q({"name":"foobar"}), render_json_output
@ -77,6 +89,7 @@ class TestJsonRenderer < ActiveSupport::TestCase
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 }
@ -86,7 +99,6 @@ class TestJsonRenderer < ActiveSupport::TestCase
test "partial should be evaluated at rendering time" do
# Set assigns
@data.stub(:respond_to?).with(:empty?).and_return(false)
@context.assigns['user'] = @data
# 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

@ -32,12 +32,12 @@ end
class Context
attr_writer :virtual_path
def initialize
@_assigns = {}
@virtual_path = nil
end
def assigns
@_assigns
end
@ -47,4 +47,11 @@ class Context
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