Compare commits

..

3 Commits

Author SHA1 Message Date
ccocchi
4dae04896d First implementation of fragment cache 2012-05-28 10:56:53 +02:00
ccocchi
4cf10fb8f6 Merge branch 'master' into fragment-cache 2012-05-04 16:05:11 +02:00
ccocchi
79d6bb9c81 Basic fragment class and renderer code. Need to be tested 2012-04-16 00:38:02 +02:00
34 changed files with 271 additions and 1115 deletions

17
.gitignore vendored
View File

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

View File

@ -1,5 +0,0 @@
language: ruby
rvm:
- 1.9.3
- jruby-19mode
- rbx-19mode

View File

@ -1,45 +0,0 @@
# CHANGELOG
## 0.3.1
* Add full template stack support to `glue` (fnordfish)
* Allow format to be a symbol (lloydmeta)
## 0.3.0
* Travis integration
* Add test for keywords used as variable names
* Add PList renderer
* Remove location header from post responses in responder
* Fix bug with incomplete template prefixing
## 0.2.2
* Add condition blocks
## 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
* Warning message printed on logger when JSON engine fail to load
## 0.1.1
* Add CHANGELOG
* 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

22
Gemfile
View File

@ -1,18 +1,18 @@
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
gem 'plist' # jquery-rails is used by the dummy application
platforms :ruby do gem 'rspec-mocks'
gem 'oj'
gem 'libxml-ruby'
end
platforms :jruby do # Declare any dependencies that are still in development here instead of in
gem 'nokogiri' # your gemspec. These might include edge Rails or gems from your path or
end # Git. Remember to move these dependencies to your gemspec before releasing
# your gem to rubygems.org.
group :test do # To use debugger
gem 'rspec-mocks' # gem 'ruby-debug19', :require => 'ruby-debug'
end

67
Gemfile.lock Normal file
View File

@ -0,0 +1,67 @@
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 Christopher Cocchi-Perrier Copyright 2012 YOURNAME
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

