jbuilder 2.2.16 → 2.6.4

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.
@@ -1 +1 @@
1
- json.extract! @<%= singular_table_name %>, <%= attributes_list_with_timestamps %>
1
+ json.partial! "<%= plural_table_name %>/<%= singular_table_name %>", <%= singular_table_name %>: @<%= singular_table_name %>
@@ -14,4 +14,11 @@ class Jbuilder
14
14
  new(message)
15
15
  end
16
16
  end
17
+
18
+ class MergeError < ::StandardError
19
+ def self.build(current_value, updates)
20
+ message = "Can't merge #{updates.inspect} into #{current_value.inspect}"
21
+ new(message)
22
+ end
23
+ end
17
24
  end
@@ -11,37 +11,15 @@ class JbuilderTemplate < Jbuilder
11
11
 
12
12
  def initialize(context, *args)
13
13
  @context = context
14
+ @cached_root = nil
14
15
  super(*args)
15
16
  end
16
17
 
17
- def partial!(name_or_options, locals = {})
18
- case name_or_options
19
- when ::Hash
20
- # partial! partial: 'name', foo: 'bar'
21
- options = name_or_options
22
- else
23
- # partial! 'name', locals: {foo: 'bar'}
24
- if locals.one? && (locals.keys.first == :locals)
25
- options = locals.merge(partial: name_or_options)
26
- else
27
- options = { partial: name_or_options, locals: locals }
28
- end
29
- # partial! 'name', foo: 'bar'
30
- as = locals.delete(:as)
31
- options[:as] = as if as.present?
32
- options[:collection] = locals[:collection] if locals.key?(:collection)
33
- end
34
-
35
- _render_partial_with_options options
36
- end
37
-
38
- def array!(collection = [], *attributes)
39
- options = attributes.extract_options!
40
-
41
- if options.key?(:partial)
42
- partial! options[:partial], options.merge(collection: collection)
18
+ def partial!(*args)
19
+ if args.one? && _is_active_model?(args.first)
20
+ _render_active_model_partial args.first
43
21
  else
44
- super
22
+ _render_explicit_partial(*args)
45
23
  end
46
24
  end
47
25
 
@@ -55,7 +33,7 @@ class JbuilderTemplate < Jbuilder
55
33
  # end
56
34
  def cache!(key=nil, options={})
57
35
  if @context.controller.perform_caching
58
- value = ::Rails.cache.fetch(_cache_key(key, options), options) do
36
+ value = _cache_fragment_for(key, options) do
59
37
  _scope { yield self }
60
38
  end
61
39
 
@@ -65,7 +43,28 @@ class JbuilderTemplate < Jbuilder
65
43
  end
66
44
  end
67
45
 
68
- # Conditionally catches the json depending in the condition given as first parameter. Has the same
46
+ # Caches the json structure at the root using a string rather than the hash structure. This is considerably
47
+ # faster, but the drawback is that it only works, as the name hints, at the root. So you cannot
48
+ # use this approach to cache deeper inside the hierarchy, like in partials or such. Continue to use #cache! there.
49
+ #
50
+ # Example:
51
+ #
52
+ # json.cache_root! @person do
53
+ # json.extract! @person, :name, :age
54
+ # end
55
+ #
56
+ # # json.extra 'This will not work either, the root must be exclusive'
57
+ def cache_root!(key=nil, options={})
58
+ if @context.controller.perform_caching
59
+ raise "cache_root! can't be used after JSON structures have been defined" if @attributes.present?
60
+
61
+ @cached_root = _cache_fragment_for([ :root, key ], options) { yield; target! }
62
+ else
63
+ yield
64
+ end
65
+ end
66
+
67
+ # Conditionally caches the json depending in the condition given as first parameter. Has the same
69
68
  # signature as the `cache` helper method in `ActionView::Helpers::CacheHelper` and so can be used in
70
69
  # the same way.
71
70
  #
@@ -78,7 +77,31 @@ class JbuilderTemplate < Jbuilder
78
77
  condition ? cache!(*args, &::Proc.new) : yield
79
78
  end
80
79
 
81
- protected
80
+ def target!
81
+ @cached_root || super
82
+ end
83
+
84
+ def array!(collection = [], *args)
85
+ options = args.first
86
+
87
+ if args.one? && _partial_options?(options)
88
+ partial! options.merge(collection: collection)
89
+ else
90
+ super
91
+ end
92
+ end
93
+
94
+ def set!(name, object = BLANK, *args)
95
+ options = args.first
96
+
97
+ if args.one? && _partial_options?(options)
98
+ _set_inline_partial name, object, options
99
+ else
100
+ super
101
+ end
102
+ end
103
+
104
+ private
82
105
 
83
106
  def _render_partial_with_options(options)
84
107
  options.reverse_merge! locals: {}
@@ -105,14 +128,38 @@ class JbuilderTemplate < Jbuilder
105
128
  @context.render options
106
129
  end
107
130
 
