jbuilder 2.11.1 → 2.11.5

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
  SHA256:
3
- metadata.gz: 3e3eebf3fe3849616361ae7df9556f466eaf84da813756b8690dfbd5c28d41a1
4
- data.tar.gz: 011c0162b09a94f229673e88bab0d0d466d9552a004afb426f817a02c3af8943
3
+ metadata.gz: 9ce8fbeac8b34ee290535e98a284a1247e2de1b5272c4679c1b89a89ff8149db
4
+ data.tar.gz: cdaeb1302c4755ca43f037328698afe29fe658f2094c99e2c6f81f0000b9368c
5
5
  SHA512:
6
- metadata.gz: 29968e2f35da52dc00db229d0085d77a7d390417bbe3011b005824ff45a1291bca0f45c59d4f40263a3ac236e31ea3fdd0cd78ba01ddef4e51414aea4ac34704
7
- data.tar.gz: '09df7fb01ce2d128b0f45116cc4f2ccb01469031a51b598f6c17c16f58ddbd0c3059095a1a6761a2f26e328b3960def55752d6ed36cca0e2952e393d712e48b3'
6
+ metadata.gz: 6903b1aa13786fd5501d8e667eb7a913ebd469d24ce183a24cdf949a62788b1e598c2e0678c5ed5e369d4232c7720e284850f735c5be6f4691e4ecf42256d21a
7
+ data.tar.gz: 2a40e415a25a4d8680ac00ba81b9984188ef35440869a357db4fd7184d3f0b36d0ce10545a0d5253e6d12374b2f1d51f5a96f1ded1370ec7eddecaee469afc61
@@ -0,0 +1,108 @@
1
+ name: Ruby test
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ name: Ruby ${{ matrix.ruby }} (${{ matrix.gemfile }})
8
+ runs-on: ubuntu-20.04
9
+ continue-on-error: ${{ matrix.experimental }}
10
+ env:
11
+ BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
12
+ BUNDLE_JOBS: 4
13
+ BUNDLE_RETRY: 3
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ ruby:
18
+ - "2.2"
19
+ - "2.3"
20
+ - "2.4"
21
+ - "2.5"
22
+ - "2.6"
23
+ - "2.7"
24
+ - "3.0"
25
+
26
+ gemfile:
27
+ - "rails_5_0"
28
+ - "rails_5_1"
29
+ - "rails_5_2"
30
+ - "rails_6_0"
31
+ - "rails_6_1"
32
+ - "rails_head"
33
+
34
+ experimental: [false]
35
+ exclude:
36
+ - ruby: 2.7
37
+ gemfile: rails_5_0
38
+ - ruby: '3.0'
39
+ gemfile: rails_5_0
40
+ - ruby: head
41
+ gemfile: rails_5_0
42
+ - ruby: 2.7
43
+ gemfile: rails_5_1
44
+ - ruby: '3.0'
45
+ gemfile: rails_5_1
46
+ - ruby: head
47
+ gemfile: rails_5_1
48
+ - ruby: 2.2
49
+ gemfile: rails_5_2
50
+ - ruby: 2.7
51
+ gemfile: rails_5_2
52
+ - ruby: '3.0'
53
+ gemfile: rails_5_2
54
+ - ruby: head
55
+ gemfile: rails_5_2
56
+ - ruby: 2.2
57
+ gemfile: rails_6_0
58
+ - ruby: 2.3
59
+ gemfile: rails_6_0
60
+ - ruby: 2.4
61
+ gemfile: rails_6_0
62
+ - ruby: '3.0'
63
+ gemfile: rails_6_0
64
+ - ruby: head
65
+ gemfile: rails_6_0
66
+ - ruby: 2.2
67
+ gemfile: rails_6_1
68
+ - ruby: 2.3
69
+ gemfile: rails_6_1
70
+ - ruby: 2.4
71
+ gemfile: rails_6_1
72
+ - ruby: 2.2
73
+ gemfile: rails_head
74
+ - ruby: 2.3
75
+ gemfile: rails_head
76
+ - ruby: 2.4
77
+ gemfile: rails_head
78
+ - ruby: 2.5
79
+ gemfile: rails_head
80
+ - ruby: 2.6
81
+ gemfile: rails_head
82
+ - ruby: 2.7
83
+ gemfile: rails_head
84
+ experimental: false
85
+ - ruby: '3.0'
86
+ gemfile: rails_head
87
+ experimental: false
88
+ include:
89
+ - ruby: 2.7
90
+ gemfile: rails_head
91
+ experimental: true
92
+ - ruby: '3.0'
93
+ gemfile: rails_head
94
+ experimental: true
95
+ - ruby: head
96
+ gemfile: rails_head
97
+ experimental: true
98
+
99
+ steps:
100
+ - uses: actions/checkout@v2
101
+
102
+ - uses: ruby/setup-ruby@v1
103
+ with:
104
+ ruby-version: ${{ matrix.ruby }}
105
+ bundler-cache: true
106
+
107
+ - name: Ruby test
108
+ run: bundle exec rake
data/Appraisals CHANGED
@@ -15,7 +15,11 @@ if RUBY_VERSION >= "2.5.0"
15
15
  gem "rails", "~> 6.0.0"