244
README.md
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 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. 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 targets **Rails 3+ application** and is compatible with mri 1.9.3, jRuby and rubinius. 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,24 +17,24 @@ gem install rabl-rails
or add directly to your `Gemfile` or add directly to your `Gemfile`
``` ```
gem 'rabl-rails' gem 'rabl'
``` ```
And that's it ! And that's it !
## Overview ## Overview
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, Once you have installed RABL, you can directly used RABL 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
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,229 +64,21 @@ 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 ## Usage
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 = :oj
# config.xml_engine = 'LibXML'
# config.use_custom_responder = false
# config.default_responder_template = 'show'
end
```
## 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 }
```
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.
```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
```
### 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)
### 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.7.6 and rabl-rails 0.3.0
Overall, Rabl-rails is **20% faster and use 10% less memory**, even **twice faster** when using extends.
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. If you found a bug, you can report it via the Github issues.
## Original idea
* [RABL](http://github.com/nesquena/rabl) Standart RABL gem. I used it a lot but I needed to improve my API response time, and
since most of the time was spent in view rendering, I decided to implement a faster rabl gem.
## 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,11 +1,12 @@
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'
require 'rabl-rails/template' require 'rabl-rails/template'
require 'rabl-rails/condition' require 'rabl-rails/fragment'
require 'rabl-rails/compiler' require 'rabl-rails/compiler'
require 'rabl-rails/renderer' require 'rabl-rails/renderer'
@ -14,74 +15,19 @@ 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 Renderer extend self
autoload :Responder, 'rabl-rails/responder'
mattr_accessor :cache_templates mattr_accessor :cache_templates
@@cache_templates = true @@cache_templates = true
mattr_accessor :include_json_root def configure
@@include_json_root = true
mattr_accessor :use_custom_responder
@@use_custom_responder = false
mattr_accessor :responder_default_template
@@responder_default_template = 'show'
mattr_reader :plist_engine
@@plist_engine = nil
mattr_accessor :include_plist_root
@@include_plist_root = nil
def self.configure
yield self yield self
ActionController::Base.responder = Responder if self.use_custom_responder
end end
def self.json_engine=(name) def cache_templates?
MultiJson.engine = name
rescue LoadError
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.plist_engine=(name)
raise "Your plist engine does not respond to #dump" unless name.respond_to?(:dump)
@@plist_engine = name
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 = MultiJson.default_engine
self.plist_engine = Plist::Emit if defined?(Plist)
if defined?(LibXML)
self.xml_engine = 'LibXML'
elsif defined?(Nokogiri)
self.xml_engine = 'Nokogiri'
end
end
end end

View File

@ -5,7 +5,8 @@ module RablRails
# #
class Compiler class Compiler
def initialize def initialize
@i = 0 @glue_count = 0
@cache_count = 0
end end
# #
@ -25,13 +26,20 @@ module RablRails
# object :@user, :root => :author # object :@user, :root => :author
# #
def object(data, options = {}) def object(data, options = {})
@template.data, @template.root_name = extract_data_and_name(data) data, name = extract_data_and_name(data)
@template.root_name = options[:root] if options.has_key? :root @template.data = data
@template.root_name = options[:root] || name
end end
alias_method :collection, :object
def root(name) #
@template.root_name = name # Sets a collection to be used as data for the template
# Example:
# collection :@users
# collection :@users, :root => :morons
#
def collection(data, options = {})
object(data)
@template.root_name = options[:root] if options[:root]
end end
# #
@ -64,10 +72,10 @@ 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.has_key? :root name = options[:root] if options[:root]
if options[:partial] if options[:partial]
template = Library.instance.compile_template_from_path(options[:partial]) template = Library.instance.get(options[:partial])
@template[name] = template.merge(:_data => data) @template[name] = template.merge!(:_data => data)
elsif block_given? elsif block_given?
@template[name] = sub_compile(data) { yield } @template[name] = sub_compile(data) { yield }
end end
@ -80,8 +88,8 @@ module RablRails
# #
def glue(data) def glue(data)
return unless block_given? return unless block_given?
name = :"_glue#{@i}" name = :"_glue#{@glue_count}"
@i += 1 @glue_count += 1
@template[name] = sub_compile(data) { yield } @template[name] = sub_compile(data) { yield }
end end
@ -96,7 +104,7 @@ module RablRails
def node(name, options = {}, &block) def node(name, options = {}, &block)
condition = options[:if] condition = options[:if]
if condition if condition.present?
if condition.is_a?(Proc) if condition.is_a?(Proc)
@template[name] = [condition, block] @template[name] = [condition, block]
else else
@ -114,22 +122,19 @@ module RablRails
# extends 'users/base' # extends 'users/base'
# #
def extends(path) def extends(path)
t = Library.instance.compile_template_from_path(path) t = Library.instance.get(path)
@template.merge!(t.source) @template.merge!(t.source)
end end
# #
# Provide a conditionnal block # cache key: ->(resource) { |resource| resource.cache_key }, expires_in: 1800
# #
# condition(->(u) { u.is_a?(Admin) }) do def cache(options = {}, &block)
# attributes :secret
# end
#
def condition(proc)
return unless block_given? return unless block_given?
name = :"_if#{@i}" key = options.delete(:key)
@i += 1 source = sub_compile { yield }
@template[name] = Condition.new(proc, sub_compile(nil) { yield }) @template[:"_cache#{@cache_count}"] = Fragment.new(key, source, options)
@cache_count += 1
end end
protected protected
@ -143,16 +148,19 @@ module RablRails
def extract_data_and_name(name_or_data) def extract_data_and_name(name_or_data)
case name_or_data case name_or_data
when Symbol when Symbol
str = name_or_data.to_s if name_or_data.to_s.start_with?('@')
str.start_with?('@') ? [name_or_data, str[1..-1]] : [name_or_data, name_or_data] [name_or_data, nil]
else
[name_or_data, name_or_data]
end
when Hash when Hash
name_or_data.first name_or_data.first
else else
name_or_data name_or_data
end end
end end
def sub_compile(data) def sub_compile(data = nil)
return {} unless block_given? return {} unless block_given?
old_template, @template = @template, {} old_template, @template = @template, {}
yield yield
@ -161,4 +169,4 @@ module RablRails
@template = old_template @template = old_template
end end
end end
end end

View File

@ -1,10 +0,0 @@
module RablRails
class Condition
attr_reader :proc, :source
def initialize(proc, source)
@proc = proc
@source = source
end
end
end

View File

@ -0,0 +1,42 @@
require 'active_support/cache'
module RablRails
class Fragment
attr_reader :compiled_source, :options
def initialize(key, source, options = {})
@key = key
@compiled_source = source
@options = options
end
def expand_cache_key(data)
if @key
@key.respond_to?(:call) ? @key.call(data) : @key
else
ActiveSupport::Cache.expand_cache_key(data, 'rabl')
end
end
end
end
#
# cache key: ->(resource) { |resource| resource.cache_key }, expires_in: 1800
#
# def cache(options = {}, &block)
# return unless block_given?
# key = options.delete(:key)
# source = sub_compiler { compile_block(&block) }
# @template[:"_cache#{cache_count}"] = Fragment.new(key, source, options)
# end
#
# when Fragment
# render_fragment(value)
#
# def render_fragment(data, fragment)
# Rails.cache.fetch(fragment.expand_cache_key(data), fragment.options) do
# render_resource(data, fragment.compiled_source)
# end
# end

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, local_assigns) get_rendered_template(#{template.source.inspect}, self)
} }
end end
end end

