jbuilder 2.2.16 → 2.6.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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