jbuilder 2.7.0 → 2.13.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.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +49 -0
  3. data/.gitignore +3 -0
  4. data/Appraisals +10 -14
  5. data/CONTRIBUTING.md +5 -13
  6. data/Gemfile +0 -1
  7. data/MIT-LICENSE +1 -1
  8. data/README.md +103 -19
  9. data/Rakefile +1 -1
  10. data/bin/release +14 -0
  11. data/bin/test +6 -0
  12. data/gemfiles/{rails_4_2.gemfile → rails_7_0.gemfile} +1 -4
  13. data/gemfiles/{rails_5_0.gemfile → rails_7_1.gemfile} +1 -4
  14. data/gemfiles/{rails_5_1.gemfile → rails_head.gemfile} +1 -4
  15. data/jbuilder.gemspec +20 -4
  16. data/lib/generators/rails/jbuilder_generator.rb +16 -2
  17. data/lib/generators/rails/scaffold_controller_generator.rb +6 -0
  18. data/lib/generators/rails/templates/api_controller.rb +10 -4
  19. data/lib/generators/rails/templates/controller.rb +21 -19
  20. data/lib/generators/rails/templates/index.json.jbuilder +1 -1
  21. data/lib/generators/rails/templates/partial.json.jbuilder +15 -1
  22. data/lib/generators/rails/templates/show.json.jbuilder +1 -1
  23. data/lib/jbuilder/collection_renderer.rb +116 -0
  24. data/lib/jbuilder/jbuilder.rb +1 -7
  25. data/lib/jbuilder/jbuilder_dependency_tracker.rb +73 -0
  26. data/lib/jbuilder/jbuilder_template.rb +80 -22
  27. data/lib/jbuilder/railtie.rb +3 -3
  28. data/lib/jbuilder/version.rb +3 -0
  29. data/lib/jbuilder.rb +71 -30
  30. data/test/jbuilder_dependency_tracker_test.rb +3 -4
  31. data/test/jbuilder_generator_test.rb +37 -5
  32. data/test/jbuilder_template_test.rb +300 -324
  33. data/test/jbuilder_test.rb +229 -4
  34. data/test/scaffold_api_controller_generator_test.rb +31 -3
  35. data/test/scaffold_controller_generator_test.rb +54 -8
  36. data/test/test_helper.rb +41 -8
  37. metadata +28 -21
  38. data/.travis.yml +0 -66
  39. data/CHANGELOG.md +0 -249
  40. data/lib/jbuilder/dependency_tracker.rb +0 -61