View File

@ -8,29 +8,30 @@ module RablRails
@cached_templates = {} @cached_templates = {}
end end
def get_rendered_template(source, context, locals = nil) def get_rendered_template(source, context)
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 = get_compiled_template(path, source)
format = context.params[:format] || 'json' format = context.params[:format] || 'json'
Renderers.const_get(format.to_s.upcase!).new(context, locals).render(compiled_template) Renderers.const_get(format.upcase!).new(context).render(compiled_template)
end end
def compile_template_from_source(source, path = nil) def get_compiled_template(path, source)
if path && RablRails.cache_templates? if path && RablRails.cache_templates?
@cached_templates[path] ||= Compiler.new.compile_source(source) @cached_templates[path] ||= Compiler.new.compile_source(source)
@cached_templates[path].dup
else else
Compiler.new.compile_source(source) Compiler.new.compile_source(source)
end end
end end
def compile_template_from_path(path) def get(path)
template = @cached_templates[path] template = @cached_templates[path]
return template if template return template if template
t = @lookup_context.find_template(path, [], false) t = @lookup_context.find_template(path, [], false)
compile_template_from_source(t.source, path) get_compiled_template(path, t.source)
end end
end end
end end

View File

@ -1,8 +1,6 @@
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,91 +1,2 @@
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'
require 'rabl-rails/renderers/plist'
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,12 +3,9 @@ module RablRails
class PartialError < StandardError; end class PartialError < StandardError; end
class Base class Base
attr_accessor :_options
def initialize(context, locals = nil) # :nodoc: def initialize(context) # :nodoc:
@_context = context @_context = context
@_options = {}
@_resource = locals[:resource] if locals
setup_render_context setup_render_context
end end
@ -19,17 +16,10 @@ module RablRails
# method defined by the renderer. # method defined by the renderer.
# #
def render(template) def render(template)
collection_or_resource = if template.data collection_or_resource = @_context.instance_variable_get(template.data) 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 output_hash = { template.root_name => output_hash } if template.root_name
format_output(output_hash) format_output(output_hash)
end end
@ -48,7 +38,8 @@ module RablRails
# template source passed. # template source passed.
# #
def render_resource(data, source) def render_resource(data, source)
source.inject({}) { |output, (key, value)| source.inject({}) { |output, current|
key, value = current
out = case value out = case value
when Symbol when Symbol
@ -61,24 +52,21 @@ 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 = if data_symbol == nil object = data_symbol.nil? ? data : data_symbol.to_s.start_with?('@') ? @_context.instance_variable_get(data_symbol) : data.send(data_symbol)
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
output.merge!(render_resource(object, current_value)) current_value.each_pair { |k, v|
output[k] = object.send(v)
}
next output next output
else # child else # child
object.respond_to?(:each) ? render_collection(object, current_value) : render_resource(object, current_value) object.respond_to?(:each) ? render_collection(object, current_value) : render_resource(object, current_value)
end end
when Condition when Fragment
if instance_exec data, &(value.proc) cache_output = Rails.cache.fetch(value.expand_cache_key(data), value.options) do
output.merge!(render_resource(data, value.source)) data.respond_to?(:each) ? render_collection(data, value.compiled_source) : render_resource(data, value.compiled_source)
end end
output.merge!(cache_output)
next output next output
end end
output[key] = out output[key] = out
@ -104,7 +92,7 @@ module RablRails
return [] if object.respond_to?(:empty?) && object.empty? return [] if object.respond_to?(:empty?) && object.empty?
template = Library.instance.compile_template_from_path(template_path) template = Library.instance.get(template_path)
object.respond_to?(:each) ? render_collection(object, template.source) : render_resource(object, template.source) object.respond_to?(:each) ? render_collection(object, template.source) : render_resource(object, template.source)
end end
@ -124,9 +112,9 @@ 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?('_') instance_variable_set("@#{k}", v) unless k.start_with?('_') || k == @data
} }
end end
end end
end end
end end

