jbuilder 2.7.0 → 2.11.2

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.
@@ -7,6 +7,12 @@ module Rails
7
7
  source_paths << File.expand_path('../templates', __FILE__)
8
8
 
9
9
  hook_for :jbuilder, type: :boolean, default: true
10
+
11
+ private
12
+
13
+ def permitted_params
14
+ attributes_names.map { |name| ":#{name}" }.join(', ')
15
+ end unless private_method_defined? :permitted_params
10
16
  end
11
17
  end
12
18
  end
@@ -1,10 +1,10 @@
1
1
  <% if namespaced? -%>
2
- require_dependency "<%= namespaced_file_path %>/application_controller"
2
+ require_dependency "<%= namespaced_path %>/application_controller"
3
3
 
4
4
  <% end -%>
5
5
  <% module_namespacing do -%>
6
6
  class <%= controller_class_name %>Controller < ApplicationController
7
- before_action :set_<%= singular_table_name %>, only: [:show, :update, :destroy]
7
+ before_action :set_<%= singular_table_name %>, only: %i[ show update destroy ]
8
8
 
9
9
  # GET <%= route_url %>
10
10
  # GET <%= route_url %>.json
@@ -51,12 +51,12 @@ class <%= controller_class_name %>Controller < ApplicationController
51
51
  @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
52
52
  end
53
53
 
54
- # Never trust parameters from the scary internet, only allow the white list through.
54
+ # Only allow a list of trusted parameters through.
55
55
  def <%= "#{singular_table_name}_params" %>
56
56
  <%- if attributes_names.empty? -%>
57
57
  params.fetch(<%= ":#{singular_table_name}" %>, {})
58
58
  <%- else -%>
59
- params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
59
+ params.require(<%= ":#{singular_table_name}" %>).permit(<%= permitted_params %>)
60
60
  <%- end -%>
61
61
  end
62
62
  end
@@ -1,19 +1,17 @@
1
1
  <% if namespaced? -%>
2
- require_dependency "<%= namespaced_file_path %>/application_controller"
2
+ require_dependency "<%= namespaced_path %>/application_controller"
3
3
 
4
4
  <% end -%>
5
5
  <% module_namespacing do -%>
6
6
  class <%= controller_class_name %>Controller < ApplicationController
7
- before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy]
7
+ before_action :set_<%= singular_table_name %>, only: %i[ show edit update destroy ]
8
8
 
9
- # GET <%= route_url %>
10
- # GET <%= route_url %>.json
9
+ # GET <%= route_url %> or <%= route_url %>.json
11
10
  def index
12
11
  @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
13
12
  end
14
13
 
15
- # GET <%= route_url %>/1
16
- # GET <%= route_url %>/1.json
14
+ # GET <%= route_url %>/1 or <%= route_url %>/1.json
17
15
  def show
18
16
  end
19
17
 
@@ -26,42 +24,39 @@ class <%= controller_class_name %>Controller < ApplicationController
26
24
  def edit
27
25
  end
28
26
 
29
- # POST <%= route_url %>
30
- # POST <%= route_url %>.json
27
+ # POST <%= route_url %> or <%= route_url %>.json
31
28
  def create
32
29
  @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
33
30
 
34
31
  respond_to do |format|
35
32
  if @<%= orm_instance.save %>
36
- format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %> }
33
+ format.html { redirect_to @<%= singular_table_name %>, notice: <%= %("#{human_name} was successfully created.") %> }
37
34
  format.json { render :show, status: :created, location: <%= "@#{singular_table_name}" %> }
38
35
  else
39
- format.html { render :new }
36
+ format.html { render :new, status: :unprocessable_entity }
40
37
  format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
41
38
  end
42
39
  end
43
40
  end
44
41
 
45
- # PATCH/PUT <%= route_url %>/1
46
- # PATCH/PUT <%= route_url %>/1.json
42
+ # PATCH/PUT <%= route_url %>/1 or <%= route_url %>/1.json
47
43
  def update
48
44
  respond_to do |format|
49
45
  if @<%= orm_instance.update("#{singular_table_name}_params") %>
50
- format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> }
46
+ format.html { redirect_to @<%= singular_table_name %>, notice: <%= %("#{human_name} was successfully updated.") %> }
51
47
  format.json { render :show, status: :ok, location: <%= "@#{singular_table_name}" %> }
