rabl-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ .bundle/
2
+ log/*.log
3
+ pkg/
4
+ test/dummy/db/*.sqlite3
5
+ test/dummy/log/*.log
6
+ test/dummy/tmp/
7
+ test/dummy/.sass-cache
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'yajl-ruby'
6
+
7
+ group :test do
8
+ gem 'rspec-mocks'
9
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,69 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rabl-rails (0.1.0)
5
+ activesupport (~> 3.0)
6
+ railties (~> 3.0)
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ actionpack (3.2.6)
12
+ activemodel (= 3.2.6)
13
+ activesupport (= 3.2.6)
14
+ builder (~> 3.0.0)
15
+ erubis (~> 2.7.0)
16
+ journey (~> 1.0.1)
17
+ rack (~> 1.4.0)
18
+ rack-cache (~> 1.2)
19
+ rack-test (~> 0.6.1)
20
+ sprockets (~> 2.1.3)
21
+ activemodel (3.2.6)
22
+ activesupport (= 3.2.6)
23
+ builder (~> 3.0.0)
24
+ activesupport (3.2.6)
25
+ i18n (~> 0.6)
26
+ multi_json (~> 1.0)
27
+ builder (3.0.0)
28
+ erubis (2.7.0)
29
+ hike (1.2.1)
30
+ i18n (0.6.0)
31
+ journey (1.0.4)
32
+ json (1.7.3)
33
+ multi_json (1.3.6)
34
+ rack (1.4.1)
35
+ rack-cache (1.2)
36
+ rack (>= 0.4)
37
+ rack-ssl (1.3.2)
38
+ rack
39
+ rack-test (0.6.1)
40
+ rack (>= 1.0)
41
+ railties (3.2.6)
42
+ actionpack (= 3.2.6)
43
+ activesupport (= 3.2.6)
44
+ rack-ssl (~> 1.3.2)
45
+ rake (>= 0.8.7)
46
+ rdoc (~> 3.4)
47
+ thor (>= 0.14.6, < 2.0)
48
+ rake (0.9.2.2)
49
+ rdoc (3.12)
50
+ json (~> 1.4)
51
+ rspec-mocks (2.11.1)
52
+ sprockets (2.1.3)
53
+ hike (~> 1.2)
54
+ rack (~> 1.0)
55
+ tilt (~> 1.1, != 1.3.0)
56
+ sqlite3 (1.3.6)
57
+ thor (0.15.4)
58
+ tilt (1.3.3)
59
+ yajl-ruby (1.1.0)
60
+
61
+ PLATFORMS
62
+ ruby
63
+
64
+ DEPENDENCIES
65
+ actionpack (~> 3.0)
66
+ rabl-rails!
67
+ rspec-mocks
68
+ sqlite3
69
+ yajl-ruby
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 Christopher Cocchi-Perrier
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # RABL for Rails #
2
+
3
+ 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).
4
+
5
+ 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 !
6
+
7
+ 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.
8
+
9
+ ## Installation
10
+
11
+ Install as a gem :
12
+
13
+ ```
14
+ gem install rabl-rails
15
+ ```
16
+
17
+ or add directly to your `Gemfile`
18
+
19
+ ```
20
+ gem 'rabl-rails'
21
+ ```
22
+
23
+ And that's it !
24
+
25
+ ## Overview
26
+
27
+ Once you have installed RABL, you can directly used RABL templates to render your resources without changing anything to you controller. As example,
28
+ assuming you have a `Post` model filled with blog posts, and a `PostController` that look like this :
29
+
30
+ ```ruby
31
+ class PostController < ApplicationController
32
+ respond_to :html, :json, :xml
33
+
34
+ def index
35
+ @posts = Post.order('created_at DESC')
36
+ respond_with(@posts)
37
+ end
38
+ end
39
+ ```
40
+
41
+ You can create the following RABL-rails template to express the API output of `@posts`
42
+
43
+ ```ruby
44
+ # app/views/post/index.rabl
45
+ collection :@posts
46
+ attributes :id, :title, :subject
47
+ child(:user) { attributes :full_name }
48
+ node(:read) { |post| post.read_by?(@user) }
49
+ ```
50
+
51
+ This would output the following JSON when visiting `http://localhost:3000/posts.json`
52
+
53
+ ```js
54
+ [{
55
+ "id" : 5, title: "...", subject: "...",
56
+ "user" : { full_name : "..." },
57
+ "read" : true
58
+ }]
59
+ ```
60
+
61
+ That's a basic overview but there is a lot more to see such as partials, inheritance or fragment caching.
62
+
63
+ ## How it works
64
+
65
+ 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.
66
+
67
+ 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.
68
+
69
+ 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).
70
+
71
+ ```ruby
72
+ # We reference the @posts varibles that will be used at rendering time
73
+ collection :@posts
74
+
75
+ # Here you can use directly the instance variable because it
76
+ # will be evaluated when rendering the object
77
+ node(:read) { |post| post.read_by?(@user) }
78
+ ```
79
+
80
+ The same rule applies for view helpers such as `current_user`
81
+
82
+ 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.
83
+
84
+ ## Usage
85
+
86
+ ### Data declaration
87
+
88
+ To declare data to use in the template, you can use either `object` or `collection` with the symbol name or your data.
89
+
90
+ ```ruby
91
+ # app/views/users/show.json.rabl
92
+ object :@user
93
+
94
+ # app/views/users/index.json.rabl
95
+ collection :@users
96
+ ```
97
+
98
+ You can specify root label for the collection using hash or `:root` option
99
+
100
+ ```ruby
101
+ collection :@posts, root: :articles
102
+ #is equivalent to
103
+ collection :@posts => :articles
104
+
105
+ # => { "articles" : [{...}, {...}] }
106
+ ```
107
+
108
+ 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.
109
+
110
+ ```ruby
111
+ object false
112
+ node(:some_count) { |_| @user.posts.count }
113
+ child(:@user) { attribute :name }
114
+ ```
115
+
116
+ ### Attributes / Methods
117
+
118
+ Basic usage is to declared attributes to include in the response. These can be database attributes or any instance method.
119
+
120
+ ```ruby
121
+ attributes :id, :title, :to_s
122
+ ```
123
+
124
+ You can aliases these attributes in your response
125
+
126
+ ```ruby
127
+ attributes title: :foo, to_s: :bar
128
+ # => { "foo" : <title value>, "bar" : <to_s value> }
129
+ ```
130
+
131
+ ### Child nodes
132
+
133
+ You can include nested information from data associated with the parent model. You can also alias these associations.
134
+ For example if you have a `Post` model that belongs to a `User`
135
+
136
+ ```ruby
137
+ object :@post
138
+ child(user: :author) do
139
+ attributes :name
140
+ end
141
+ # => { "post" : { "author" : { "name" : "John D." } } }
142
+ ```
143
+
144
+ You can also use arbitrary data source with child nodes
145
+ ```ruby
146
+ child(:@users) do
147
+ attributes :id, :name
148
+ end
149
+ ```
150
+
151
+ ### Custom nodes
152
+
153
+ You can create custom node in your response, based on the result of the given block
154
+
155
+ ```ruby
156
+ object :@user
157
+ node(:full_name) { |u| u.first_name + " " + u.last_name }
158
+ # => { "user" : { "full_name" : "John Doe" } }
159
+ ```
160
+
161
+ You can add the node only if a condition is true
162
+
163
+ ```ruby
164
+ node(:email, if: -> { |u| u.valid_email? }) do |u|
165
+ u.email
166
+ end
167
+ ```
168
+
169
+ Nodes are evaluated at the rendering time, so you can use any instance variables or view helpers inside them
170
+
171
+ ```ruby
172
+ node(:url) { |post| post_url(post) }
173
+ ```
174
+
175
+ Custom nodes are really usefull to create flexible representations of your resources.
176
+
177
+ ### Extends & Partials
178
+
179
+ 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.
180
+
181
+ ```ruby
182
+ # app/views/users/base.json.rabl
183
+ attributes :id, :name
184
+
185
+ # app/views/users/private.json.rabl
186
+ extends 'users/base'
187
+ attributes :super_secret_attribute
188
+ ```
189
+
190
+ You can also extends template in child nodes using `partial` option (this is the same as using `extends` in the child block)
191
+
192
+ ```ruby
193
+ collection @posts
194
+ attribute :title
195
+ child(:user, partial: 'users/base')
196
+ ```
197
+
198
+ Partials can also be used inside custom nodes. When using partial this way, you MUST declare the object associated to the partial
199
+
200
+ ```ruby
201
+ node(:location) do |user|
202
+ { city: user.city, address: partial('users/address', object: m.address) }
203
+ end
204
+ ```
205
+
206
+ ### Nesting
207
+
208
+ 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
209
+
210
+ ```ruby
211
+ object :@thread
212
+ attribute :caption
213
+ child :posts do
214
+ attribute :title
215
+ child :comments do
216
+ extends 'comments/base'
217
+ end
218
+ end
219
+ ```
220
+
221
+ ## Performance
222
+
223
+ Benchmarks have been made using this [application](http://github.com/ccocchi/rabl-benchmark), with rabl 0.6.14 and rabl-rails 0.1.0
224
+
225
+ Overall, Rabl-rails is **20% faster and use 10% less memory**.
226
+
227
+ You can see full tests on test application repository.
228
+
229
+ ## Caching
230
+
231
+ Caching is not a part of Rabl-rails. It is already in Rails itself, because caching all view output is the same as action caching (Rails caching is even better because it will also not run your queries).
232
+
233
+ And caching each object in a collection can be really not effective with big collections or simple objects. This is also a nightmare with cache expiration.
234
+
235
+ ## Authors and contributors
236
+
237
+ * [Christopher Cocchi-Perrier](http://github.com/ccocchi) - Creator of the project
238
+
239
+ 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
240
+ Want to make another change ? Just fork and contribute, any help is very much appreciated
241
+
242
+ ## Original idea
243
+
244
+ * [RABL](http://github.com/nesquena/rabl) Standart RABL gem. I used it a lot before deciding I wanted faster views
245
+
246
+ ## Copyright
247
+
248
+ Copyright © 2011-2012 Christopher Cocchi-Perrier. See [MIT-LICENSE](http://github.com/ccocchi/rabl-rails/blob/master/MIT-LICENSE) for details.
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env rake
2
+ # begin
3
+ # require 'bundler/setup'
4
+ # rescue LoadError
5
+ # puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ # end
7
+ # begin
8
+ # require 'rdoc/task'
9
+ # rescue LoadError
10
+ # require 'rdoc/rdoc'
11
+ # require 'rake/rdoctask'
12
+ # RDoc::Task = Rake::RDocTask
13
+ # end
14
+ #
15
+ # RDoc::Task.new(:rdoc) do |rdoc|
16
+ # rdoc.rdoc_dir = 'rdoc'
17
+ # rdoc.title = 'RablRails'
18
+ # rdoc.options << '--line-numbers'
19
+ # rdoc.rdoc_files.include('README.rdoc')
20
+ # rdoc.rdoc_files.include('lib/**/*.rb')
21
+ # end
22
+
23
+ require 'bundler'
24
+ Bundler::GemHelper.install_tasks
25
+
26
+ require 'rake/testtask'
27
+ Rake::TestTask.new(:test) do |t|
28
+ t.libs << 'lib'
29
+ t.libs << 'test'
30
+ t.pattern = 'test/**/*_test.rb'
31
+ t.verbose = true
32
+ end
33
+
34
+
35
+ task :default => :test
data/lib/rabl-rails.rb ADDED
@@ -0,0 +1,41 @@
1
+ require 'rails/railtie'
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/class/attribute_accessors'
5
+
6
+ require 'rabl-rails/version'
7
+ require 'rabl-rails/template'
8
+ require 'rabl-rails/compiler'
9
+
10
+ require 'rabl-rails/renderer'
11
+
12
+ require 'rabl-rails/library'
13
+ require 'rabl-rails/handler'
14
+ require 'rabl-rails/railtie'
15
+
16
+ require 'multi_json'
17
+
18
+ module RablRails
19
+ mattr_accessor :cache_templates
20
+ @@cache_templates = true
21
+
22
+ mattr_accessor :include_json_root
23
+ @@include_json_root = true
24
+
25
+ mattr_accessor :json_engine
26
+ @@json_engine = :yajl
27
+
28
+ def self.configure
29
+ yield self
30
+ post_configure
31
+ end
32
+
33
+ def self.cache_templates?
34
+ ActionController::Base.perform_caching && @@cache_templates
35
+ end
36
+
37
+ private
38
+ def self.post_configure
39
+ MultiJson.engine = self.json_engine
40
+ end
41
+ end
@@ -0,0 +1,146 @@
1
+ module RablRails
2
+ #
3
+ # Class that will compile RABL source code into a hash
4
+ # representing data structure
5
+ #
6
+ class Compiler
7
+ def initialize
8
+ @glue_count = 0
9
+ end
10
+
11
+ #
12
+ # Compile from source code and return the CompiledTemplate
13
+ # created.
14
+ #
15
+ def compile_source(source)
16
+ @template = CompiledTemplate.new
17
+ instance_eval(source)
18
+ @template
19
+ end
20
+
21
+ #
22
+ # Sets the object to be used as the data for the template
23
+ # Example:
24
+ # object :@user
25
+ # object :@user, :root => :author
26
+ #
27
+ def object(data, options = {})
28
+ @template.data, @template.root_name = extract_data_and_name(data)
29
+ @template.root_name = options[:root] if options.has_key? :root
30
+ end
31
+ alias_method :collection, :object
32
+
33
+ #
34
+ # Includes the attribute or method in the output
35
+ # Example:
36
+ # attributes :id, :name
37
+ # attribute :email => :super_secret
38
+ #
39
+ def attribute(*args)
40
+ if args.first.is_a?(Hash)
41
+ args.first.each_pair { |k, v| @template[v] = k }
42
+ else
43
+ options = args.extract_options!
44
+ args.each { |name|
45
+ key = options[:as] || name
46
+ @template[key] = name
47
+ }
48
+ end
49
+ end
50
+ alias_method :attributes, :attribute
51
+
52
+ #
53
+ # Creates a child node to be included in the output.
54
+ # name_or data can be an object or collection or a method to call on the data. It
55
+ # accepts :root and :partial options.
56
+ # Notes that partial and blocks are not compatible
57
+ # Example:
58
+ # child(:@posts, :root => :posts) { attribute :id }
59
+ # child(:posts, :partial => 'posts/base')
60
+ #
61
+ def child(name_or_data, options = {})
62
+ data, name = extract_data_and_name(name_or_data)
63
+ name = options[:root] if options.has_key? :root
64
+ if options[:partial]
65
+ template = Library.instance.compile_template_from_path(options[:partial])
66
+ @template[name] = template.merge!(:_data => data)
67
+ elsif block_given?
68
+ @template[name] = sub_compile(data) { yield }
69
+ end
70
+ end
71
+
72
+ #
73
+ # Glues data from a child node to the output
74
+ # Example:
75
+ # glue(:@user) { attribute :name }
76
+ #
77
+ def glue(data)
78
+ return unless block_given?
79
+ name = :"_glue#{@glue_count}"
80
+ @glue_count += 1
81
+ @template[name] = sub_compile(data) { yield }
82
+ end
83
+
84
+ #
85
+ # Creates an arbitrary node in the json output.
86
+ # It accepts :if option to create conditionnal nodes. The current data will
87
+ # be passed to the block so it is advised to use it instead of ivars.
88
+ # Example:
89
+ # node(:name) { |user| user.first_name + user.last_name }
90
+ # node(:role, if: ->(u) { !u.admin? }) { |u| u.role }
91
+ #
92
+ def node(name, options = {}, &block)
93
+ condition = options[:if]
94
+
95
+ if condition
96
+ if condition.is_a?(Proc)
97
+ @template[name] = [condition, block]
98
+ else
99
+ @template[name] = block if condition
100
+ end
101
+ else
102
+ @template[name] = block
103
+ end
104
+ end
105
+ alias_method :code, :node
106
+
107
+ #
108
+ # Extends an existing rabl template
109
+ # Example:
110
+ # extends 'users/base'
111
+ #
112
+ def extends(path)
113
+ t = Library.instance.compile_template_from_path(path)
114
+ @template.merge!(t.source)
115
+ end
116
+
117
+ protected
118
+
119
+ #
120
+ # Extract data root_name and root name
121
+ # Example:
122
+ # :@users -> [:@users, nil]
123
+ # :@users => :authors -> [:@users, :authors]
124
+ #
125
+ def extract_data_and_name(name_or_data)
126
+ case name_or_data
127
+ when Symbol
128
+ str = name_or_data.to_s
129
+ str.start_with?('@') ? [name_or_data, str[1..-1]] : [name_or_data, name_or_data]
130
+ when Hash
131
+ name_or_data.first
132
+ else
133
+ name_or_data
134
+ end
135
+ end
136
+
137
+ def sub_compile(data)
138
+ return {} unless block_given?
139
+ old_template, @template = @template, {}
140
+ yield
141
+ @template.merge!(:_data => data)
142
+ ensure
143
+ @template = old_template
144
+ end
145
+ end
146
+ end