131
+ def _cache_fragment_for(key, options, &block)
132
+ key = _cache_key(key, options)
133
+ _read_fragment_cache(key, options) || _write_fragment_cache(key, options, &block)
134
+ end
135
+
136
+ def _read_fragment_cache(key, options = nil)
137
+ @context.controller.instrument_fragment_cache :read_fragment, key do
138
+ ::Rails.cache.read(key, options)
139
+ end
140
+ end
141
+
142
+ def _write_fragment_cache(key, options = nil)
143
+ @context.controller.instrument_fragment_cache :write_fragment, key do
144
+ yield.tap do |value|
145
+ ::Rails.cache.write(key, value, options)
146
+ end
147
+ end
148
+ end
149
+
108
150
  def _cache_key(key, options)
109
- key = _fragment_name_with_digest(key, options)
110
- key = url_for(key).split('://', 2).last if ::Hash === key
151
+ name_options = options.slice(:skip_digest, :virtual_path)
152
+ key = _fragment_name_with_digest(key, name_options)
153
+
154
+ if @context.respond_to?(:fragment_cache_key)
155
+ key = @context.fragment_cache_key(key)
156
+ else
157
+ key = url_for(key).split('://', 2).last if ::Hash === key
158
+ end
159
+
111
160
  ::ActiveSupport::Cache.expand_cache_key(key, :jbuilder)
112
161
  end
113
162
 
114
- private
115
-
116
163
  def _fragment_name_with_digest(key, options)
117
164
  if @context.respond_to?(:cache_fragment_name)
118
165
  # Current compatibility, fragment_name_with_digest is private again and cache_fragment_name
@@ -126,16 +173,56 @@ class JbuilderTemplate < Jbuilder
126
173
  end
127
174
  end
128
175
 
129
- def _mapable_arguments?(value, *args)
130
- return true if super
131
- options = args.last
132
- ::Hash === options && options.key?(:as)
176
+ def _partial_options?(options)
177
+ ::Hash === options && options.key?(:as) && options.key?(:partial)
178
+ end
179
+
180
+ def _is_active_model?(object)
181
+ object.class.respond_to?(:model_name) && object.respond_to?(:to_partial_path)
182
+ end
183
+
184
+ def _set_inline_partial(name, object, options)
185
+ value = if object.nil?
186
+ []
187
+ elsif _is_collection?(object)
188
+ _scope{ _render_partial_with_options options.merge(collection: object) }
189
+ else
190
+ locals = ::Hash[options[:as], object]
191
+ _scope{ _render_partial options.merge(locals: locals) }
192
+ end
193
+
194
+ set! name, value
195
+ end
196
+
197
+ def _render_explicit_partial(name_or_options, locals = {})
198
+ case name_or_options
199
+ when ::Hash
200
+ # partial! partial: 'name', foo: 'bar'
201
+ options = name_or_options
202
+ else
203
+ # partial! 'name', locals: {foo: 'bar'}
204
+ if locals.one? && (locals.keys.first == :locals)
205
+ options = locals.merge(partial: name_or_options)
206
+ else
207
+ options = { partial: name_or_options, locals: locals }
208
+ end
209
+ # partial! 'name', foo: 'bar'
210
+ as = locals.delete(:as)
211
+ options[:as] = as if as.present?
212
+ options[:collection] = locals[:collection] if locals.key?(:collection)
213
+ end
214
+
215
+ _render_partial_with_options options
216
+ end
217
+
218
+ def _render_active_model_partial(object)
219
+ @context.render object, json: self
133
220
  end
134
221
  end
135
222
 
136
223
  class JbuilderHandler
137
224
  cattr_accessor :default_format
138
- self.default_format = Mime::JSON
225
+ self.default_format = Mime[:json]
139
226
 
140
227
  def self.call(template)
141
228
  # this juggling is required to keep line numbers right in the error
@@ -11,8 +11,8 @@ class Jbuilder
11
11
  args.each do |name|
12
12
  @format[name] = []
13
13
  end
14
- options.each do |name, paramaters|
15
- @format[name] = paramaters
14
+ options.each do |name, parameters|
15
+ @format[name] = parameters
16
16
  end
17
17
  end
18
18
 
@@ -3,15 +3,24 @@ require 'jbuilder/jbuilder_template'
3
3
 
4
4
  class Jbuilder
5
5
  class Railtie < ::Rails::Railtie
6
- initializer :jbuilder do |app|
6
+ initializer :jbuilder do
7
7
  ActiveSupport.on_load :action_view do
8
8
  ActionView::Template.register_template_handler :jbuilder, JbuilderHandler
9
9
  require 'jbuilder/dependency_tracker'
10
10
  end
11
11
 
12
- if app.config.respond_to?(:api_only) && app.config.api_only
12
+ if Rails::VERSION::MAJOR >= 5
13
+ module ::ActionController
14
+ module ApiRendering
15
+ include ActionView::Rendering
16
+ end
17
+ end
18
+
13
19
  ActiveSupport.on_load :action_controller do
