Merge branch 'master' into xml-renderer

This commit is contained in:
ccocchi 2012-09-17 13:48:36 +02:00
commit 9c9a953e22
23 changed files with 539 additions and 160 deletions

17
.gitignore vendored
View File

@ -1,7 +1,10 @@
.bundle/ ## General
log/*.log log
pkg/ doc
test/dummy/db/*.sqlite3 rdoc
test/dummy/log/*.log
test/dummy/tmp/ ## Bundler
test/dummy/.sass-cache .bundle
pkg
Gemfile.lock

24
CHANGELOG.md Normal file
View File

@ -0,0 +1,24 @@
# CHANGELOG
## 0.2.0 (unrelased)
* 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 correcly 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
* Warning message printed on logger when JSON engine fail to load
## 0.1.1
* Add CHANGELOG
* Remove unnused test in loop
* Speed up rendering by not double copying variable from context
* Rename private variable to avoid name conflict
* Remove sqlite3 development dependency

17
Gemfile
View File

@ -1,18 +1,9 @@
source "http://rubygems.org" source "http://rubygems.org"
# Declare your gem's dependencies in rabl-rails.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
# development dependencies will be added by default to the :development group.
gemspec gemspec
# jquery-rails is used by the dummy application gem 'yajl-ruby'
gem 'rspec-mocks' group :test do
gem 'rspec-mocks'
# Declare any dependencies that are still in development here instead of in end
# your gemspec. These might include edge Rails or gems from your path or
# Git. Remember to move these dependencies to your gemspec before releasing
# your gem to rubygems.org.
# To use debugger
# gem 'ruby-debug19', :require => 'ruby-debug'

View File

@ -1,67 +0,0 @@
PATH
remote: .
specs:
rabl-rails (0.1.0)
activesupport (~> 3.2.1)
GEM
remote: http://rubygems.org/
specs:
actionpack (3.2.1)
activemodel (= 3.2.1)
activesupport (= 3.2.1)
builder (~> 3.0.0)
erubis (~> 2.7.0)
journey (~> 1.0.1)
rack (~> 1.4.0)
rack-cache (~> 1.1)
rack-test (~> 0.6.1)
sprockets (~> 2.1.2)
activemodel (3.2.1)
activesupport (= 3.2.1)
builder (~> 3.0.0)
activesupport (3.2.1)
i18n (~> 0.6)
multi_json (~> 1.0)
builder (3.0.0)
erubis (2.7.0)
hike (1.2.1)
i18n (0.6.0)
journey (1.0.3)
json (1.6.5)
multi_json (1.1.0)
rack (1.4.1)
rack-cache (1.1)
rack (>= 0.4)
rack-ssl (1.3.2)
rack
rack-test (0.6.1)
rack (>= 1.0)
railties (3.2.1)
actionpack (= 3.2.1)
activesupport (= 3.2.1)
rack-ssl (~> 1.3.2)
rake (>= 0.8.7)
rdoc (~> 3.4)
thor (~> 0.14.6)
rake (0.9.2.2)
rdoc (3.12)
json (~> 1.4)
rspec-mocks (2.8.0)
sprockets (2.1.2)
hike (~> 1.2)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sqlite3 (1.3.5)
thor (0.14.6)
tilt (1.3.3)
PLATFORMS
ruby
DEPENDENCIES
actionpack (~> 3.2.1)
rabl-rails!
railties (~> 3.2.1)
rspec-mocks
sqlite3

View File

@ -1,4 +1,4 @@
Copyright 2012 YOURNAME Copyright 2012 Christopher Cocchi-Perrier
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

214
README.md
View File

@ -4,7 +4,7 @@ RABL (Ruby API Builder Language) is a ruby templating system for rendering resou
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 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 !
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. 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.
## Installation ## Installation
@ -17,7 +17,7 @@ gem install rabl-rails
or add directly to your `Gemfile` or add directly to your `Gemfile`
``` ```
gem 'rabl' gem 'rabl-rails'
``` ```
And that's it ! And that's it !
@ -29,12 +29,12 @@ assuming you have a `Post` model filled with blog posts, and a `PostController`
```ruby ```ruby
class PostController < ApplicationController class PostController < ApplicationController
respond_to :html, :json, :xml respond_to :html, :json, :xml
def index def index
@posts = Post.order('created_at DESC') @posts = Post.order('created_at DESC')
respond_with(@posts) respond_with(@posts)
end end
end end
``` ```
@ -64,21 +64,209 @@ That's a basic overview but there is a lot more to see such as partials, inherit
As opposed to standard RABL gem, this gem separate compiling (a.k.a transforming a RABL-rails template into a Ruby hash) and the actual rendering of the object or collection. This allow to only compile the template once and only Ruby hashes. As opposed to standard RABL gem, this gem separate compiling (a.k.a transforming a RABL-rails template into a Ruby hash) and the actual rendering of the object or collection. This allow to only compile the template once and only Ruby hashes.
The fact of compiling the template outside of any rendering context prevent us to use any instances variables (with the exception of node) in the template because they are rendering objects. So instead, you'll have to use symbols of these variables. For example, to render the collection `@posts` inside your `PostController`, you need to use `:@posts` inside of the template. The fact of compiling the template outside of any rendering context prevent us to use any instances variables (with the exception of node) in the template because they are rendering objects. So instead, you'll have to use symbols of these variables.For example, to render the collection `@posts` inside your `PostController`, you need to use `:@posts` inside of the template.
The only places where you can actually used instance variables are into Proc (or lambda) or into custom node (because they are treated as Proc). The only places where you can actually used instance variables are into Proc (or lambda) or into custom node (because they are treated as Proc).
```ruby ```ruby
# We reference the @posts varibles that will be used at rendering time # We reference the @posts varibles that will be used at rendering time
collection :@posts collection :@posts
# Here you can use directly the instance variable because it # 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) } node(:read) { |post| post.read_by?(@user) }
``` ```
The same rule applies for view helpers such as `current_user` The same rule applies for view helpers such as `current_user`
After the template is compiled into a hash, Rabl-rails will use a renderer to do the actual output. Actually, only JSON and XML formats are supported. After the template is compiled into a hash, Rabl-rails will use a renderer to do the actual output. Actually, only JSON and XML formats are supported.
## 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:
```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
```
## Usage ## Usage
### Data declaration
To declare data to use in the template, you can use either `object` or `collection` with the symbol name or your data.
```ruby
# app/views/users/show.json.rabl
object :@user
# app/views/users/index.json.rabl
collection :@users
```
You can specify root label for the collection using hash or `:root` option
```ruby
collection :@posts, root: :articles
#is equivalent to
collection :@posts => :articles
# => { "articles" : [{...}, {...}] }
```
There are rares cases when the template doesn't map directly to any object. In these cases, you can set data to false or skip data declaration altogether.
```ruby
object false
node(:some_count) { |_| @user.posts.count }
child(:@user) { attribute :name }
```
### Attributes / Methods
Basic usage is to declared attributes to include in the response. These can be database attributes or any instance method.
```ruby
attributes :id, :title, :to_s
```
You can aliases these attributes in your response
```ruby
attributes title: :foo, to_s: :bar
# => { "foo" : <title value>, "bar" : <to_s value> }
```
### Child nodes
You can include nested information from data associated with the parent model. You can also alias these associations.
For example if you have a `Post` model that belongs to a `User`
```ruby
object :@post
child(user: :author) do
attributes :name
end
# => { "post" : { "author" : { "name" : "John D." } } }
```
You can also use arbitrary data source with child nodes
```ruby
child(:@users) do
attributes :id, :name
end
```
### Custom nodes
You can create custom node in your response, based on the result of the given block
```ruby
object :@user
node(:full_name) { |u| u.first_name + " " + u.last_name }
# => { "user" : { "full_name" : "John Doe" } }
```
You can add the node only if a condition is true
```ruby
node(:email, if: -> { |u| u.valid_email? }) do |u|
u.email
end
```
Nodes are evaluated at the rendering time, so you can use any instance variables or view helpers inside them
```ruby
node(:url) { |post| post_url(post) }
```
Custom nodes are really usefull to create flexible representations of your resources.
### Extends & Partials
Often objects have a basic representation that is shared accross different views and enriched according to it. To avoid code redundancy you can extend your template from any other RABL template.
```ruby
# app/views/users/base.json.rabl
attributes :id, :name
# app/views/users/private.json.rabl
extends 'users/base'
attributes :super_secret_attribute
```
You can also extends template in child nodes using `partial` option (this is the same as using `extends` in the child block)
```ruby
collection @posts
attribute :title
child(:user, partial: 'users/base')
```
Partials can also be used inside custom nodes. When using partial this way, you MUST declare the object associated to the partial
```ruby
node(:location) do |user|
{ city: user.city, address: partial('users/address', object: m.address) }
end
```
### Nesting
Rabl allow you to define easily your templates, even with hierarchy of 2 or 3 levels. Let's suppose your have a `thread` model that has many `posts` and that each post has many `comments`. We can display a full thread in a few lines
```ruby
object :@thread
attribute :caption
child :posts do
attribute :title
child :comments do
extends 'comments/base'
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
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:
```ruby
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)
## 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**.
You can see full tests on test application repository.
## Authors and contributors
* [Christopher Cocchi-Perrier](http://github.com/ccocchi) - Creator of the project
Want to add another format to Rabl-rails ? Checkout [JSON renderer](http://github.com/ccocchi/rabl-rails/blob/master/lib/rabl-rails/renderers/json.rb) for reference
Want to make another change ? Just fork and contribute, any help is very much appreciated
## Original idea
* [RABL](http://github.com/nesquena/rabl) Standart RABL gem. I used it a lot before deciding I wanted faster views
## Copyright
Copyright © 2011-2012 Christopher Cocchi-Perrier. See [MIT-LICENSE](http://github.com/ccocchi/rabl-rails/blob/master/MIT-LICENSE) for details.

View File

@ -1,7 +1,6 @@
require 'rails/railtie' require 'rails/railtie'
require 'active_support' require 'active_support'
require 'active_support/json'
require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/attribute_accessors'
require 'rabl-rails/version' require 'rabl-rails/version'
@ -14,10 +13,12 @@ require 'rabl-rails/library'
require 'rabl-rails/handler' require 'rabl-rails/handler'
require 'rabl-rails/railtie' require 'rabl-rails/railtie'
require 'multi_json'
module RablRails module RablRails
extend self extend Renderer
autoload :Responder, 'rabl-rails/responder'
mattr_accessor :cache_templates mattr_accessor :cache_templates
@@cache_templates = true @@cache_templates = true
@ -25,11 +26,33 @@ module RablRails
mattr_accessor :include_json_root mattr_accessor :include_json_root
@@include_json_root = true @@include_json_root = true
def configure 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 yield self
ActionController::Base.responder = Responder if self.use_custom_responder
end end
def cache_templates? 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)
end
def self.cache_templates?
ActionController::Base.perform_caching && @@cache_templates ActionController::Base.perform_caching && @@cache_templates
end end
def self.load_default_engines!
self.json_engine = :yajl
end
end end

View File

@ -26,7 +26,7 @@ module RablRails
# #
def object(data, options = {}) def object(data, options = {})
@template.data, @template.root_name = extract_data_and_name(data) @template.data, @template.root_name = extract_data_and_name(data)
@template.root_name = options[:root] if options[:root] @template.root_name = options[:root] if options.has_key? :root
end end
alias_method :collection, :object alias_method :collection, :object
@ -60,7 +60,7 @@ module RablRails
# #
def child(name_or_data, options = {}) def child(name_or_data, options = {})
data, name = extract_data_and_name(name_or_data) data, name = extract_data_and_name(name_or_data)
name = options[:root] if options[:root] name = options[:root] if options.has_key? :root
if options[:partial] if options[:partial]
template = Library.instance.compile_template_from_path(options[:partial]) template = Library.instance.compile_template_from_path(options[:partial])
@template[name] = template.merge!(:_data => data) @template[name] = template.merge!(:_data => data)

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,6 +1,8 @@
module RablRails module RablRails
class Railtie < Rails::Railtie class Railtie < Rails::Railtie
initializer "rabl.initialize" do |app| initializer "rabl.initialize" do |app|
RablRails.load_default_engines!
ActiveSupport.on_load(:action_view) do ActiveSupport.on_load(:action_view) do
ActionView::Template.register_template_handler :rabl, RablRails::Handlers::Rabl ActionView::Template.register_template_handler :rabl, RablRails::Handlers::Rabl
end end

View File

@ -1,2 +1,89 @@
require 'rabl-rails/renderers/base' require 'rabl-rails/renderers/base'
require 'rabl-rails/renderers/json' require 'rabl-rails/renderers/json'
module RablRails
module Renderer
class TemplateNotFound < StandardError; end
mattr_reader :view_path
@@view_path = 'app/views'
class LookupContext
T = Struct.new(:source)
def initialize(view_path, format)
@view_path = view_path || RablRails::Renderer.view_path
@format = format
end
#
# Manually find given rabl template file with given format.
# View path can be set via options, otherwise default Rails
# path is used
#
def find_template(name, opt, partial = false)
path = File.join(@view_path, "#{name}.#{@format}.rabl")
File.exists?(path) ? T.new(File.read(path)) : nil
end
end
#
# Context class to emulate normal Rails view
# context
#
class Context
attr_reader :format
def initialize(path, options)
@virtual_path = path
@format = options.delete(:format) || 'json'
@_assigns = {}
@options = options
options[:locals].each { |k, v| @_assigns[k.to_s] = v } if options[:locals]
end
def assigns
@_assigns
end
def params
{ format: format }
end
def lookup_context
@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
# defined in the controller), you can passed them as locals
# options.
# For example, if you have this template:
# object :@user
# node(:read) { |u| u.has_read?(@post) }
#
# Your method call should look like this:
# RablRails.render(user, 'users/show', locals: { post: Post.new })
#
def render(object, template, options = {})
object = options[:locals].delete(:object) if !object && options[:locals]
c = Context.new(template, options)
t = c.lookup_context.find_template(template, [], false)
raise TemplateNotFound unless t
Library.instance.get_rendered_template(t.source, c, resource: object)
end
end
end

View File

@ -3,11 +3,12 @@ module RablRails
class PartialError < StandardError; end class PartialError < StandardError; end
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,10 +19,17 @@ module RablRails
# method defined by the renderer. # method defined by the renderer.
# #
def render(template) def render(template)
collection_or_resource = @_context.instance_variable_get(template.data) if template.data 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) : 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
format_output(output_hash) format_output(output_hash)
end end
@ -54,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?('@') ? @_context.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|
@ -108,7 +122,7 @@ module RablRails
# #
def setup_render_context def setup_render_context
@_context.instance_variable_get(:@_assigns).each_pair { |k, v| @_context.instance_variable_get(:@_assigns).each_pair { |k, v|
instance_variable_set("@#{k}", v) unless k.start_with?('_') || k == @data instance_variable_set("@#{k}", v) unless k.start_with?('_')
} }
end end
end end

View File

@ -2,8 +2,8 @@ module RablRails
module Renderers module Renderers
class JSON < Base class JSON < Base
def format_output(hash) def format_output(hash)
hash = { options[:root_name] => hash } if options[:root_name] && RablRails.include_json_root hash = { _options[:root_name] => hash } if _options[:root_name] && RablRails.include_json_root
ActiveSupport::JSON.encode(hash) MultiJson.encode(hash)
end end
end end
end end

View File

@ -0,0 +1,27 @@
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
@api_template = @controller.respond_to?(:responder_default_template, true) ? controller.send(:responder_default_template) : nil
end
protected
def api_behavior(error)
rabl_options = options.merge(template: @api_template || RablRails.responder_default_template)
if get?
controller.default_render rabl_options
elsif post?
controller.default_render rabl_options.merge!(status: :created, location: api_location, locals: { resource: resource })
else
head :no_content
end
end
end
end

View File

@ -1,3 +1,3 @@
module RablRails module RablRails
VERSION = '0.1.0' VERSION = '0.1.3'
end end

View File

@ -1,24 +1,22 @@
$:.push File.expand_path("../lib", __FILE__) $:.push File.expand_path("../lib", __FILE__)
# Maintain your gem's version:
require "rabl-rails/version" require "rabl-rails/version"
# Describe your gem and declare its dependencies:
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = "rabl-rails" s.name = "rabl-rails"
s.version = RablRails::VERSION s.version = RablRails::VERSION
s.authors = ["TODO: Your name"] s.platform = Gem::Platform::RUBY
s.email = ["TODO: Your email"] s.authors = ["Christopher Cocchi-Perrier"]
s.homepage = "TODO" s.email = ["cocchi.c@gmail.com"]
s.summary = "TODO: Summary of RablRails." s.homepage = "https://github.com/ccocchi/rabl-rails"
s.description = "TODO: Description of RablRails." s.summary = "Fast Rails 3+ templating system with JSON and XML support"
s.description = "Fast Rails 3+ templating system with JSON and XML support"
s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.md"] s.files = `git ls-files`.split("\n")
s.test_files = Dir["test/**/*"] s.test_files = `git ls-files -- test/*`.split("\n")
s.require_paths = ["lib"]
s.add_dependency "activesupport", "~> 3.2.1" s.add_dependency "activesupport", "~> 3.0"
s.add_dependency "railties", "~> 3.0"
s.add_development_dependency "sqlite3" s.add_development_dependency "actionpack", "~> 3.0"
s.add_development_dependency "railties", "~> 3.2.1"
s.add_development_dependency "actionpack", "~> 3.2.1"
end end

