jb 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4a9a695209a26ce29bec15c3875242b2681f33df
4
- data.tar.gz: 2b5a75df2740953a69f6a6e8c2703e4b2bdf1f13
3
+ metadata.gz: 7cc429668499e99d5acd8fc816c5edbecd363d57
4
+ data.tar.gz: debb9265b363fa660ef632fa1fde79c76f0a1065
5
5
  SHA512:
6
- metadata.gz: 2ebedcd7cc5498c5143ad74121dbd7ddfa5cf4fcb8f0089a1d26b7b92fe3b01d6c41399bf73c9b971f9535569344ad0200bf1c12b40d02667a1419ce7b168f21
7
- data.tar.gz: 3beb62bd3a5024b9fb0af6f7f948a712bc161f5fe509aed130f24e38bfe4aafd035586382c895646163a5e9133f17dd23c71ee37504c7ea9ef3db5614fb25826
6
+ metadata.gz: 0f24ea0311ab49e1bdc5559f04e9feabc829643c4a9f1bdf9714084bb903589d81ea266ef8c269adb06c10ba27b9b7b255eae9a1e3366116e475f8f9ac212d9f
7
+ data.tar.gz: 30a0eb11d629dae406df1407de317cad6aad8bd59119cbd926f1041342483f3dad4a5a3735c8cf8433c2693aa506c37955f874cbf486910fba51a587fce271c7
data/Gemfile CHANGED
@@ -2,3 +2,8 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in jb.gemspec
4
4
  gemspec
5
+
6
+ # For benchmarking
7
+ gem 'jbuilder'
8
+ gem 'benchmark-ips'
9
+ gem 'action_args'
data/README.md CHANGED
@@ -16,8 +16,7 @@ And bundle.
16
16
 
17
17
  ## Usage
18
18
 
19
- Write a template that contains a Ruby code that returns a Ruby Hash / Array object.
20
- Then the object will be `to_json`ed to a JSON String.
19
+ Put a template file named `*.jb` in your Rails app's `app/views/*` directory, and render it.
21
20
 
22
21
 
23
22
  ## Features
@@ -27,6 +26,299 @@ Then the object will be `to_json`ed to a JSON String.
27
26
  * `render_partial` with :collection option actually renders the collection (unlike Jbuilder)
28
27
 
29
28
 