14
- include ActionView::Rendering
20
+ if self == ActionController::API
21
+ include ActionController::Helpers
22
+ include ActionController::ImplicitRender
23
+ end
15
24
  end
16
25
  end
17
26
  end
data/lib/jbuilder.rb CHANGED
@@ -3,6 +3,7 @@ require 'jbuilder/blank'
3
3
  require 'jbuilder/key_formatter'
4
4
  require 'jbuilder/errors'
5
5
  require 'multi_json'
6
+ require 'ostruct'
6
7
 
7
8
  class Jbuilder
8
9
  @@key_formatter = nil
@@ -23,6 +24,7 @@ class Jbuilder
23
24
  end
24
25
 
25
26
  BLANK = Blank.new
27
+ NON_ENUMERABLES = [ ::Struct, ::OpenStruct ].to_set
26
28
 
27
29
  def set!(key, value = BLANK, *args)
28
30
  result = if ::Kernel.block_given?
@@ -46,7 +48,7 @@ class Jbuilder
46
48
  # { "age": 32 }
47
49
  value
48
50
  end
49
- elsif _mapable_arguments?(value, *args)
51
+ elsif _is_collection?(value)
50
52
  # json.comments @post.comments, :content, :created_at
51
53
  # { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
52
54
  _scope{ array! value, *args }
@@ -59,8 +61,13 @@ class Jbuilder
59
61
  _set_value key, result
60
62
  end
61
63
 
62
- alias_method :method_missing, :set!
63
- private :method_missing
64
+ def method_missing(*args)
65
+ if ::Kernel.block_given?
66
+ set!(*args, &::Proc.new)
67
+ else
68
+ set!(*args)
69
+ end
70
+ end
64
71
 
65
72
  # Specifies formatting to be applied to the key. Passing in a name of a function
66
73
  # will cause that function to be called on the key. So :upcase will upper case
@@ -263,14 +270,14 @@ class Jbuilder
263
270
  def _merge_values(current_value, updates)
264
271
  if _blank?(updates)
265
272
  current_value
266
- elsif _blank?(current_value) || updates.nil?
273
+ elsif _blank?(current_value) || updates.nil? || current_value.empty? && ::Array === updates
267
274
  updates
268
- elsif ::Array === updates
269
- ::Array === current_value ? current_value + updates : updates
270
- elsif ::Hash === current_value
275
+ elsif ::Array === current_value && ::Array === updates
276
+ current_value + updates
277
+ elsif ::Hash === current_value && ::Hash === updates
271
278
  current_value.merge(updates)
272
279
  else
273
- raise "Can't merge #{updates.inspect} with #{current_value.inspect}"
280
+ raise MergeError.build(current_value, updates)
274
281
  end
275
282
  end
276
283
 
@@ -301,13 +308,17 @@ class Jbuilder
301
308
  @attributes, @key_formatter = parent_attributes, parent_formatter
302
309
  end
303
310
 
304
- def _mapable_arguments?(value, *args)
305
- value.respond_to?(:map)
311
+ def _is_collection?(object)
312
+ _object_respond_to?(object, :map, :count) && NON_ENUMERABLES.none?{ |klass| klass === object }
306
313
  end
307
314
 
308
315
  def _blank?(value=@attributes)
309
316
  BLANK == value
310
317
  end
318
+
319
+ def _object_respond_to?(object, *methods)
320
+ methods.all?{ |m| object.respond_to?(m) }
321
+ end
311
322
  end
312
323
 
313
324
  require 'jbuilder/railtie' if defined?(Rails)
@@ -14,19 +14,25 @@ class JbuilderGeneratorTest < Rails::Generators::TestCase
14
14
  %w(index show).each do |view|
15
15
  assert_file "app/views/posts/#{view}.json.jbuilder"
16
16
  end
17
+ assert_file "app/views/posts/_post.json.jbuilder"
17
18
  end
18
19
 
19
20
  test 'index content' do
20
21
  run_generator
21
22
 
22
23
  assert_file 'app/views/posts/index.json.jbuilder' do |content|
23
- assert_match /json\.array!\(@posts\) do \|post\|/, content
24
- assert_match /json\.extract! post, :id, :title, :body/, content
25
- assert_match /json\.url post_url\(post, format: :json\)/, content
24
+ assert_match %r{json.array! @posts, partial: 'posts/post', as: :post}, content
26
25
  end
27
26
 
28
27
  assert_file 'app/views/posts/show.json.jbuilder' do |content|
29
- assert_match /json\.extract! @post, :id, :title, :body, :created_at, :updated_at/, content
28
+ assert_match %r{json.partial! \"posts/post\", post: @post}, content
30
29
  end
30
+
31
+ assert_file 'app/views/posts/_post.json.jbuilder' do |content|
32
+ assert_match %r{json\.extract! post, :id, :title, :body}, content
33
+ assert_match %r{json\.url post_url\(post, format: :json\)}, content
34
+ end
35
+
36
+
31
37
  end
32
38
  end