View File

@ -43,6 +43,11 @@ class CompilerTest < ActiveSupport::TestCase
assert_equal :users, t.root_name assert_equal :users, t.root_name
end end
test "root can be set to false via options" do
t = @compiler.compile_source(%( object :@user, root: false))
assert_equal false, t.root_name
end
# Compilation # Compilation
test "simple attributes are compiled to hash" do test "simple attributes are compiled to hash" do

View File

@ -19,12 +19,10 @@ 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.stub(:instance_variable_get).with(:@user).and_return(@user) @context.assigns['user'] = @user
@context.stub(:instance_variable_get).with(:@virtual_path).and_return('users/show') @context.virtual_path = 'users/show'
@context.stub(:instance_variable_get).with(:@_assigns).and_return({})
@context.stub(:lookup_context).and_return(mock(:find_template => mock(:source => %{ object :@comment\n attribute :content }))) @context.stub(:lookup_context).and_return(mock(:find_template => mock(:source => %{ object :@comment\n attribute :content })))
end end
@ -40,7 +38,7 @@ class DeepNestingTest < ActiveSupport::TestCase
end end
} }
assert_equal(ActiveSupport::JSON.encode(:user => { assert_equal(MultiJson.encode(:user => {
:id => 1, :id => 1,
:name => 'foobar', :name => 'foobar',
:posts => [{ :posts => [{

View File

@ -24,7 +24,7 @@ class NonRestfulResponseTest < ActiveSupport::TestCase
end end
} }
assert_equal(ActiveSupport::JSON.encode({ assert_equal(MultiJson.encode({
:post_count => 10, :post_count => 10,
:user => { :user => {
:id => 1, :id => 1,

63
test/render_test.rb Normal file
View File

@ -0,0 +1,63 @@
require 'test_helper'
require 'pathname'
require 'tmpdir'
class RenderTest < ActiveSupport::TestCase
setup do
@user = User.new(1, 'Marty')
@tmp_path = Pathname.new(Dir.mktmpdir)
end
test "allow object to be passed as an option" do
File.open(@tmp_path + "nil.json.rabl", "w") do |f|
f.puts %q{
object :@user
attributes :name
}
end
assert_equal %q({"user":{"name":"Marty"}}), RablRails.render(nil, 'nil', locals: { object: @user }, view_path: @tmp_path)
end
test "load source from file" do
File.open(@tmp_path + "show.json.rabl", "w") do |f|
f.puts %q{
object :@user
attributes :id, :name
}
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 }
}
end
assert_equal %q({"username":"Marty"}), RablRails.render(nil, 'instance', view_path: @tmp_path, locals: { user: @user })
end
test "handle path for extends" do
File.open(@tmp_path + "extend.json.rabl", "w") do |f|
f.puts %q{
object :@user
extends 'base'
}
end
File.open(@tmp_path + "base.json.rabl", "w") do |f|
f.puts %q{
attribute :name, as: :extended_name
}
end
assert_equal %q({"user":{"extended_name":"Marty"}}), RablRails.render(@user, 'extend', view_path: @tmp_path)
end
end

View File

@ -4,11 +4,9 @@ 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.stub(:instance_variable_get).with(:@data).and_return(@data) @context.assigns['data'] = @data
@context.stub(:instance_variable_get).with(:@_assigns).and_return({})
@template = RablRails::CompiledTemplate.new @template = RablRails::CompiledTemplate.new
@template.data = :@data @template.data = :@data
@ -24,11 +22,18 @@ class TestJsonRenderer < ActiveSupport::TestCase
end end
test "render collection with empty template" do test "render collection with empty template" do
@context.stub(:instance_variable_get).with(:@data).and_return([@data]) @context.assigns['data'] = [@data]
@template.source = {} @template.source = {}
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
@ -45,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
@ -52,7 +63,7 @@ class TestJsonRenderer < ActiveSupport::TestCase
test "render collection with attributes" do test "render collection with attributes" do
@data = [User.new(1, 'foo', 'male'), User.new(2, 'bar', 'female')] @data = [User.new(1, 'foo', 'male'), User.new(2, 'bar', 'female')]
@context.stub(:instance_variable_get).with(:@data).and_return(@data) @context.assigns['data'] = @data
@template.source = { :uid => :id, :name => :name, :gender => :sex } @template.source = { :uid => :id, :name => :name, :gender => :sex }
assert_equal %q([{"uid":1,"name":"foo","gender":"male"},{"uid":2,"name":"bar","gender":"female"}]), render_json_output assert_equal %q([{"uid":1,"name":"foo","gender":"male"},{"uid":2,"name":"bar","gender":"female"}]), render_json_output
end end
@ -78,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 }
@ -87,8 +99,7 @@ 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
@context.stub(:instance_variable_get).with(:@_assigns).and_return({'user' => @data}) @context.assigns['user'] = @data
@data.stub(:respond_to?).with(:empty?).and_return(false)
# Stub Library#get # Stub Library#get
t = RablRails::CompiledTemplate.new t = RablRails::CompiledTemplate.new

View File

@ -31,10 +31,15 @@ module ActiveSupport
end end
class Context class Context
attr_accessor :virtual_path attr_writer :virtual_path
def initialize def initialize
@_assigns = {} @_assigns = {}
@virtual_path = nil
end
def assigns
@_assigns
end end
def params def params
@ -42,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