@@ -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 -%>
@@ -1 +1 @@
1
- json.partial! "<%= plural_table_name %>/<%= singular_table_name %>", <%= singular_table_name %>: @<%= singular_table_name %>
1
+ json.partial! "<%= partial_path_name %>", <%= singular_table_name %>: @<%= singular_table_name %>
@@ -0,0 +1,116 @@
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
+
110
+ class EnumerableCompat < ::SimpleDelegator
111
+ # Rails 6.1 requires this.
112
+ def size(*args, &block)
113
+ __getobj__.count(*args, &block)
114
+ end
115
+ end
116
+ end
@@ -1,7 +1 @@
1
- Jbuilder = Class.new(begin
2
- require 'active_support/proxy_object'
3
- ActiveSupport::ProxyObject
4
- rescue LoadError
5
- require 'active_support/basic_object'
6
- ActiveSupport::BasicObject
7
- end)
1
+ Jbuilder = Class.new(BasicObject)
@@ -0,0 +1,73 @@
1
+ class Jbuilder::DependencyTracker
2
+ EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
3
+
4
+ # Matches:
5
+ # json.partial! "messages/message"
6
+ # json.partial!('messages/message')
7
+ #
8
+ DIRECT_RENDERS = /
9
+ \w+\.partial! # json.partial!
10
+ \(?\s* # optional parenthesis
11
+ (['"])([^'"]+)\1 # quoted value
12
+ /x
13
+
14
+ # Matches:
15
+ # json.partial! partial: "comments/comment"
16
+ # json.comments @post.comments, partial: "comments/comment", as: :comment
17
+ # json.array! @posts, partial: "posts/post", as: :post
18
+ # = render partial: "account"
19
+ #
20
+ INDIRECT_RENDERS = /
21
+ (?::partial\s*=>|partial:) # partial: or :partial =>
22
+ \s* # optional whitespace
23
+ (['"])([^'"]+)\1 # quoted value
24
+ /x
25
+
26
+ def self.call(name, template, view_paths = nil)
27
+ new(name, template, view_paths).dependencies
28
+ end
29
+
30
+ def initialize(name, template, view_paths = nil)
31
+ @name, @template, @view_paths = name, template, view_paths
32
+ end
33
+
34
+ def dependencies
35
+ direct_dependencies + indirect_dependencies + explicit_dependencies
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :name, :template
41
+
42
+ def direct_dependencies
43
+ source.scan(DIRECT_RENDERS).map(&:second)
44
+ end
45
+
46
+ def indirect_dependencies
47
+ source.scan(INDIRECT_RENDERS).map(&:second)
48
+ end
49
+
50
+ def explicit_dependencies
51
+ dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
52
+
53
+ wildcards, explicits = dependencies.partition { |dependency| dependency.end_with?("/*") }
54
+
55
+ (explicits + resolve_directories(wildcards)).uniq
56
+ end
57
+
58
+ def resolve_directories(wildcard_dependencies)
59
+ return [] unless @view_paths
60
+ return [] if wildcard_dependencies.empty?
61
+
62
+ # Remove trailing "/*"
63
+ prefixes = wildcard_dependencies.map { |query| query[0..-3] }
64
+
65
+ @view_paths.flat_map(&:all_template_paths).uniq.filter_map { |path|
66
+ path.to_s if prefixes.include?(path.prefix)
67
+ }.sort
68
+ end
69
+
70
+ def source
71
+ template.source
72
+ end
73
+ 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
@@ -56,7 +89,7 @@ class JbuilderTemplate < Jbuilder
56
89
  # # json.extra 'This will not work either, the root must be exclusive'
57
90
  def cache_root!(key=nil, options={})
58
91
  if @context.controller.perform_caching
59
- raise "cache_root! can't be used after JSON structures have been defined" if @attributes.present?
92
+ ::Kernel.raise "cache_root! can't be used after JSON structures have been defined" if @attributes.present?
60
93
 
61
94
  @cached_root = _cache_fragment_for([ :root, key ], options) { yield; target! }
62
95
  else
@@ -73,8 +106,8 @@ class JbuilderTemplate < Jbuilder
73
106
  # json.cache_if! !admin?, @person, expires_in: 10.minutes do
74
107
  # json.extract! @person, :name, :age
75
108
  # end
76
- def cache_if!(condition, *args)
77
- condition ? cache!(*args, &::Proc.new) : yield
109
+ def cache_if!(condition, *args, &block)
110
+ condition ? cache!(*args, &block) : yield
78
111
  end
79
112
 
80
113
  def target!
@@ -104,19 +137,48 @@ class JbuilderTemplate < Jbuilder
104
137
  private
105
138
 
106
139
  def _render_partial_with_options(options)
107
- options.reverse_merge! locals: {}
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
+ collection = EnumerableCompat.new(collection) if collection.respond_to?(:count) && !collection.respond_to?(:size)
149
+
150
+ if options.has_key?(:layout)
151
+ ::Kernel.raise ::NotImplementedError, "The `:layout' option is not supported in collection rendering."
152
+ end
153
+
154
+ if options.has_key?(:spacer_template)
155
+ ::Kernel.raise ::NotImplementedError, "The `:spacer_template' option is not supported in collection rendering."
156
+ end
157
+
158
+ if collection.present?
159
+ results = CollectionRenderer
160
+ .new(@context.lookup_context, options) { |&block| _scope(&block) }
161
+ .render_collection_with_partial(collection, partial, @context, nil)
162
+
163
+ array! if results.respond_to?(:body) && results.body.nil?
164
+ else
165
+ array!
166
+ end
167
+ elsif as && options.key?(:collection) && !CollectionRenderer.supported?
168
+ # For Rails <= 5.2:
112
169
  as = as.to_sym
113
170
  collection = options.delete(:collection)
114
- locals = options.delete(:locals)
115
- array! collection do |member|
116
- member_locals = locals.clone
117
- member_locals.merge! collection: collection
118
- member_locals.merge! as => member
119
- _render_partial options.merge(locals: member_locals)
171
+
172
+ if collection.present?
173
+ locals = options.delete(:locals)
174
+ array! collection do |member|
175
+ member_locals = locals.clone
176
+ member_locals.merge! collection: collection
177
+ member_locals.merge! as => member
178
+ _render_partial options.merge(locals: member_locals)
179
+ end
180
+ else
181
+ array!
120
182
  end
121
183
  else
122
184
  _render_partial options
@@ -151,8 +213,8 @@ class JbuilderTemplate < Jbuilder
151
213
  name_options = options.slice(:skip_digest, :virtual_path)
152
214
  key = _fragment_name_with_digest(key, name_options)
153
215
 
154
- if @context.respond_to?(:fragment_cache_key)
155
- key = @context.fragment_cache_key(key)
216
+ if @context.respond_to?(:combined_fragment_cache_key)
217
+ key = @context.combined_fragment_cache_key(key)
156
218
  else
157
219
  key = url_for(key).split('://', 2).last if ::Hash === key
158
220
  end
@@ -162,12 +224,7 @@ class JbuilderTemplate < Jbuilder
162
224
 
163
225
  def _fragment_name_with_digest(key, options)
164
226
  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
- @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)
227
+ @context.cache_fragment_name(key, **options)
171
228
  else
172
229
  key
173
230
  end
@@ -222,11 +279,12 @@ end
222
279
 
223
280
  class JbuilderHandler
224
281
  cattr_accessor :default_format
225
- self.default_format = Mime[:json]
282
+ self.default_format = :json
226
283
 
227
- def self.call(template)
284
+ def self.call(template, source = nil)
285
+ source ||= template.source
228
286
  # this juggling is required to keep line numbers right in the error
229
- %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{template.source}
287
+ %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{source}
230
288
  json.target! unless (__already_defined && __already_defined != "method")}
231
289
  end
232
290
  end
@@ -1,4 +1,4 @@
1
- require 'rails/railtie'
1
+ require 'rails'
2
2
  require 'jbuilder/jbuilder_template'
3
3
 
4
4
  class Jbuilder
@@ -6,7 +6,7 @@ class Jbuilder
6
6
  initializer :jbuilder do
7
7
  ActiveSupport.on_load :action_view do
8
8
  ActionView::Template.register_template_handler :jbuilder, JbuilderHandler
9
- require 'jbuilder/dependency_tracker'
9
+ require 'jbuilder/jbuilder_dependency_tracker'
10
10
  end
11
11
 
12
12
  if Rails::VERSION::MAJOR >= 5
@@ -17,7 +17,7 @@ class Jbuilder
17
17
  end
18
18
 
19
19
  ActiveSupport.on_load :action_controller do
20
- if self == ActionController::API
20
+ if name == 'ActionController::API'
21
21
  include ActionController::Helpers
22
22
  include ActionController::ImplicitRender
23
23
  end
@@ -0,0 +1,3 @@
1
+ class Jbuilder
2
+ VERSION = "2.13.0"
3
+ end
data/lib/jbuilder.rb CHANGED
@@ -1,19 +1,23 @@
1
+ require 'active_support'
1
2
  require 'jbuilder/jbuilder'
2
3
  require 'jbuilder/blank'
3
4
  require 'jbuilder/key_formatter'
4
5
  require 'jbuilder/errors'
5
- require 'multi_json'
6
- require 'ostruct'
6
+ require 'jbuilder/version'
7
+ require 'json'
8
+ require 'active_support/core_ext/hash/deep_merge'
7
9
 
8
10
  class Jbuilder
9
11
  @@key_formatter = nil
10
12
  @@ignore_nil = false
13
+ @@deep_format_keys = false
11
14
 
12
15
  def initialize(options = {})
13
16
  @attributes = {}
14
17
 
15
18
  @key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil}
16
19
  @ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
20
+ @deep_format_keys = options.fetch(:deep_format_keys, @@deep_format_keys)
17
21
 
18
22
  yield self if ::Kernel.block_given?
19
23
  end
@@ -24,14 +28,13 @@ class Jbuilder
24
28
  end
25
29
 
26
30
  BLANK = Blank.new
27
- 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,29 +268,30 @@ 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)
264
293
  current_value = _blank? ? BLANK : @attributes.fetch(_key(key), BLANK)
265
- raise NullError.build(key) if current_value.nil?
294
+ ::Kernel.raise NullError.build(key) if current_value.nil?
266
295
  new_value = _scope{ yield self }
267
296
  _merge_values(current_value, new_value)
268
297
  end
@@ -275,9 +304,9 @@ 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
- raise MergeError.build(current_value, updates)
309
+ ::Kernel.raise MergeError.build(current_value, updates)
281
310
  end
282
311
  end
283
312
 
@@ -285,9 +314,21 @@ 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
- raise NullError.build(key) if @attributes.nil?
290
- raise ArrayError.build(key) if ::Array === @attributes
330
+ ::Kernel.raise NullError.build(key) if @attributes.nil?
331
+ ::Kernel.raise ArrayError.build(key) if ::Array === @attributes
291
332
  return if @ignore_nil && value.nil? or _blank?(value)
292
333
  @attributes = {} if _blank?
293
334
  @attributes[_key(key)] = value
@@ -300,16 +341,16 @@ 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)
312
- _object_respond_to?(object, :map, :count) && NON_ENUMERABLES.none?{ |klass| klass === object }
353
+ _object_respond_to?(object, :map, :count) && !(::Struct === object)
313
354
  end