52
48
  else
53
- format.html { render :edit }
49
+ format.html { render :edit, status: :unprocessable_entity }
54
50
  format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
55
51
  end
56
52
  end
57
53
  end
58
54
 
59
- # DELETE <%= route_url %>/1
60
- # DELETE <%= route_url %>/1.json
55
+ # DELETE <%= route_url %>/1 or <%= route_url %>/1.json
61
56
  def destroy
62
57
  @<%= orm_instance.destroy %>
63
58
  respond_to do |format|
64
- format.html { redirect_to <%= index_helper %>_url, notice: <%= "'#{human_name} was successfully destroyed.'" %> }
59
+ format.html { redirect_to <%= index_helper %>_url, notice: <%= %("#{human_name} was successfully destroyed.") %> }
65
60
  format.json { head :no_content }
66
61
  end
67
62
  end
@@ -72,12 +67,12 @@ class <%= controller_class_name %>Controller < ApplicationController
72
67
  @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
73
68
  end
74
69
 
75
- # Never trust parameters from the scary internet, only allow the white list through.
70
+ # Only allow a list of trusted parameters through.
76
71
  def <%= "#{singular_table_name}_params" %>
77
72
  <%- if attributes_names.empty? -%>
78
73
  params.fetch(<%= ":#{singular_table_name}" %>, {})
79
74
  <%- else -%>
80
- params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
75
+ params.require(<%= ":#{singular_table_name}" %>).permit(<%= permitted_params %>)
81
76
  <%- end -%>
82
77
  end
83
78
  end
@@ -1 +1 @@
1
- json.array! @<%= plural_table_name %>, partial: '<%= plural_table_name %>/<%= singular_table_name %>', as: :<%= singular_table_name %>
1
+ json.array! @<%= plural_table_name %>, partial: "<%= plural_table_name %>/<%= singular_table_name %>", as: :<%= singular_table_name %>
@@ -1,2 +1,16 @@
1
- json.extract! <%= singular_table_name %>, <%= attributes_list_with_timestamps %>
1
+ json.extract! <%= singular_table_name %>, <%= full_attributes_list %>
2
2
  json.url <%= singular_table_name %>_url(<%= singular_table_name %>, format: :json)
3
+ <%- virtual_attributes.each do |attribute| -%>
4
+ <%- if attribute.type == :rich_text -%>
5
+ json.<%= attribute.name %> <%= singular_table_name %>.<%= attribute.name %>.to_s
6
+ <%- elsif attribute.type == :attachment -%>
7
+ json.<%= attribute.name %> url_for(<%= singular_table_name %>.<%= attribute.name %>)
8
+ <%- elsif attribute.type == :attachments -%>
9
+ json.<%= attribute.name %> do
10
+ json.array!(<%= singular_table_name %>.<%= attribute.name %>) do |<%= attribute.singular_name %>|
11
+ json.id <%= attribute.singular_name %>.id
12
+ json.url url_for(<%= attribute.singular_name %>)
13
+ end
14
+ end
15
+ <%- end -%>
16
+ <%- end -%>
data/lib/jbuilder.rb CHANGED
@@ -2,18 +2,21 @@ require 'jbuilder/jbuilder'
2
2
  require 'jbuilder/blank'
3
3
  require 'jbuilder/key_formatter'
4
4
  require 'jbuilder/errors'
5
- require 'multi_json'
5
+ require 'json'
6
6
  require 'ostruct'
7
+ require 'active_support/core_ext/hash/deep_merge'
7
8
 
8
9
  class Jbuilder
9
10
  @@key_formatter = nil
10
11
  @@ignore_nil = false
12
+ @@deep_format_keys = false
11
13
 
12
14
  def initialize(options = {})
13
15
  @attributes = {}
14
16
 
15
17
  @key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil}
16
18
  @ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
19
+ @deep_format_keys = options.fetch(:deep_format_keys, @@deep_format_keys)
17
20
 
18
21
  yield self if ::Kernel.block_given?
19
22
  end
@@ -26,12 +29,12 @@ class Jbuilder
26
29
  BLANK = Blank.new
27
30
  NON_ENUMERABLES = [ ::Struct, ::OpenStruct ].to_set
