rabl-rails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +69 -0
- data/MIT-LICENSE +20 -0
- data/README.md +248 -0
- data/Rakefile +35 -0
- data/lib/rabl-rails.rb +41 -0
- data/lib/rabl-rails/compiler.rb +146 -0
- data/lib/rabl-rails/handler.rb +15 -0
- data/lib/rabl-rails/library.rb +37 -0
- data/lib/rabl-rails/railtie.rb +9 -0
- data/lib/rabl-rails/renderer.rb +2 -0
- data/lib/rabl-rails/renderers/base.rb +116 -0
- data/lib/rabl-rails/renderers/json.rb +10 -0
- data/lib/rabl-rails/template.rb +11 -0
- data/lib/rabl-rails/version.rb +3 -0
- data/lib/tasks/rabl-rails.rake +4 -0
- data/rabl-rails.gemspec +23 -0
- data/test/cache_templates_test.rb +34 -0
- data/test/compiler_test.rb +163 -0
- data/test/deep_nesting_test.rb +58 -0
- data/test/non_restful_response_test.rb +35 -0
- data/test/renderers/json_renderer_test.rb +131 -0
- data/test/test_helper.rb +45 -0
- metadata +139 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
module RablRails
|
2
|
+
module Handlers
|
3
|
+
class Rabl
|
4
|
+
cattr_accessor :default_format
|
5
|
+
self.default_format = 'application/json'
|
6
|
+
|
7
|
+
def self.call(template)
|
8
|
+
%{
|
9
|
+
RablRails::Library.instance.
|
10
|
+
get_rendered_template(#{template.source.inspect}, self)
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module RablRails
|
4
|
+
class Library
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@cached_templates = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_rendered_template(source, context)
|
12
|
+
path = context.instance_variable_get(:@virtual_path)
|
13
|
+
@lookup_context = context.lookup_context
|
14
|
+
|
15
|
+
compiled_template = compile_template_from_source(source, path)
|
16
|
+
|
17
|
+
format = context.params[:format] || 'json'
|
18
|
+
Renderers.const_get(format.upcase!).new(context).render(compiled_template)
|
19
|
+
end
|
20
|
+
|
21
|
+
def compile_template_from_source(source, path = nil)
|
22
|
+
if path && RablRails.cache_templates?
|
23
|
+
@cached_templates[path] ||= Compiler.new.compile_source(source)
|
24
|
+
@cached_templates[path].dup
|
25
|
+
else
|
26
|
+
Compiler.new.compile_source(source)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def compile_template_from_path(path)
|
31
|
+
template = @cached_templates[path]
|
32
|
+
return template if template
|
33
|
+
t = @lookup_context.find_template(path, [], false)
|
34
|
+
compile_template_from_source(t.source, path)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module RablRails
|
2
|
+
module Renderers
|
3
|
+
class PartialError < StandardError; end
|
4
|
+
|
5
|
+
class Base
|
6
|
+
attr_accessor :options
|
7
|
+
|
8
|
+
def initialize(context) # :nodoc:
|
9
|
+
@_context = context
|
10
|
+
@options = {}
|
11
|
+
setup_render_context
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Render a template.
|
16
|
+
# Uses the compiled template source to get a hash with the actual
|
17
|
+
# data and then format the result according to the `format_result`
|
18
|
+
# method defined by the renderer.
|
19
|
+
#
|
20
|
+
def render(template)
|
21
|
+
collection_or_resource = @_context.instance_variable_get(template.data) if template.data
|
22
|
+
output_hash = collection_or_resource.respond_to?(:each) ? render_collection(collection_or_resource, template.source) :
|
23
|
+
render_resource(collection_or_resource, template.source)
|
24
|
+
options[:root_name] = template.root_name
|
25
|
+
format_output(output_hash)
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Format a hash into the desired output.
|
30
|
+
# Renderer subclasses must implement this method
|
31
|
+
#
|
32
|
+
def format_output(hash)
|
33
|
+
raise "Muse be implemented by renderer"
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
#
|
39
|
+
# Render a single resource as a hash, according to the compiled
|
40
|
+
# template source passed.
|
41
|
+
#
|
42
|
+
def render_resource(data, source)
|
43
|
+
source.inject({}) { |output, current|
|
44
|
+
key, value = current
|
45
|
+
|
46
|
+
out = case value
|
47
|
+
when Symbol
|
48
|
+
data.send(value) # attributes
|
49
|
+
when Proc
|
50
|
+
instance_exec data, &value # node
|
51
|
+
when Array # node with condition
|
52
|
+
next output if !instance_exec data, &(value.first)
|
53
|
+
instance_exec data, &(value.last)
|
54
|
+
when Hash
|
55
|
+
current_value = value.dup
|
56
|
+
data_symbol = current_value.delete(:_data)
|
57
|
+
object = data_symbol.nil? ? data : data_symbol.to_s.start_with?('@') ? @_context.instance_variable_get(data_symbol) : data.send(data_symbol)
|
58
|
+
|
59
|
+
if key.to_s.start_with?('_') # glue
|
60
|
+
current_value.each_pair { |k, v|
|
61
|
+
output[k] = object.send(v)
|
62
|
+
}
|
63
|
+
next output
|
64
|
+
else # child
|
65
|
+
object.respond_to?(:each) ? render_collection(object, current_value) : render_resource(object, current_value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
output[key] = out
|
69
|
+
output
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Call the render_resource mtehod on each object of the collection
|
75
|
+
# and return an array of the returned values.
|
76
|
+
#
|
77
|
+
def render_collection(collection, source)
|
78
|
+
collection.map { |o| render_resource(o, source) }
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Allow to use partial inside of node blocks (they are evaluated at)
|
83
|
+
# rendering time.
|
84
|
+
#
|
85
|
+
def partial(template_path, options = {})
|
86
|
+
raise PartialError.new("No object was given to partial #{template_path}") unless options[:object]
|
87
|
+
object = options[:object]
|
88
|
+
|
89
|
+
return [] if object.respond_to?(:empty?) && object.empty?
|
90
|
+
|
91
|
+
template = Library.instance.compile_template_from_path(template_path)
|
92
|
+
object.respond_to?(:each) ? render_collection(object, template.source) : render_resource(object, template.source)
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# If a method is called inside a 'node' property or a 'if' lambda
|
97
|
+
# it will be passed to context if it exists or treated as a standard
|
98
|
+
# missing method.
|
99
|
+
#
|
100
|
+
def method_missing(name, *args, &block)
|
101
|
+
@_context.respond_to?(name) ? @_context.send(name, *args, &block) : super
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# Copy assigns from controller's context into this
|
106
|
+
# renderer context to include instances variables when
|
107
|
+
# evaluating 'node' properties.
|
108
|
+
#
|
109
|
+
def setup_render_context
|
110
|
+
@_context.instance_variable_get(:@_assigns).each_pair { |k, v|
|
111
|
+
instance_variable_set("@#{k}", v) unless k.start_with?('_') || k == @data
|
112
|
+
}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/rabl-rails.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "rabl-rails/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "rabl-rails"
|
6
|
+
s.version = RablRails::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Christopher Cocchi-Perrier"]
|
9
|
+
s.email = ["cocchi.c@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/ccocchi/rabl-rails"
|
11
|
+
s.summary = "Fast Rails 3+ templating system with JSON and XML support"
|
12
|
+
s.description = "Fast Rails 3+ templating system with JSON and XML support"
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.add_dependency "activesupport", "~> 3.0"
|
19
|
+
s.add_dependency "railties", "~> 3.0"
|
20
|
+
|
21
|
+
s.add_development_dependency "sqlite3"
|
22
|
+
s.add_development_dependency "actionpack", "~> 3.0"
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class CacheTemplatesTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
setup do
|
6
|
+
RablRails::Library.reset_instance
|
7
|
+
@library = RablRails::Library.instance
|
8
|
+
RablRails.cache_templates = true
|
9
|
+
end
|
10
|
+
|
11
|
+
test "cache templates if perform_caching is active and cache_templates is enabled" do
|
12
|
+
ActionController::Base.stub(:perform_caching).and_return(true)
|
13
|
+
@library.compile_template_from_source('', 'some/path')
|
14
|
+
t = @library.compile_template_from_source("attribute :id", 'some/path')
|
15
|
+
|
16
|
+
assert_equal({}, t.source)
|
17
|
+
end
|
18
|
+
|
19
|
+
test "cached templates should not be modifiable in place" do
|
20
|
+
ActionController::Base.stub(:perform_caching).and_return(true)
|
21
|
+
@library.compile_template_from_source('', 'some/path')
|
22
|
+
t = @library.compile_template_from_source("attribute :id", 'some/path')
|
23
|
+
|
24
|
+
assert_equal({}, t.source)
|
25
|
+
end
|
26
|
+
|
27
|
+
test "don't cache templates cache_templates is enabled but perform_caching is not active" do
|
28
|
+
ActionController::Base.stub(:perform_caching).and_return(false)
|
29
|
+
@library.compile_template_from_source('', 'some/path')
|
30
|
+
t = @library.compile_template_from_source("attribute :id", 'some/path')
|
31
|
+
|
32
|
+
assert_equal({ :id => :id }, t.source)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class CompilerTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
setup do
|
6
|
+
@user = User.new
|
7
|
+
@compiler = RablRails::Compiler.new
|
8
|
+
end
|
9
|
+
|
10
|
+
test "compiler return a compiled template" do
|
11
|
+
assert_instance_of RablRails::CompiledTemplate, @compiler.compile_source("")
|
12
|
+
end
|
13
|
+
|
14
|
+
test "object set data for the template" do
|
15
|
+
t = @compiler.compile_source(%{ object :@user })
|
16
|
+
assert_equal :@user, t.data
|
17
|
+
assert_equal({}, t.source)
|
18
|
+
end
|
19
|
+
|
20
|
+
test "object property can define root name" do
|
21
|
+
t = @compiler.compile_source(%{ object :@user => :author })
|
22
|
+
assert_equal :@user, t.data
|
23
|
+
assert_equal :author, t.root_name
|
24
|
+
assert_equal({}, t.source)
|
25
|
+
end
|
26
|
+
|
27
|
+
test "collection set the data for the template" do
|
28
|
+
t = @compiler.compile_source(%{ collection :@user })
|
29
|
+
assert_equal :@user, t.data
|
30
|
+
assert_equal({}, t.source)
|
31
|
+
end
|
32
|
+
|
33
|
+
test "collection property can define root name" do
|
34
|
+
t = @compiler.compile_source(%{ collection :@user => :users })
|
35
|
+
assert_equal :@user, t.data
|
36
|
+
assert_equal :users, t.root_name
|
37
|
+
assert_equal({}, t.source)
|
38
|
+
end
|
39
|
+
|
40
|
+
test "collection property can define root name via options" do
|
41
|
+
t = @compiler.compile_source(%{ collection :@user, :root => :users })
|
42
|
+
assert_equal :@user, t.data
|
43
|
+
assert_equal :users, t.root_name
|
44
|
+
end
|
45
|
+
|
46
|
+
test "root can be set to false via options" do
|
47
|
+
t = @compiler.compile_source(%( object :@user, root: false))
|
48
|
+
assert_equal false, t.root_name
|
49
|
+
end
|
50
|
+
|
51
|
+
# Compilation
|
52
|
+
|
53
|
+
test "simple attributes are compiled to hash" do
|
54
|
+
t = @compiler.compile_source(%{ attributes :id, :name })
|
55
|
+
assert_equal({ :id => :id, :name => :name}, t.source)
|
56
|
+
end
|
57
|
+
|
58
|
+
test "attributes appeared only once even if called mutiple times" do
|
59
|
+
t = @compiler.compile_source(%{ attribute :id ; attribute :id })
|
60
|
+
assert_equal({ :id => :id }, t.source)
|
61
|
+
end
|
62
|
+
|
63
|
+
test "attribute can be aliased through :as option" do
|
64
|
+
t = @compiler.compile_source(%{ attribute :foo, :as => :bar })
|
65
|
+
assert_equal({ :bar => :foo}, t.source)
|
66
|
+
end
|
67
|
+
|
68
|
+
test "attribute can be aliased through hash" do
|
69
|
+
t = @compiler.compile_source(%{ attribute :foo => :bar })
|
70
|
+
assert_equal({ :bar => :foo }, t.source)
|
71
|
+
end
|
72
|
+
|
73
|
+
test "multiple attributes can be aliased" do
|
74
|
+
t = @compiler.compile_source(%{ attributes :foo => :bar, :id => :uid })
|
75
|
+
assert_equal({ :bar => :foo, :uid => :id }, t.source)
|
76
|
+
end
|
77
|
+
|
78
|
+
test "child with association use association name as data" do
|
79
|
+
t = @compiler.compile_source(%{ child :address do attributes :foo end})
|
80
|
+
assert_equal({ :address => { :_data => :address, :foo => :foo } }, t.source)
|
81
|
+
end
|
82
|
+
|
83
|
+
test "child with association can be aliased" do
|
84
|
+
t = @compiler.compile_source(%{ child :address => :bar do attributes :foo end})
|
85
|
+
assert_equal({ :bar => { :_data => :address, :foo => :foo } }, t.source)
|
86
|
+
end
|
87
|
+
|
88
|
+
test "child with root name defined as option" do
|
89
|
+
t = @compiler.compile_source(%{ child(:user, :root => :author) do attributes :foo end })
|
90
|
+
assert_equal({ :author => { :_data => :user, :foo => :foo } }, t.source)
|
91
|
+
end
|
92
|
+
|
93
|
+
test "child with arbitrary source store the data with the template" do
|
94
|
+
t = @compiler.compile_source(%{ child :@user => :author do attribute :name end })
|
95
|
+
assert_equal({ :author => { :_data => :@user, :name => :name } }, t.source)
|
96
|
+
end
|
97
|
+
|
98
|
+
test "child with succint partial notation" do
|
99
|
+
mock_template = RablRails::CompiledTemplate.new
|
100
|
+
mock_template.source = { :id => :id }
|
101
|
+
RablRails::Library.reset_instance
|
102
|
+
RablRails::Library.instance.stub(:compile_template_from_path).with('users/base').and_return(mock_template)
|
103
|
+
|
104
|
+
t = @compiler.compile_source(%{child(:user, :partial => 'users/base') })
|
105
|
+
assert_equal( {:user => { :_data => :user, :id => :id } }, t.source)
|
106
|
+
end
|
107
|
+
|
108
|
+
test "glue is compiled as a child but with anonymous name" do
|
109
|
+
t = @compiler.compile_source(%{ glue(:@user) do attribute :name end })
|
110
|
+
assert_equal({ :_glue0 => { :_data => :@user, :name => :name } }, t.source)
|
111
|
+
end
|
112
|
+
|
113
|
+
test "multiple glue don't come with name collisions" do
|
114
|
+
t = @compiler.compile_source(%{
|
115
|
+
glue :@user do attribute :name end
|
116
|
+
glue :@user do attribute :foo end
|
117
|
+
})
|
118
|
+
|
119
|
+
assert_equal({
|
120
|
+
:_glue0 => { :_data => :@user, :name => :name},
|
121
|
+
:_glue1 => { :_data => :@user, :foo => :foo}
|
122
|
+
}, t.source)
|
123
|
+
end
|
124
|
+
|
125
|
+
test "extends use other template source as itself" do
|
126
|
+
template = mock('template', :source => { :id => :id })
|
127
|
+
RablRails::Library.reset_instance
|
128
|
+
RablRails::Library.instance.stub(:compile_template_from_path).with('users/base').and_return(template)
|
129
|
+
t = @compiler.compile_source(%{ extends 'users/base' })
|
130
|
+
assert_equal({ :id => :id }, t.source)
|
131
|
+
end
|
132
|
+
|
133
|
+
test "node are compiled without evaluating the block" do
|
134
|
+
t = @compiler.compile_source(%{ node(:foo) { bar } })
|
135
|
+
assert_not_nil t.source[:foo]
|
136
|
+
assert_instance_of Proc, t.source[:foo]
|
137
|
+
end
|
138
|
+
|
139
|
+
test "node with condition are compiled as an array of procs" do
|
140
|
+
t = @compiler.compile_source(%{ node(:foo, :if => lambda { |m| m.foo.present? }) do |m| m.foo end })
|
141
|
+
assert_not_nil t.source[:foo]
|
142
|
+
assert_instance_of Array, t.source[:foo]
|
143
|
+
assert_equal 2, t.source[:foo].size
|
144
|
+
end
|
145
|
+
|
146
|
+
test "compile with no object" do
|
147
|
+
t = @compiler.compile_source(%{
|
148
|
+
object false
|
149
|
+
child(:@user => :user) do
|
150
|
+
attribute :id
|
151
|
+
end
|
152
|
+
})
|
153
|
+
|
154
|
+
assert_equal({ :user => { :_data => :@user, :id => :id } }, t.source)
|
155
|
+
assert_equal false, t.data
|
156
|
+
end
|
157
|
+
|
158
|
+
test "name extraction from argument" do
|
159
|
+
assert_equal [:@users, 'users'], @compiler.send(:extract_data_and_name, :@users)
|
160
|
+
assert_equal [:users, :users], @compiler.send(:extract_data_and_name, :users)
|
161
|
+
assert_equal [:@users, :authors], @compiler.send(:extract_data_and_name, :@users => :authors)
|
162
|
+
end
|
163
|
+
end
|