29
+ ## Syntax
30
+
31
+ A `.jb` template should contain Ruby code that returns any Ruby Object that responds_to `to_json` (generally Hash or Array).
32
+ Then the return value will be `to_json`ed to a JSON String.
33
+
34
+
35
+ ## Examples
36
+
37
+ ``` ruby
38
+ # app/views/messages/show.json.jb
39
+
40
+ json = {
41
+ content: format_content(@message.content),
42
+ created_at: @message.created_at,
43
+ updated_at: @message.updated_at,
44
+ author: {
45
+ name: @message.creator.name.familiar,
46
+ email_address: @message.creator.email_address_with_name,
47
+ url: url_for(@message.creator, format: :json)
48
+ }
49
+ }
50
+
51
+ if current_user.admin?
52
+ json[:visitors] = calculate_visitors(@message)
53
+ end
54
+
55
+ json[:comments] = {
56
+ content: @message.comments.content,
57
+ created_at: @message.comments.created_at
58
+ }
59
+
60
+ json[:attachments] = @message.attachments.map do |attachment|
61
+ filename: attachment.filename,
62
+ url: url_for(attachment)
63
+ end
64
+
65
+ json
66
+ ```
67
+
68
+ This will build the following structure:
69
+
70
+ ``` javascript
71
+ {
72
+ "content": "10x JSON",
73
+ "created_at": "2016-06-29T20:45:28-05:00",
74
+ "updated_at": "2016-06-29T20:45:28-05:00",
75
+
76
+ "author": {
77
+ "name": "Yukihiro Matz",
78
+ "email_address": "matz@example.com",
79
+ "url": "http://example.com/users/1-matz.json"
80
+ },
81
+
82
+ "visitors": 1326,
83
+
84
+ "comments": [
85
+ { "content": "Hello, world!", "created_at": "2016-06-29T20:45:28-05:00" },
86
+ { "content": "<script>alert('Hello, world!');</script>", "created_at": "2016-06-29T20:47:28-05:00" }
87
+ ],
88
+
89
+ "attachments": [
90
+ { "filename": "sushi.png", "url": "http://example.com/downloads/sushi.png" },
91
+ { "filename": "sake.jpg", "url": "http://example.com/downloads/sake.jpg" }
92
+ ]
93
+ }
94
+ ```
95
+
96
+ To define attribute and structure names dynamically, just use Ruby Hash.
97
+ Note that modern Ruby Hash syntax pretty much looks alike JSON syntax.
98
+ It's super-straight forward. Who needs a DSL to do this?
99
+
100
+ ``` ruby
101
+ {author: {name: 'Matz'}}
102
+
103
+ # => {"author": {"name": "Matz"}}
104
+ ```
105
+
106
+ Top level arrays can be handled directly. Useful for index and other collection actions.
107
+ And you know, Ruby is such a powerful language for manipulating collections:
108
+
109
+ ``` ruby
110
+ # @comments = @post.comments
111
+
112
+ @comments.reject {|c| c.marked_as_spam_by?(current_user) }.map do |comment|
113
+ body: comment.body,
114
+ author: {
115
+ first_name: comment.author.first_name,
116
+ last_name: comment.author.last_name
117
+ }
118
+ end
119
+
120
+ # => [{"body": "🍣 is omakase...", "author": {"first_name": "Yukihiro", "last_name": "Matz"}}]
121
+ ```
122
+
123
+ Jb has no special DSL method for extracting attributes from array directly, but you can do that with Ruby.
124
+
125
+ ``` ruby
126
+ # @people = People.all
127
+
128
+ @people.map {|p| {id: p.id, name: p.name}}
129
+
130
+ # => [{"id": 1, "name": "Matz"}, {"id": 2, "name": "Nobu"}]
131
+ ```
132
+
133
+ You can use Jb directly as an Action View template language.
134
+ When required in Rails, you can create views ala show.json.jb.
135
+ You'll notice in the following example that the `.jb` template
136
+ doesn't have to be one big Ruby Hash literal as a whole
137
+ but it can be any Ruby code that finally returns a Hash instance.
138
+
139
+ ``` ruby
140
+ # Any helpers available to views are available in the template
141
+ json = {
142
+ content: format_content(@message.content),
143
+ created_at: @message.created_at,
144
+ updated_at: @message.updated_at,
145
+
146
+ author: {
147
+ name: @message.creator.name.familiar,
148
+ email_address: @message.creator.email_address_with_name,
149
+ url: url_for(@message.creator, format: :json)
150
+ }
151
+ }
152
+
153
+ if current_user.admin?
154
+ json[:visitors] = calculate_visitors(@message)
155
+ end
156
+
157
+ json
158
+ ```
159
+
160
+ You can use partials as well. The following will render the file
161
+ `views/comments/_comments.json.jb`, and set a local variable
162
+ `comments` with all this message's comments, which you can use inside
163
+ the partial.
164
+
165
+ ```ruby
166
+ render 'comments/comments', comments: @message.comments
167
+ ```
168
+
169
+ It's also possible to render collections of partials:
170
+
171
+ ```ruby
172
+ render partial: 'posts/post', collection: @posts, as: :post
173
+
174
+ # or
175
+
176
+ render @post.comments
177
+ ```
178
+
179
+ You can pass any objects into partial templates with or without `:locals` option.
180
+
181
+ ```ruby
182
+ render 'sub_template', locals: {user: user}
183
+
184
+ # or
185
+
186
+ render 'sub_template', user: user
187
+ ```
188
+
189
+ You can of course include Ruby `nil` as a Hash value if you want. That would become `null` in the JSON.
190
+
191
+ To prevent Jb from including null values in the output, Active Support provides `Hash#compact!` method for you:
192
+
193
+ ```ruby
194
+ {foo: nil, bar: 'bar'}.compact
195
+
196
+ # => {"bar": "bar"}
197
+ ```
198
+
199
+ If you want to cache a template fragment, just directly call `Rails.cache.fetch`:
200
+
201
+ ```ruby
202
+ Rails.cache.fetch ['v1', @person], expires_in: 10.minutes do
203
+ {name: @person.name, age: @person.age}
204
+ end
205
+ ```
206
+
207
+
208
+ ## The Generator
209
+ Jb extends the default Rails scaffold generator and adds some .jb templates.
210
+ If you don't need them, please configure like so.
211
+
212
+ ```ruby
213
+ Rails.application.config.generators.jb false
214
+ ```
215
+
216
+
217
+ ## Why is Jb fast?
218
+
219
+ Jbuilder's `partial` + `:collection` [internally calls `array!` method](https://github.com/rails/jbuilder/blob/83a682aeebde96c6ef02ce742c0b97dc393f5e22/lib/jbuilder/jbuilder_template.rb#L85-L95)
220
+ inside which [`_render_partial` is called per each element of the given collection](https://github.com/rails/jbuilder/blob/83a682aeebde96c6ef02ce742c0b97dc393f5e22/lib/jbuilder/jbuilder_template.rb#L93),
221
+ and then it [falls back to the `view_context`'s `render` method](https://github.com/rails/jbuilder/blob/83a682aeebde96c6ef02ce742c0b97dc393f5e22/lib/jbuilder/jbuilder_template.rb#L100-L103).
222
+
223
+ So, for example if the collection has 100 elements, Jbuilder's `render partial:` performs `render` method 100 times, and so it calls `find_template` method (which is known as one of the heaviest parts of Action View) 100 times.
224
+
225
+ OTOH, Jb simply calls [ActionView::PartialRenderer's `render`](https://github.com/rails/rails/blob/49a881e0db1ef64fcbae2b7ddccfd5ccea26ae01/actionview/lib/action_view/renderer/partial_renderer.rb#L423-L443) which is cleverly implmented to `find_template` only once beforehand, then pass each element to that template.
226
+
227
+
228
+ ## Bencharks
229
+ Here're the results of a benchmark (which you can find [here](https://github.com/amatsuda/jb/blob/master/test/dummy_app/app/controllers/benchmarks_controller.rb) in this repo) rendering a collection to JSON.
230
+
231
+ ### RAILS_ENV=development
232
+ ```
233
+ % ./bin/benchmark.sh
234
+ * Rendering 10 partials via render_partial
235
+ Warming up --------------------------------------
236
+ jb 15.000 i/100ms
237
+ jbuilder 8.000 i/100ms
238
+ Calculating -------------------------------------
239
+ jb 156.375 (± 7.0%) i/s - 780.000 in 5.016581s
240
+ jbuilder 87.890 (± 6.8%) i/s - 440.000 in 5.037225s
241
+
242
+ Comparison:
243
+ jb: 156.4 i/s
244
+ jbuilder: 87.9 i/s - 1.78x slower
245
+
246
+
247
+ * Rendering 100 partials via render_partial
248
+ Warming up --------------------------------------
249
+ jb 13.000 i/100ms
250
+ jbuilder 1.000 i/100ms
251
+ Calculating -------------------------------------
252
+ jb 121.187 (±14.0%) i/s - 598.000 in 5.049667s
253
+ jbuilder 11.478 (±26.1%) i/s - 54.000 in 5.061996s
254
+
255
+ Comparison:
256
+ jb: 121.2 i/s
257
+ jbuilder: 11.5 i/s - 10.56x slower
258
+
259
+
260
+ * Rendering 1000 partials via render_partial
261
+ Warming up --------------------------------------
262
+ jb 4.000 i/100ms
263
+ jbuilder 1.000 i/100ms
264
+ Calculating -------------------------------------
265
+ jb 51.472 (± 7.8%) i/s - 256.000 in 5.006584s
266
+ jbuilder 1.510 (± 0.0%) i/s - 8.000 in 5.383548s
267
+
268
+ Comparison:
269
+ jb: 51.5 i/s
270
+ jbuilder: 1.5 i/s - 34.08x slower
271
+ ```
272
+
273
+
274
+ ### RAILS_ENV=production
275
+ ```
276
+ % RAILS_ENV=production ./bin/benchmark.sh
277
+ * Rendering 10 partials via render_partial
278
+ Warming up --------------------------------------
279
+ jb 123.000 i/100ms
280
+ jbuilder 41.000 i/100ms
281
+ Calculating -------------------------------------
282
+ jb 1.406k (± 4.2%) i/s - 7.134k in 5.084030s
283
+ jbuilder 418.360 (± 9.8%) i/s - 2.091k in 5.043381s
284
+
285
+ Comparison:
286
+ jb: 1405.8 i/s
287
+ jbuilder: 418.4 i/s - 3.36x slower
288
+
289
+
290
+ * Rendering 100 partials via render_partial
291
+ Warming up --------------------------------------
292
+ jb 37.000 i/100ms
293
+ jbuilder 5.000 i/100ms
294
+ Calculating -------------------------------------
295
+ jb 383.082 (± 8.4%) i/s - 1.924k in 5.061973s
296
+ jbuilder 49.914 (± 8.0%) i/s - 250.000 in 5.040364s
297
+
298
+ Comparison:
299
+ jb: 383.1 i/s
300
+ jbuilder: 49.9 i/s - 7.67x slower
301
+
302
+
303
+ * Rendering 1000 partials via render_partial
304
+ Warming up --------------------------------------
305
+ jb 4.000 i/100ms
306
+ jbuilder 1.000 i/100ms
307
+ Calculating -------------------------------------
308
+ jb 43.017 (± 9.3%) i/s - 216.000 in 5.080482s
309
+ jbuilder 4.604 (±21.7%) i/s - 23.000 in 5.082100s
310
+
311
+ Comparison:
312
+ jb: 43.0 i/s
313
+ jbuilder: 4.6 i/s - 9.34x slower
314
+ ```
315
+
316
+
317
+ ### Summary
318
+
319
+ According to the benchmark results, you can expect 2-30x performance improvement in development env, and 3-10x performance improvement in production env.
320
+
321
+
30
322
  ## Contributing
31
323
 
32
324
  Pull requests are welcome on GitHub at https://github.com/amatsuda/jb.
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+
3
+ cd $(dirname "$0")/../test/dummy_app
4
+
5
+ BUNDLE_GEMFILE=../../Gemfile bundle e rails r bin/bench.rb
@@ -0,0 +1,34 @@
1
+ require 'rails/generators/named_base'
2
+ require 'rails/generators/resource_helpers'
3
+
4
+ module Rails
5
+ module Generators
6
+ class JbGenerator < NamedBase # :nodoc:
7
+ include Rails::Generators::ResourceHelpers
8
+
9
+ source_root File.expand_path('../templates', __FILE__)
10
+
11
+ argument :attributes, type: :array, default: [], banner: 'field:type field:type'
12
+
13
+ def create_root_folder
14
+ path = File.join('app/views', controller_file_path)
15
+ empty_directory path unless File.directory?(path)
16
+ end
17
+
18
+ def copy_view_files
19
+ template 'index.json.jb', File.join('app/views', controller_file_path, 'index.json.jb')
20
+ template 'show.json.jb', File.join('app/views', controller_file_path, 'show.json.jb')
21
+ end
22
+
23
+
24
+ private
25
+ def attributes_names
26
+ [:id] + super
27
+ end
28
+
29
+ def attributes_names_with_timestamps
30
+ attributes_names + %w(created_at updated_at)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator'
3
+
4
+ module Rails
5
+ module Generators
6
+ class ScaffoldControllerGenerator
7
+ hook_for :jb, default: true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ @<%= plural_table_name %>.map do |<%= singular_table_name %>|
2
+ {
3
+ <% attributes_names.each do |attr| -%>
4
+ <%= attr %>: <%= singular_table_name %>.<%= attr %>,
5
+ <% end -%>
6
+ url: <%= singular_table_name %>_url(<%= singular_table_name %>, format: :json)
7
+ }
8
+ end
@@ -0,0 +1,3 @@
1
+ {
2
+ <%= attributes_names_with_timestamps.map {|attr| "#{attr}: @#{singular_table_name}.#{attr}"}.join(",\n ") %>
3
+ }
@@ -7,5 +7,11 @@ module Jb
7
7
  ::ActionView::Template.register_template_handler :jb, Jb::Handler
8
8
  end
9
9
  end
10
+
11
+ generators do |app|
12
+ Rails::Generators.configure! app.config.generators
13
+ Rails::Generators.hidden_namespaces.uniq!
14
+ require_relative '../generators/rails/scaffold_controller_generator'
15
+ end
10
16
  end
11
17
  end
@@ -1,3 +1,3 @@
1
1
  module Jb
2
- VERSION = "0.1.1"
2
+ VERSION = '0.2.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Akira Matsuda
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-07-04 00:00:00.000000000 Z
11
+ date: 2016-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -93,9 +93,14 @@ files:
93
93
  - LICENSE.txt
94
94
  - README.md
95
95
  - Rakefile
96
+ - bin/benchmark.sh
96
97
  - bin/console
97
98
  - bin/setup
98
99
  - jb.gemspec
100
+ - lib/generators/rails/jb_generator.rb
101
+ - lib/generators/rails/scaffold_controller_generator.rb
102
+ - lib/generators/rails/templates/index.json.jb
103
+ - lib/generators/rails/templates/show.json.jb
99
104
  - lib/jb.rb
100
105
  - lib/jb/action_view_monkeys.rb
101
106
  - lib/jb/handler.rb
@@ -121,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
126
  version: '0'
122
127
  requirements: []
123
128
  rubyforge_project:
124
- rubygems_version: 2.5.1
129
+ rubygems_version: 2.6.4
125
130
  signing_key:
126
131
  specification_version: 4
127
132
  summary: Faster and simpler Jbuilder alternative