rabl-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|