314
355
 
315
356
  def _blank?(value=@attributes)
@@ -1,6 +1,5 @@
1
1
  require 'test_helper'
2
- require 'jbuilder/dependency_tracker'
3
-
2
+ require 'jbuilder/jbuilder_dependency_tracker'
4
3
 
5
4
  class FakeTemplate
6
5
  attr_reader :source, :handler
@@ -53,7 +52,7 @@ class JbuilderDependencyTrackerTest < ActiveSupport::TestCase
53
52
  assert_equal %w[path/to/partial], dependencies
54
53
  end
55
54
 
56
- test 'detects partial in indirect collecton calls' do
55
+ test 'detects partial in indirect collection calls' do
57
56
  dependencies = track_dependencies <<-RUBY
58
57
  json.comments @post.comments, partial: 'comments/comment', as: :comment
59
58
  RUBY
@@ -61,7 +60,7 @@ class JbuilderDependencyTrackerTest < ActiveSupport::TestCase
61
60
  assert_equal %w[comments/comment], dependencies
62
61
  end
63
62
 
64
- test 'detects explicit depedency' do
63
+ test 'detects explicit dependency' do
65
64
  dependencies = track_dependencies <<-RUBY
66
65
  # Template Dependency: path/to/partial
67
66
  json.foo 'bar'