28
31
 
29
- def set!(key, value = BLANK, *args)
32
+ def set!(key, value = BLANK, *args, &block)
30
33
  result = if ::Kernel.block_given?
31
34
  if !_blank?(value)
32
35
  # json.comments @post.comments { |comment| ... }
33
36
  # { "comments": [ { ... }, { ... } ] }
34
- _scope{ array! value, &::Proc.new }
37
+ _scope{ array! value, &block }
35
38
  else
36
39
  # json.comments { ... }
37
40
  # { "comments": ... }
@@ -42,11 +45,11 @@ class Jbuilder
42
45
  # json.age 32
43
46
  # json.person another_jbuilder
44
47
  # { "age": 32, "person": { ... }
45
- value.attributes!
48
+ _format_keys(value.attributes!)
46
49
  else
47
50
  # json.age 32
48
51
  # { "age": 32 }
49
- value
52
+ _format_keys(value)
50
53
  end
51
54
  elsif _is_collection?(value)
52
55
  # json.comments @post.comments, :content, :created_at
@@ -61,9 +64,9 @@ class Jbuilder
61
64
  _set_value key, result
62
65
  end
63
66
 
64
- def method_missing(*args)
67
+ def method_missing(*args, &block)
65
68
  if ::Kernel.block_given?
66
- set!(*args, &::Proc.new)
69
+ set!(*args, &block)
67
70
  else
68
71
  set!(*args)
69
72
  end
@@ -130,6 +133,31 @@ class Jbuilder
130
133
  @@ignore_nil = value
131
134
  end
132
135
 
136
+ # Deeply apply key format to nested hashes and arrays passed to
137
+ # methods like set!, merge! or array!.
138
+ #
139
+ # Example:
140
+ #
141
+ # json.key_format! camelize: :lower
142
+ # json.settings({some_value: "abc"})
143
+ #
144
+ # { "settings": { "some_value": "abc" }}
145
+ #
146
+ # json.key_format! camelize: :lower
147
+ # json.deep_format_keys!
148
+ # json.settings({some_value: "abc"})
149
+ #
150
+ # { "settings": { "someValue": "abc" }}
151
+ #
152
+ def deep_format_keys!(value = true)
153
+ @deep_format_keys = value
154
+ end
155
+
156
+ # Same as instance method deep_format_keys! except sets the default.
157
+ def self.deep_format_keys(value = true)
158
+ @@deep_format_keys = value
159
+ end
160
+
133
161
  # Turns the current element into an array and yields a builder to add a hash.
134
162
  #
135
163
  # Example:
@@ -163,7 +191,7 @@ class Jbuilder
163
191
  #
164
192
  # [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ]
165
193
  #
166
- # If you are using Ruby 1.9+, you can use the call syntax instead of an explicit extract! call:
194
+ # You can use the call syntax instead of an explicit extract! call:
167
195
  #
168
196
  # json.(@people) { |person| ... }
169
197
  #
@@ -181,18 +209,18 @@ class Jbuilder
181
209
  # json.array! [1, 2, 3]
182
210
  #
183
211
  # [1,2,3]
184
- def array!(collection = [], *attributes)
212
+ def array!(collection = [], *attributes, &block)
185
213
  array = if collection.nil?
186
214
  []
187
215
  elsif ::Kernel.block_given?
188
- _map_collection(collection, &::Proc.new)
216
+ _map_collection(collection, &block)
189
217
  elsif attributes.any?
190
218
  _map_collection(collection) { |element| extract! element, *attributes }
191
219
  else
192
- collection.to_a
220
+ _format_keys(collection.to_a)
193
221
  end
194
222
 
195
- merge! array
223
+ @attributes = _merge_values(@attributes, array)
196
224
  end
197
225
 
198
226
  # Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON.
@@ -220,9 +248,9 @@ class Jbuilder
220
248
  end
221
249
  end
222
250
 
223
- def call(object, *attributes)
251
+ def call(object, *attributes, &block)
224
252
  if ::Kernel.block_given?
225
- array! object, &::Proc.new
253
+ array! object, &block
226
254
  else
227
255
  extract! object, *attributes
228
256
  end
@@ -240,24 +268,25 @@ class Jbuilder
240
268
  @attributes
241
269
  end