View File

@ -2,8 +2,7 @@ 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 ActiveSupport::JSON.encode(hash)
MultiJson.encode(hash)
end end
end end
end end

View File

@ -1,11 +0,0 @@
module RablRails
module Renderers
class PLIST < Base
def format_output(hash)
hash = { _options[:root_name] => hash } if _options[:root_name] && RablRails.include_plist_root
RablRails.plist_engine.dump(hash)
end
end
end
end

View File

@ -1,14 +0,0 @@
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

@ -1,47 +0,0 @@
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[:prefixes] = controller._prefixes
options[:template] ||= template
controller.default_render options.merge(status: :created)
else
head :no_content
end
rescue ActionView::MissingTemplate => e
super(e)
end
end
end

View File

@ -2,10 +2,10 @@ module RablRails
class CompiledTemplate class CompiledTemplate
attr_accessor :source, :data, :root_name attr_accessor :source, :data, :root_name
delegate :[], :[]=, :merge!, :merge, :to => :source delegate :[], :[]=, :merge!, :to => :source
def initialize def initialize
@source = {} @source = {}
end end
end end
end end

View File

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

View File

@ -1,22 +1,24 @@
$:.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.platform = Gem::Platform::RUBY s.authors = ["TODO: Your name"]
s.authors = ["Christopher Cocchi-Perrier"] s.email = ["TODO: Your email"]
s.email = ["cocchi.c@gmail.com"] s.homepage = "TODO"
s.homepage = "https://github.com/ccocchi/rabl-rails" s.summary = "TODO: Summary of RablRails."
s.summary = "Fast Rails 3+ templating system with JSON and XML support" s.description = "TODO: Description of RablRails."
s.description = "Fast Rails 3+ templating system with JSON and XML support"
s.files = `git ls-files`.split("\n") s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.md"]
s.test_files = `git ls-files -- test/*`.split("\n") s.test_files = Dir["test/**/*"]
s.require_paths = ["lib"]
s.add_dependency "activesupport", "~> 3.0" s.add_dependency "activesupport", "~> 3.2.1"
s.add_dependency "railties", "~> 3.0"
s.add_development_dependency "actionpack", "~> 3.0" s.add_development_dependency "sqlite3"
s.add_development_dependency "railties", "~> 3.2.1"
s.add_development_dependency "actionpack", "~> 3.2.1"
end end

View File

@ -10,24 +10,24 @@ class CacheTemplatesTest < ActiveSupport::TestCase
test "cache templates if perform_caching is active and cache_templates is enabled" do test "cache templates if perform_caching is active and cache_templates is enabled" do
ActionController::Base.stub(:perform_caching).and_return(true) ActionController::Base.stub(:perform_caching).and_return(true)
@library.compile_template_from_source('', 'some/path') @library.get_compiled_template('some/path', "")
t = @library.compile_template_from_source("attribute :id", 'some/path') t = @library.get_compiled_template('some/path', "attribute :id")
assert_equal({}, t.source) assert_equal({}, t.source)
end end
test "cached templates should not be modifiable in place" do test "cached templates should not be modifiable in place" do
ActionController::Base.stub(:perform_caching).and_return(true) ActionController::Base.stub(:perform_caching).and_return(true)
@library.compile_template_from_source('', 'some/path') @library.get_compiled_template('some/path', "")
t = @library.compile_template_from_source("attribute :id", 'some/path') t = @library.get_compiled_template('some/path', "attribute :id")
assert_equal({}, t.source) assert_equal({}, t.source)
end end
test "don't cache templates cache_templates is enabled but perform_caching is not active" do test "don't cache templates cache_templates is enabled but perform_caching is not active" do
ActionController::Base.stub(:perform_caching).and_return(false) ActionController::Base.stub(:perform_caching).and_return(false)
@library.compile_template_from_source('', 'some/path') @library.get_compiled_template('some/path', "")
t = @library.compile_template_from_source("attribute :id", 'some/path') t = @library.get_compiled_template('some/path', "attribute :id")
assert_equal({ :id => :id }, t.source) assert_equal({ :id => :id }, t.source)
end end