16
16
  end
17
17
 
18
+ appraise "rails-6-1" do
19
+ gem "rails", "~> 6.1.0"
20
+ end
21
+
18
22
  appraise "rails-head" do
19
- gem "rails", github: "rails/rails"
23
+ gem "rails", github: "rails/rails", branch: "main"
20
24
  end
21
25
  end
data/CONTRIBUTING.md CHANGED
@@ -1,11 +1,11 @@
1
1
  Contributing to Jbuilder
2
2
  =====================
3
3
 
4
- [![Build Status](https://api.travis-ci.org/rails/jbuilder.svg?branch=master)][travis]
4
+ [![Build Status](https://github.com/rails/jbuilder/workflows/Ruby%20test/badge.svg)][test]
5
5
  [![Gem Version](https://badge.fury.io/rb/jbuilder.svg)][gem]
6
6
  [![Code Climate](https://codeclimate.com/github/rails/jbuilder/badges/gpa.svg)][codeclimate]
7
7
 
8
- [travis]: https://travis-ci.org/rails/jbuilder
8
+ [test]: https://github.com/rails/jbuilder/actions?query=branch%3Amaster
9
9
  [gem]: https://rubygems.org/gems/jbuilder
10
10
  [codeclimate]: https://codeclimate.com/github/rails/jbuilder
11
11
 
@@ -95,7 +95,7 @@ git push origin my-feature-branch -f
95
95
 
96
96
  #### Check on Your Pull Request
97
97
 
98
- Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above.
98
+ Go back to your pull request after a few minutes and see whether it passed muster with GitHub Actions. Everything should look green, otherwise fix issues and amend your commit as described above.
99
99
 
100
100
  #### Be Patient
101
101
 
data/README.md CHANGED
@@ -108,6 +108,33 @@ json.array! @people, :id, :name
108
108
  # => [ { "id": 1, "name": "David" }, { "id": 2, "name": "Jamie" } ]
109
109
  ```
110
110
 
111
+ To make a plain array without keys, construct and pass in a standard Ruby array.
112
+
113
+ ```ruby
114
+ my_array = %w(David Jamie)
115
+
116
+ json.people my_array
117
+
118
+ # => "people": [ "David", "Jamie" ]
119
+ ```
120
+
121
+ You don't always have or need a collection when building an array.
122
+
123
+ ```ruby
124
+ json.people do
125
+ json.child! do
126
+ json.id 1
127
+ json.name 'David'
128
+ end
129
+ json.child! do
130
+ json.id 2
131
+ json.name 'Jamie'
132
+ end
133
+ end
134
+
135
+ # => { "people": [ { "id": 1, "name": "David" }, { "id": 2, "name": "Jamie" } ] }
136
+ ```
137
+
111
138
  Jbuilder objects can be directly nested inside each other. Useful for composing objects.
112
139
 
113
140
  ``` ruby
@@ -137,7 +164,7 @@ company.to_builder.target!
137
164
  ```
138
165
 
139
166
  You can either use Jbuilder stand-alone or directly as an ActionView template
140
- language. When required in Rails, you can create views a la show.json.jbuilder
167
+ language. When required in Rails, you can create views à la show.json.jbuilder
141
168
  (the json is already yielded):
142
169
 
143
170
  ``` ruby
@@ -171,19 +198,19 @@ It's also possible to render collections of partials:
171
198
  json.array! @posts, partial: 'posts/post', as: :post
172
199
 
173
200
  # or
174
-
175
201
  json.partial! 'posts/post', collection: @posts, as: :post
176
202
 
177
203
  # or
178
-
179
204
  json.partial! partial: 'posts/post', collection: @posts, as: :post
180
205
 
181
206
  # or
182
-
183
207
  json.comments @post.comments, partial: 'comments/comment', as: :comment
184
208
  ```
185
209
 
186
- The `as: :some_symbol` is used with partials. It will take care of mapping the passed in object to a variable for the partial. If the value is a collection (either implicitly or explicitly by using the `collection:` option, then each value of the collection is passed to the partial as the variable `some_symbol`. If the value is a singular object, then the object is passed to the partial as the variable `some_symbol`.
210
+ The `as: :some_symbol` is used with partials. It will take care of mapping the passed in object to a variable for the
211
+ partial. If the value is a collection either implicitly or explicitly by using the `collection:` option, then each
212
+ value of the collection is passed to the partial as the variable `some_symbol`. If the value is a singular object,
213
+ then the object is passed to the partial as the variable `some_symbol`.
187
214
 
188
215
  Be sure not to confuse the `as:` option to mean nesting of the partial. For example:
189
216
 
@@ -193,7 +220,7 @@ Be sure not to confuse the `as:` option to mean nesting of the partial. For exam
193
220
  json.partial! @comment, as: :comment
194
221
  ```
195
222
 
196
- is quite different than:
223
+ is quite different from:
197
224
 
198
225
  ```ruby
199
226
  # comment attributes are nested under a "comment" property
@@ -236,6 +263,8 @@ json.bar "bar"
236
263
  # => { "bar": "bar" }
237
264
  ```
238
265
 
266
+ ## Caching
267
+
239
268
  Fragment caching is supported, it uses `Rails.cache` and works like caching in
240
269
  HTML templates:
241
270
 
@@ -253,9 +282,25 @@ json.cache_if! !admin?, ['v1', @person], expires_in: 10.minutes do
253
282
  end
254
283
  ```
255
284
 
256
- If you are rendering fragments for a collection of objects, have a look at
257
- `jbuilder_cache_multi` gem. It uses fetch_multi (>= Rails 4.1) to fetch
258
- multiple keys at once.
285
+ Aside from that, the `:cached` options on collection rendering is available on Rails >= 6.0. This will cache the
286
+ rendered results effectively using the multi fetch feature.
287
+
288
+ ```ruby
289
+ json.array! @posts, partial: "posts/post", as: :post, cached: true
290
+
291
+ # or:
292
+ json.comments @post.comments, partial: "comments/comment", as: :comment, cached: true
293
+ ```
294
+
295
+ If your collection cache depends on multiple sources (try to avoid this to keep things simple), you can name all these dependencies as part of a block that returns an array:
296
+
297
+ ```ruby
298
+ json.array! @posts, partial: "posts/post", as: :post, cached: -> post { [post, current_user] }
299
+ ```
300
+
301
+ This will include both records as part of the cache key and updating either of them will expire the cache.
302
+
303
+ ## Formatting Keys
259
304
 
260
305
  Keys can be auto formatted using `key_format!`, this can be used to convert
261
306
  keynames from the standard ruby_format to camelCase:
@@ -274,6 +319,25 @@ environment.rb for example):
274
319
  Jbuilder.key_format camelize: :lower
275
320
  ```
276
321
 
322
+ By default, key format is not applied to keys of hashes that are
323
+ passed to methods like `set!`, `array!` or `merge!`. You can opt into
324
+ deeply transforming these as well:
325
+
326
+ ``` ruby
327
+ json.key_format! camelize: :lower
328
+ json.deep_format_keys!
329
+ json.settings([{some_value: "abc"}])
330
+
331
+ # => { "settings": [{ "someValue": "abc" }]}
332
+ ```
333
+
334
+ You can set this globally with the class method `deep_format_keys` (from inside your
335
+ environment.rb for example):
336
+
337
+ ``` ruby
338
+ Jbuilder.deep_format_keys true
339
+ ```
340
+
277
341
  ## Contributing to Jbuilder
278
342
 
279
343
  Jbuilder is the work of many contributors. You're encouraged to submit pull requests, propose
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require "bundler/setup"
2
2
  require "bundler/gem_tasks"
3
3
  require "rake/testtask"
4
4
 
5
- if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"]
5
+ if !ENV["APPRAISAL_INITIALIZED"] && !ENV["CI"]
6
6
  require "appraisal/task"
7
7
  Appraisal::Task.new
8
8
  task default: :appraisal
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake"
6
+ gem "mocha", require: false
7
+ gem "appraisal"
8
+ gem "rails", "~> 6.1.0"
9
+
10
+ gemspec path: "../"
@@ -5,6 +5,6 @@ source "https://rubygems.org"
5
5
  gem "rake"
6
6
  gem "mocha", require: false
7
7
  gem "appraisal"
8
- gem "rails", github: "rails/rails"
8
+ gem "rails", github: "rails/rails", branch: "main"
9
9
 
10
10
  gemspec path: "../"
data/jbuilder.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'jbuilder'
3
- s.version = '2.11.1'
3
+ s.version = '2.11.5'
4
4
  s.authors = 'David Heinemeier Hansson'
5
5
  s.email = 'david@basecamp.com'
6
6
  s.summary = 'Create JSON structures via a Builder-style DSL'
@@ -10,6 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.required_ruby_version = '>= 2.2.2'
11
11
 
12
12
  s.add_dependency 'activesupport', '>= 5.0.0'
13
+ s.add_dependency 'actionview', '>= 5.0.0'
13
14
 
14
15
  if RUBY_ENGINE == 'rbx'
15
16
  s.add_development_dependency('racc')
@@ -19,4 +20,12 @@ Gem::Specification.new do |s|
19
20
 
20
21
  s.files = `git ls-files`.split("\n")
21
22
  s.test_files = `git ls-files -- test/*`.split("\n")
23
+
24
+ s.metadata = {
25
+ "bug_tracker_uri" => "https://github.com/rails/jbuilder/issues",
26
+ "changelog_uri" => "https://github.com/rails/jbuilder/releases/tag/v#{s.version}",
27
+ "mailing_list_uri" => "https://discuss.rubyonrails.org/c/rubyonrails-talk",
28
+ "source_code_uri" => "https://github.com/rails/jbuilder/tree/v#{s.version}",
29
+ "rubygems_mfa_required" => "true",
30
+ }
22
31
  end
@@ -30,7 +30,7 @@ class <%= controller_class_name %>Controller < ApplicationController
30
30
 
31
31
  respond_to do |format|
32
32
  if @<%= orm_instance.save %>
33
- format.html { redirect_to @<%= singular_table_name %>, notice: <%= %("#{human_name} was successfully created.") %> }
33
+ format.html { redirect_to <%= show_helper %>, notice: <%= %("#{human_name} was successfully created.") %> }
34
34
  format.json { render :show, status: :created, location: <%= "@#{singular_table_name}" %> }
35
35
  else
36
36
  format.html { render :new, status: :unprocessable_entity }
@@ -43,7 +43,7 @@ class <%= controller_class_name %>Controller < ApplicationController
43
43
  def update
44
44
  respond_to do |format|
45
45
  if @<%= orm_instance.update("#{singular_table_name}_params") %>
46
- format.html { redirect_to @<%= singular_table_name %>, notice: <%= %("#{human_name} was successfully updated.") %> }
46
+ format.html { redirect_to <%= show_helper %>, notice: <%= %("#{human_name} was successfully updated.") %> }
47
47
  format.json { render :show, status: :ok, location: <%= "@#{singular_table_name}" %> }
48
48
  else
49
49
  format.html { render :edit, status: :unprocessable_entity }
@@ -55,6 +55,7 @@ class <%= controller_class_name %>Controller < ApplicationController
55
55
  # DELETE <%= route_url %>/1 or <%= route_url %>/1.json
56
56
  def destroy
57
57
  @<%= orm_instance.destroy %>
58
+
58
59
  respond_to do |format|
59
60
  format.html { redirect_to <%= index_helper %>_url, notice: <%= %("#{human_name} was successfully destroyed.") %> }
60
61
  format.json { head :no_content }
@@ -0,0 +1,109 @@
1
+ require 'delegate'
2
+ require 'active_support/concern'
3
+ require 'action_view'
4
+
5
+ begin
6
+ require 'action_view/renderer/collection_renderer'
7
+ rescue LoadError
8
+ require 'action_view/renderer/partial_renderer'
9
+ end
10
+
11
+ class Jbuilder
12
+ module CollectionRenderable # :nodoc:
13
+ extend ActiveSupport::Concern
14
+
15
+ class_methods do
16
+ def supported?
17
+ superclass.private_method_defined?(:build_rendered_template) && self.superclass.private_method_defined?(:build_rendered_collection)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def build_rendered_template(content, template, layout = nil)
24
+ super(content || json.attributes!, template)
25
+ end
26
+
27
+ def build_rendered_collection(templates, _spacer)
28
+ json.merge!(templates.map(&:body))
29
+ end
30
+
31
+ def json
32
+ @options[:locals].fetch(:json)
33
+ end
34
+
35
+ class ScopedIterator < ::SimpleDelegator # :nodoc:
36
+ include Enumerable
37
+
38
+ def initialize(obj, scope)
39
+ super(obj)
40
+ @scope = scope
41
+ end
42
+
43
+ # Rails 6.0 support:
44
+ def each
45
+ return enum_for(:each) unless block_given?
46
+
47
+ __getobj__.each do |object|
48
+ @scope.call { yield(object) }
49
+ end
50
+ end
51
+
52
+ # Rails 6.1 support:
53
+ def each_with_info
54
+ return enum_for(:each_with_info) unless block_given?
55
+
56
+ __getobj__.each_with_info do |object, info|
57
+ @scope.call { yield(object, info) }
58
+ end
59
+ end
60
+ end
61
+
62
+ private_constant :ScopedIterator
63
+ end
64
+
65
+ if defined?(::ActionView::CollectionRenderer)
66
+ # Rails 6.1 support:
67
+ class CollectionRenderer < ::ActionView::CollectionRenderer # :nodoc:
68
+ include CollectionRenderable
69
+
70
+ def initialize(lookup_context, options, &scope)
71
+ super(lookup_context, options)
72
+ @scope = scope
73
+ end
74
+
75
+ private
76
+ def collection_with_template(view, template, layout, collection)
77
+ super(view, template, layout, ScopedIterator.new(collection, @scope))
78
+ end
79
+ end
80
+ else
81
+ # Rails 6.0 support:
82
+ class CollectionRenderer < ::ActionView::PartialRenderer # :nodoc:
83
+ include CollectionRenderable
84
+
85
+ def initialize(lookup_context, options, &scope)
86
+ super(lookup_context)
87
+ @options = options
88
+ @scope = scope
89
+ end
90
+
91
+ def render_collection_with_partial(collection, partial, context, block)
92
+ render(context, @options.merge(collection: collection, partial: partial), block)
93
+ end
94
+
95
+ private
96
+ def collection_without_template(view)
97
+ @collection = ScopedIterator.new(@collection, @scope)
98
+
99
+ super(view)
100
+ end
101
+
102
+ def collection_with_template(view, template)
103
+ @collection = ScopedIterator.new(@collection, @scope)
104
+
105
+ super(view, template)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -1,4 +1,5 @@
1
1
  require 'jbuilder/jbuilder'
2
+ require 'jbuilder/collection_renderer'
2
3
  require 'action_dispatch/http/mime_type'
3
4
  require 'active_support/cache'
4
5
 
@@ -15,6 +16,38 @@ class JbuilderTemplate < Jbuilder
15
16
  super(*args)
16
17
  end
17
18
 
19
+ # Generates JSON using the template specified with the `:partial` option. For example, the code below will render
20
+ # the file `views/comments/_comments.json.jbuilder`, and set a local variable comments with all this message's
21
+ # comments, which can be used inside the partial.
22
+ #
23
+ # Example:
24
+ #
25
+ # json.partial! 'comments/comments', comments: @message.comments
26
+ #
27
+ # There are multiple ways to generate a collection of elements as JSON, as ilustrated below:
28
+ #
29
+ # Example:
30
+ #
31
+ # json.array! @posts, partial: 'posts/post', as: :post
32
+ #
33
+ # # or:
34
+ # json.partial! 'posts/post', collection: @posts, as: :post
35
+ #
36
+ # # or:
37
+ # json.partial! partial: 'posts/post', collection: @posts, as: :post
38
+ #
39
+ # # or:
40
+ # json.comments @post.comments, partial: 'comments/comment', as: :comment
41
+ #
42
+ # Aside from that, the `:cached` options is available on Rails >= 6.0. This will cache the rendered results
43
+ # effectively using the multi fetch feature.
44
+ #
45
+ # Example:
46
+ #
47
+ # json.array! @posts, partial: "posts/post", as: :post, cached: true
48
+ #
49
+ # json.comments @post.comments, partial: "comments/comment", as: :comment, cached: true
50
+ #
18
51
  def partial!(*args)
19
52
  if args.one? && _is_active_model?(args.first)
20
53
  _render_active_model_partial args.first
@@ -104,11 +137,30 @@ class JbuilderTemplate < Jbuilder
104
137
  private
105
138
 
106
139
  def _render_partial_with_options(options)
107
- options.reverse_merge! locals: options.except(:partial, :as, :collection)
140
+ options.reverse_merge! locals: options.except(:partial, :as, :collection, :cached)
108
141
  options.reverse_merge! ::JbuilderTemplate.template_lookup_options
109
142
  as = options[:as]
110
143
 
111
- if as && options.key?(:collection)
144
+ if as && options.key?(:collection) && CollectionRenderer.supported?
145
+ collection = options.delete(:collection) || []
146
+ partial = options.delete(:partial)
147
+ options[:locals].merge!(json: self)
148
+
149
+ if options.has_key?(:layout)
150
+ raise ::NotImplementedError, "The `:layout' option is not supported in collection rendering."
151
+ end
152
+
153
+ if options.has_key?(:spacer_template)
154
+ raise ::NotImplementedError, "The `:spacer_template' option is not supported in collection rendering."
155
+ end
156
+
157
+ results = CollectionRenderer
158
+ .new(@context.lookup_context, options) { |&block| _scope(&block) }
159
+ .render_collection_with_partial(collection, partial, @context, nil)
160
+
161
+ array! if results.respond_to?(:body) && results.body.nil?
162
+ elsif as && options.key?(:collection) && !CollectionRenderer.supported?
163
+ # For Rails <= 5.2:
112
164
  as = as.to_sym
113
165
  collection = options.delete(:collection)
114
166
  locals = options.delete(:locals)
@@ -162,12 +214,7 @@ class JbuilderTemplate < Jbuilder
162
214
 
163
215
  def _fragment_name_with_digest(key, options)
164
216
  if @context.respond_to?(:cache_fragment_name)
165
- # Current compatibility, fragment_name_with_digest is private again and cache_fragment_name
166
- # should be used instead.
167
217
  @context.cache_fragment_name(key, **options)
168
- elsif @context.respond_to?(:fragment_name_with_digest)
169
- # Backwards compatibility for period of time when fragment_name_with_digest was made public.
170
- @context.fragment_name_with_digest(key)
171
218
  else
172
219
  key
173
220
  end
@@ -1,4 +1,4 @@
1
- require 'rails/railtie'
1
+ require 'rails'
2
2
  require 'jbuilder/jbuilder_template'
3
3
 
4
4
  class Jbuilder