242
270
 
243
- # Merges hash or array into current builder.
244
- def merge!(hash_or_array)
245
- @attributes = _merge_values(@attributes, hash_or_array)
271
+ # Merges hash, array, or Jbuilder instance into current builder.
272
+ def merge!(object)
273
+ hash_or_array = ::Jbuilder === object ? object.attributes! : object
274
+ @attributes = _merge_values(@attributes, _format_keys(hash_or_array))
246
275
  end
247
276
 
248
277
  # Encodes the current builder as JSON.
249
278
  def target!
250
- ::MultiJson.dump(@attributes)
279
+ @attributes.to_json
251
280
  end
252
281
 
253
282
  private
254
283
 
255
284
  def _extract_hash_values(object, attributes)
256
- attributes.each{ |key| _set_value key, object.fetch(key) }
285
+ attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
257
286
  end
258
287
 
259
288
  def _extract_method_values(object, attributes)
260
- attributes.each{ |key| _set_value key, object.public_send(key) }
289
+ attributes.each{ |key| _set_value key, _format_keys(object.public_send(key)) }
261
290
  end
262
291
 
263
292
  def _merge_block(key)
@@ -275,7 +304,7 @@ class Jbuilder
275
304
  elsif ::Array === current_value && ::Array === updates
276
305
  current_value + updates
277
306
  elsif ::Hash === current_value && ::Hash === updates
278
- current_value.merge(updates)
307
+ current_value.deep_merge(updates)
279
308
  else
280
309
  raise MergeError.build(current_value, updates)
281
310
  end
@@ -285,6 +314,18 @@ class Jbuilder
285
314
  @key_formatter ? @key_formatter.format(key) : key.to_s
286
315
  end
287
316
 
317
+ def _format_keys(hash_or_array)
318
+ return hash_or_array unless @deep_format_keys
319
+
320
+ if ::Array === hash_or_array
321
+ hash_or_array.map { |value| _format_keys(value) }
322
+ elsif ::Hash === hash_or_array
323
+ ::Hash[hash_or_array.collect { |k, v| [_key(k), _format_keys(v)] }]
324
+ else
325
+ hash_or_array
326
+ end
327
+ end
328
+
288
329
  def _set_value(key, value)
289
330
  raise NullError.build(key) if @attributes.nil?
290
331
  raise ArrayError.build(key) if ::Array === @attributes
@@ -300,12 +341,12 @@ class Jbuilder
300
341
  end
301
342
 
302
343
  def _scope
303
- parent_attributes, parent_formatter = @attributes, @key_formatter
344
+ parent_attributes, parent_formatter, parent_deep_format_keys = @attributes, @key_formatter, @deep_format_keys
304
345
  @attributes = BLANK
305
346
  yield
306
347
  @attributes
307
348
  ensure
308
- @attributes, @key_formatter = parent_attributes, parent_formatter
349
+ @attributes, @key_formatter, @deep_format_keys = parent_attributes, parent_formatter, parent_deep_format_keys
309
350
  end
310
351
 
311
352
  def _is_collection?(object)
@@ -73,8 +73,8 @@ class JbuilderTemplate < Jbuilder
73
73
  # json.cache_if! !admin?, @person, expires_in: 10.minutes do
74
74
  # json.extract! @person, :name, :age
75
75
  # end
76
- def cache_if!(condition, *args)
77
- condition ? cache!(*args, &::Proc.new) : yield
76
+ def cache_if!(condition, *args, &block)
77
+ condition ? cache!(*args, &block) : yield
78
78
  end
79
79
 
80
80
  def target!
@@ -104,7 +104,7 @@ class JbuilderTemplate < Jbuilder
104
104
  private
105
105
 
106
106
  def _render_partial_with_options(options)
107
- options.reverse_merge! locals: {}
107
+ options.reverse_merge! locals: options.except(:partial, :as, :collection)
108
108
  options.reverse_merge! ::JbuilderTemplate.template_lookup_options
109
109
  as = options[:as]
110
110
 
@@ -151,8 +151,8 @@ class JbuilderTemplate < Jbuilder
151
151
  name_options = options.slice(:skip_digest, :virtual_path)
152
152
  key = _fragment_name_with_digest(key, name_options)
153
153
 