View File

@ -24,16 +24,6 @@ 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
@ -53,11 +43,6 @@ 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
@ -109,11 +94,10 @@ class CompilerTest < ActiveSupport::TestCase
mock_template = RablRails::CompiledTemplate.new mock_template = RablRails::CompiledTemplate.new
mock_template.source = { :id => :id } mock_template.source = { :id => :id }
RablRails::Library.reset_instance RablRails::Library.reset_instance
RablRails::Library.instance.stub(:compile_template_from_path).with('users/base').and_return(mock_template) RablRails::Library.instance.stub(:get).with('users/base').and_return(mock_template)
t = @compiler.compile_source(%{child(:user, :partial => 'users/base') }) t = @compiler.compile_source(%{child(:user, :partial => 'users/base') })
assert_equal( {:user => { :_data => :user, :id => :id } }, t.source) assert_equal( {:user => { :_data => :user, :id => :id } }, t.source)
assert_equal( {:id => :id}, mock_template.source)
end end
test "glue is compiled as a child but with anonymous name" do test "glue is compiled as a child but with anonymous name" do
@ -133,22 +117,10 @@ class CompilerTest < ActiveSupport::TestCase
}, t.source) }, t.source)
end end
test "glue accepts all dsl in its body" do
t = @compiler.compile_source(%{
glue :@user do node(:foo) { |u| u.name } end
})
assert_not_nil(t.source[:_glue0])
s = t.source[:_glue0]
assert_equal(:@user, s[:_data])
assert_instance_of(Proc, s[:foo])
end
test "extends use other template source as itself" do test "extends use other template source as itself" do
template = mock('template', :source => { :id => :id }) template = mock('template', :source => { :id => :id })
RablRails::Library.reset_instance RablRails::Library.reset_instance
RablRails::Library.instance.stub(:compile_template_from_path).with('users/base').and_return(template) RablRails::Library.instance.stub(:get).with('users/base').and_return(template)
t = @compiler.compile_source(%{ extends 'users/base' }) t = @compiler.compile_source(%{ extends 'users/base' })
assert_equal({ :id => :id }, t.source) assert_equal({ :id => :id }, t.source)
end end
@ -166,12 +138,6 @@ class CompilerTest < ActiveSupport::TestCase
assert_equal 2, t.source[:foo].size assert_equal 2, t.source[:foo].size
end end
test "conditionnal block compile nicely" do
t = @compiler.compile_source(%{ condition(->(u) {}) do attributes :secret end })
assert_instance_of RablRails::Condition, t.source[:_if0]
assert_equal({ :secret => :secret }, t.source[:_if0].source)
end
test "compile with no object" do test "compile with no object" do
t = @compiler.compile_source(%{ t = @compiler.compile_source(%{
object false object false
@ -183,10 +149,14 @@ class CompilerTest < ActiveSupport::TestCase
assert_equal({ :user => { :_data => :@user, :id => :id } }, t.source) assert_equal({ :user => { :_data => :@user, :id => :id } }, t.source)
assert_equal false, t.data assert_equal false, t.data
end end
test "name extraction from argument" do test "cache are compiled to fragment" do
assert_equal [:@users, 'users'], @compiler.send(:extract_data_and_name, :@users) t = @compiler.compile_source(%{
assert_equal [:users, :users], @compiler.send(:extract_data_and_name, :users) cache do
assert_equal [:@users, :authors], @compiler.send(:extract_data_and_name, :@users => :authors) attributes :name
end
})
assert_instance_of RablRails::Fragment, t.source[:_cache0]
assert_equal({ :name => :name }, t.source[:_cache0].compiled_source)
end end
end end

View File

@ -19,10 +19,12 @@ 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.stub(:instance_variable_get).with(:@user).and_return(@user)
@context.virtual_path = 'users/show' @context.stub(:instance_variable_get).with(:@virtual_path).and_return('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
@ -38,7 +40,7 @@ class DeepNestingTest < ActiveSupport::TestCase
end end
} }
assert_equal(MultiJson.encode(:user => { assert_equal(ActiveSupport::JSON.encode({
:id => 1, :id => 1,
:name => 'foobar', :name => 'foobar',
:posts => [{ :posts => [{

View File

@ -0,0 +1,4 @@
require 'test_helper'
class FragmentCacheTest < ActiveSupport::TestCase
end

View File

@ -1,47 +0,0 @@
require 'test_helper'
class KeywordTest < ActiveSupport::TestCase
class Collection
attr_accessor :id, :name
def initialize(id, name)
@id = id
@name = name
end
def cover(size)
"foo_#{size}"
end
end
setup do
RablRails::Library.reset_instance
@context = Context.new
@user = User.new(1, 'Marty')
@collections = [Collection.new(1, 'first'), Collection.new(2, 'last')]
@context.assigns['user'] = @user
@context.assigns['collections'] = @collections
@context.virtual_path = 'user/show'
@context.stub(lookup_context: nil)
end
test "collections model should render correctly" do
source = %{
object :@user
child(:@collections => :collections) do
attributes :id, :name
node(:cover_url) { |c|
c.cover(:medium)
}
end
}
assert_equal(MultiJson.encode(
user: { collections: [{
id: 1, name: 'first', cover_url: "foo_medium"
}, {
id: 2, name: 'last', cover_url: "foo_medium"
}] }
), RablRails::Library.instance.get_rendered_template(source, @context))
end
end

View File

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

View File

@ -1,63 +0,0 @@
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,9 +4,11 @@ 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.stub(:instance_variable_get).with(:@data).and_return(@data)
@context.stub(:instance_variable_get).with(:@_assigns).and_return({})
@template = RablRails::CompiledTemplate.new @template = RablRails::CompiledTemplate.new
@template.data = :@data @template.data = :@data
@ -22,18 +24,11 @@ class TestJsonRenderer < ActiveSupport::TestCase
end end
test "render collection with empty template" do test "render collection with empty template" do
@context.assigns['data'] = [@data] @context.stub(:instance_variable_get).with(:@data).and_return([@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
@ -50,25 +45,14 @@ 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
end end
test "render glued node" do
@template.source = { :_glue0 => { :_data => :@data, :foo => lambda { |u| u.name } } }
assert_equal(%q({"foo":"foobar"}), render_json_output)
end
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.assigns['data'] = @data @context.stub(:instance_variable_get).with(:@data).and_return(@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
@ -92,9 +76,8 @@ class TestJsonRenderer < ActiveSupport::TestCase
@template.source = { :name => [condition, proc] } @template.source = { :name => [condition, proc] }
assert_equal %q({}), render_json_output assert_equal %q({}), render_json_output
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 }
@ -104,13 +87,14 @@ 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.assigns['user'] = @data @context.stub(:instance_variable_get).with(:@_assigns).and_return({'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
t.source = { :name => :name } t.source = { :name => :name }
RablRails::Library.reset_instance RablRails::Library.reset_instance
RablRails::Library.instance.should_receive(:compile_template_from_path).with('users/base').and_return(t) RablRails::Library.instance.should_receive(:get).with('users/base').and_return(t)
@template.data = false @template.data = false
@template.source = { :user => ->(s) { partial('users/base', :object => @user) } } @template.source = { :user => ->(s) { partial('users/base', :object => @user) } }
@ -131,30 +115,4 @@ class TestJsonRenderer < ActiveSupport::TestCase
assert_equal %q({"users":[]}), render_json_output assert_equal %q({"users":[]}), render_json_output
end end
test "condition blocks are transparent if the condition passed" do
c = RablRails::Condition.new(->(u) { true }, { :name => :name })
@template.source = { :_if0 => c }
assert_equal %q({"name":"foobar"}), render_json_output
end
test "condition blocks are ignored if the condition is not met" do
c = RablRails::Condition.new(->(u) { false }, { :name => :name })
@template.source = { :_if0 => c }
assert_equal %q({}), render_json_output
end
test "render object with root node" do
RablRails.include_json_root = true
@template.root_name = :author
@template.source = { :id => :id, :name => :name }
assert_equal %q({"author":{"id":1,"name":"foobar"}}), render_json_output
end
test "render object with root options set to false" do
RablRails.include_json_root = false
@template.root_name = :author
@template.source = { :id => :id, :name => :name }
assert_equal %q({"id":1,"name":"foobar"}), render_json_output
end
end end

View File

@ -1,135 +0,0 @@
require 'test_helper'
class TestPlistRenderer < 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_plist_output
output = RablRails::Renderers::PLIST.new(@context).render(@template).to_s.gsub!(INDENT_REGEXP, '')
output.sub!(HEADER_REGEXP, '').gsub!(%r(</?plist[^>]*>), '').sub!(%r(<dict/?>), '').sub(%r(</dict>), '')
end
test "plist engine should responsd to #dump" do
assert_raises(RuntimeError) { RablRails.plist_engine = Object.new }
end
test "render object wth empty template" do
@template.source = {}
assert_equal %q(), render_plist_output
end
test "render collection with empty template" do
@context.assigns['data'] = [@data]
@template.source = {}
assert_equal %q(<array></array>), render_plist_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(<key>id</key><integer>1</integer>), render_plist_output
end
test "render single object attributes" do
@template.source = { :id => :id, :name => :name }
assert_equal %q(<key>id</key><integer>1</integer><key>name</key><string>foobar</string>), render_plist_output
end
test "render child with object association" do
@data.stub(:address).and_return(mock(:city => 'Paris'))
@template.source = { :address => { :_data => :address, :city => :city } }
assert_equal %q(<key>address</key><dict><key>city</key><string>Paris</string></dict>), render_plist_output
end
test "render child with arbitrary data source" do
@template.source = { :author => { :_data => :@data, :name => :name } }
assert_equal %q(<key>author</key><dict><key>name</key><string>foobar</string></dict>), render_plist_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(<key>author</key><dict><key>name</key><string>foobar</string></dict>), render_plist_output
end
test "render node property" do
proc = lambda { |object| object.name }
@template.source = { :name => proc }
assert_equal %q(<key>name</key><string>foobar</string>), render_plist_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(<key>name</key><string>foobar</string>), render_plist_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(), render_plist_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(<key>name</key><string>marty</string>), render_plist_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_plist_output }
end
test "partial with empty values should not raise an error" do
@template.data = false
@template.source = { :users => ->(s) { partial('users/base', :object => []) } }
assert_equal %q(<key>users</key><array/>), render_plist_output
end
test "condition blocks are transparent if the condition passed" do
c = RablRails::Condition.new(->(u) { true }, { :name => :name })
@template.source = { :_if0 => c }
assert_equal %q(<key>name</key><string>foobar</string>), render_plist_output
end
test "condition blocks are ignored if the condition is not met" do
c = RablRails::Condition.new(->(u) { false }, { :name => :name })
@template.source = { :_if0 => c }
assert_equal %q(), render_plist_output
end
test "render object with root node" do
RablRails.include_plist_root = true
@template.root_name = :author
@template.source = { :id => :id, :name => :name }
assert_equal %q(<key>author</key><dict><key>id</key><integer>1</integer><key>name</key><string>foobar</string></dict>), render_plist_output
end
test "render object with root options set to false" do
RablRails.include_plist_root = false
@template.root_name = :author
@template.source = { :id => :id, :name => :name }
assert_equal %q(<key>id</key><integer>1</integer><key>name</key><string>foobar</string>), render_plist_output
end
end

View File

@ -1,131 +0,0 @@
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

@ -22,15 +22,6 @@ class <<Singleton
end end
require 'rabl-rails' require 'rabl-rails'
require 'plist'
if RUBY_ENGINE == 'jruby'
require 'nokogiri'
else
require 'libxml'
end
RablRails.load_default_engines!
module ActiveSupport module ActiveSupport
class TestCase class TestCase
@ -40,15 +31,10 @@ module ActiveSupport
end end
class Context class Context
attr_writer :virtual_path attr_accessor :virtual_path
def initialize def initialize
@_assigns = {} @_assigns = {}
@virtual_path = nil
end
def assigns
@_assigns
end end
def params def params
@ -56,11 +42,4 @@ class Context
end end
end end
class User User = Struct.new(:id, :name, :sex)
attr_accessor :id, :name, :sex
def initialize(id=nil, name=nil, sex=nil)
@id = id
@name = name
@sex = sex
end
end