jb 0.1.1 → 0.2.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.
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