154
- if @context.respond_to?(:fragment_cache_key)
155
- key = @context.fragment_cache_key(key)
154
+ if @context.respond_to?(:combined_fragment_cache_key)
155
+ key = @context.combined_fragment_cache_key(key)
156
156
  else
157
157
  key = url_for(key).split('://', 2).last if ::Hash === key
158
158
  end
@@ -164,7 +164,7 @@ class JbuilderTemplate < Jbuilder
164
164
  if @context.respond_to?(:cache_fragment_name)
165
165
  # Current compatibility, fragment_name_with_digest is private again and cache_fragment_name
166
166
  # should be used instead.
167
- @context.cache_fragment_name(key, options)
167
+ @context.cache_fragment_name(key, **options)
168
168
  elsif @context.respond_to?(:fragment_name_with_digest)
169
169
  # Backwards compatibility for period of time when fragment_name_with_digest was made public.
170
170
  @context.fragment_name_with_digest(key)
@@ -222,11 +222,12 @@ end
222
222
 
223
223
  class JbuilderHandler
224
224
  cattr_accessor :default_format
225
- self.default_format = Mime[:json]
225
+ self.default_format = :json
226
226
 
227
- def self.call(template)
227
+ def self.call(template, source = nil)
228
+ source ||= template.source
228
229
  # this juggling is required to keep line numbers right in the error
229
- %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{template.source}
230
+ %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{source}
230
231
  json.target! unless (__already_defined && __already_defined != "method")}
231
232
  end
232
233
  end
@@ -53,7 +53,7 @@ class JbuilderDependencyTrackerTest < ActiveSupport::TestCase
53
53
  assert_equal %w[path/to/partial], dependencies
54
54
  end
55
55
 
56
- test 'detects partial in indirect collecton calls' do
56
+ test 'detects partial in indirect collection calls' do
57
57
  dependencies = track_dependencies <<-RUBY
58
58
  json.comments @post.comments, partial: 'comments/comment', as: :comment
59
59
  RUBY
@@ -21,18 +21,38 @@ class JbuilderGeneratorTest < Rails::Generators::TestCase
21
21
  run_generator
22
22
 
23
23
  assert_file 'app/views/posts/index.json.jbuilder' do |content|
24
- assert_match %r{json.array! @posts, partial: 'posts/post', as: :post}, content
24
+ assert_match %r{json\.array! @posts, partial: "posts/post", as: :post}, content
25
25
  end
26
26
 
27
27
  assert_file 'app/views/posts/show.json.jbuilder' do |content|
28
- assert_match %r{json.partial! \"posts/post\", post: @post}, content
28
+ assert_match %r{json\.partial! "posts/post", post: @post}, content
29
29
  end
30
-
31
- assert_file 'app/views/posts/_post.json.jbuilder' do |content|
30
+
31
+ assert_file 'app/views/posts/_post.json.jbuilder' do |content|
32
32
  assert_match %r{json\.extract! post, :id, :title, :body}, content
33
+ assert_match %r{:created_at, :updated_at}, content
33
34
  assert_match %r{json\.url post_url\(post, format: :json\)}, content
34
35
  end
35
-
36
+ end
37
+
38
+ test 'timestamps are not generated in partial with --no-timestamps' do
39
+ run_generator %w(Post title body:text --no-timestamps)
40
+
41
+ assert_file 'app/views/posts/_post.json.jbuilder' do |content|
42
+ assert_match %r{json\.extract! post, :id, :title, :body$}, content
43
+ assert_no_match %r{:created_at, :updated_at}, content
44
+ end
45
+ end
36
46
 
47
+ if Rails::VERSION::MAJOR >= 6
48
+ test 'handles virtual attributes' do
49
+ run_generator %w(Message content:rich_text video:attachment photos:attachments)
50
+
51
+ assert_file 'app/views/messages/_message.json.jbuilder' do |content|
52
+ assert_match %r{json\.content message\.content\.to_s}, content
53
+ assert_match %r{json\.video url_for\(message\.video\)}, content
54
+ assert_match %r{json\.photos do\n json\.array!\(message\.photos\) do \|photo\|\n json\.id photo\.id\n json\.url url_for\(photo\)\n end\nend}, content
55
+ end
56
+ end
37
57
  end
38
58
  end