jbuilder 1.5.3 → 2.11.5
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.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +108 -0
- data/.gitignore +4 -1
- data/Appraisals +25 -0
- data/CONTRIBUTING.md +106 -0
- data/Gemfile +4 -5
- data/MIT-LICENSE +2 -2
- data/README.md +182 -40
- data/Rakefile +15 -10
- data/gemfiles/rails_5_0.gemfile +10 -0
- data/gemfiles/rails_5_1.gemfile +10 -0
- data/gemfiles/rails_5_2.gemfile +10 -0
- data/gemfiles/rails_6_0.gemfile +10 -0
- data/gemfiles/rails_6_1.gemfile +10 -0
- data/gemfiles/rails_head.gemfile +10 -0
- data/jbuilder.gemspec +25 -8
- data/lib/generators/rails/jbuilder_generator.rb +13 -2
- data/lib/generators/rails/scaffold_controller_generator.rb +9 -3
- data/lib/generators/rails/templates/api_controller.rb +63 -0
- data/lib/generators/rails/templates/controller.rb +18 -22
- data/lib/generators/rails/templates/index.json.jbuilder +1 -4
- data/lib/generators/rails/templates/partial.json.jbuilder +16 -0
- data/lib/generators/rails/templates/show.json.jbuilder +1 -1
- data/lib/jbuilder/blank.rb +11 -0
- data/lib/jbuilder/collection_renderer.rb +109 -0
- data/lib/jbuilder/dependency_tracker.rb +61 -0
- data/lib/jbuilder/errors.rb +24 -0
- data/lib/jbuilder/jbuilder.rb +7 -0
- data/lib/jbuilder/jbuilder_template.rb +228 -67
- data/lib/jbuilder/key_formatter.rb +34 -0
- data/lib/jbuilder/railtie.rb +31 -6
- data/lib/jbuilder.rb +144 -137
- data/test/jbuilder_dependency_tracker_test.rb +72 -0
- data/test/jbuilder_generator_test.rb +31 -4
- data/test/jbuilder_template_test.rb +319 -163
- data/test/jbuilder_test.rb +613 -298
- data/test/scaffold_api_controller_generator_test.rb +70 -0
- data/test/scaffold_controller_generator_test.rb +62 -19
- data/test/test_helper.rb +36 -0
- metadata +47 -22
- data/.travis.yml +0 -38
- data/Gemfile.old +0 -7
data/lib/jbuilder.rb
CHANGED
@@ -1,53 +1,25 @@
|
|
1
|
-
require 'active_support
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
require '
|
5
|
-
require '
|
6
|
-
require '
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
class Jbuilder < JbuilderProxy
|
17
|
-
class NullError < ::NoMethodError
|
18
|
-
def initialize(key)
|
19
|
-
super "Failed to add #{key.to_s.inspect} property to null object"
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
class KeyFormatter
|
24
|
-
def initialize(*args)
|
25
|
-
@format = ::ActiveSupport::OrderedHash.new
|
26
|
-
@cache = {}
|
1
|
+
require 'active_support'
|
2
|
+
require 'jbuilder/jbuilder'
|
3
|
+
require 'jbuilder/blank'
|
4
|
+
require 'jbuilder/key_formatter'
|
5
|
+
require 'jbuilder/errors'
|
6
|
+
require 'json'
|
7
|
+
require 'ostruct'
|
8
|
+
require 'active_support/core_ext/hash/deep_merge'
|
9
|
+
|
10
|
+
class Jbuilder
|
11
|
+
@@key_formatter = nil
|
12
|
+
@@ignore_nil = false
|
13
|
+
@@deep_format_keys = false
|
27
14
|
|
28
|
-
|
29
|
-
|
30
|
-
@format[name] = []
|
31
|
-
end
|
32
|
-
options.each do |name, paramaters|
|
33
|
-
@format[name] = paramaters
|
34
|
-
end
|
35
|
-
end
|
15
|
+
def initialize(options = {})
|
16
|
+
@attributes = {}
|
36
17
|
|
37
|
-
|
38
|
-
|
39
|
-
|
18
|
+
@key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil}
|
19
|
+
@ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
|
20
|
+
@deep_format_keys = options.fetch(:deep_format_keys, @@deep_format_keys)
|
40
21
|
|
41
|
-
|
42
|
-
@cache[key] ||= @format.inject(key.to_s) do |result, args|
|
43
|
-
func, args = args
|
44
|
-
if ::Proc === func
|
45
|
-
func.call result, *args
|
46
|
-
else
|
47
|
-
result.send func, *args
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
22
|
+
yield self if ::Kernel.block_given?
|
51
23
|
end
|
52
24
|
|
53
25
|
# Yields a builder and automatically turns the result into a JSON string
|
@@ -55,59 +27,51 @@ class Jbuilder < JbuilderProxy
|
|
55
27
|
new(*args, &block).target!
|
56
28
|
end
|
57
29
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
def initialize(*args, &block)
|
62
|
-
@attributes = ::ActiveSupport::OrderedHash.new
|
63
|
-
|
64
|
-
options = args.extract_options!
|
65
|
-
@key_formatter = options.fetch(:key_formatter){ @@key_formatter.clone }
|
66
|
-
@ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
|
67
|
-
yield self if block
|
68
|
-
end
|
69
|
-
|
70
|
-
BLANK = ::Object.new
|
30
|
+
BLANK = Blank.new
|
31
|
+
NON_ENUMERABLES = [ ::Struct, ::OpenStruct ].to_set
|
71
32
|
|
72
33
|
def set!(key, value = BLANK, *args, &block)
|
73
|
-
|
74
|
-
|
75
|
-
if BLANK != value
|
34
|
+
result = if ::Kernel.block_given?
|
35
|
+
if !_blank?(value)
|
76
36
|
# json.comments @post.comments { |comment| ... }
|
77
37
|
# { "comments": [ { ... }, { ... } ] }
|
78
38
|
_scope{ array! value, &block }
|
79
39
|
else
|
80
40
|
# json.comments { ... }
|
81
41
|
# { "comments": ... }
|
82
|
-
|
42
|
+
_merge_block(key){ yield self }
|
83
43
|
end
|
84
44
|
elsif args.empty?
|
85
45
|
if ::Jbuilder === value
|
86
46
|
# json.age 32
|
87
47
|
# json.person another_jbuilder
|
88
48
|
# { "age": 32, "person": { ... }
|
89
|
-
value.attributes!
|
49
|
+
_format_keys(value.attributes!)
|
90
50
|
else
|
91
51
|
# json.age 32
|
92
52
|
# { "age": 32 }
|
93
|
-
value
|
53
|
+
_format_keys(value)
|
94
54
|
end
|
95
|
-
elsif
|
55
|
+
elsif _is_collection?(value)
|
96
56
|
# json.comments @post.comments, :content, :created_at
|
97
57
|
# { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
|
98
58
|
_scope{ array! value, *args }
|
99
59
|
else
|
100
60
|
# json.author @post.creator, :name, :email_address
|
101
61
|
# { "author": { "name": "David", "email_address": "david@loudthinking.com" } }
|
102
|
-
|
62
|
+
_merge_block(key){ extract! value, *args }
|
103
63
|
end
|
104
64
|
|
105
65
|
_set_value key, result
|
106
66
|
end
|
107
67
|
|
108
|
-
|
109
|
-
|
110
|
-
|
68
|
+
def method_missing(*args, &block)
|
69
|
+
if ::Kernel.block_given?
|
70
|
+
set!(*args, &block)
|
71
|
+
else
|
72
|
+
set!(*args)
|
73
|
+
end
|
74
|
+
end
|
111
75
|
|
112
76
|
# Specifies formatting to be applied to the key. Passing in a name of a function
|
113
77
|
# will cause that function to be called on the key. So :upcase will upper case
|
@@ -170,6 +134,31 @@ class Jbuilder < JbuilderProxy
|
|
170
134
|
@@ignore_nil = value
|
171
135
|
end
|
172
136
|
|
137
|
+
# Deeply apply key format to nested hashes and arrays passed to
|
138
|
+
# methods like set!, merge! or array!.
|
139
|
+
#
|
140
|
+
# Example:
|
141
|
+
#
|
142
|
+
# json.key_format! camelize: :lower
|
143
|
+
# json.settings({some_value: "abc"})
|
144
|
+
#
|
145
|
+
# { "settings": { "some_value": "abc" }}
|
146
|
+
#
|
147
|
+
# json.key_format! camelize: :lower
|
148
|
+
# json.deep_format_keys!
|
149
|
+
# json.settings({some_value: "abc"})
|
150
|
+
#
|
151
|
+
# { "settings": { "someValue": "abc" }}
|
152
|
+
#
|
153
|
+
def deep_format_keys!(value = true)
|
154
|
+
@deep_format_keys = value
|
155
|
+
end
|
156
|
+
|
157
|
+
# Same as instance method deep_format_keys! except sets the default.
|
158
|
+
def self.deep_format_keys(value = true)
|
159
|
+
@@deep_format_keys = value
|
160
|
+
end
|
161
|
+
|
173
162
|
# Turns the current element into an array and yields a builder to add a hash.
|
174
163
|
#
|
175
164
|
# Example:
|
@@ -188,7 +177,7 @@ class Jbuilder < JbuilderProxy
|
|
188
177
|
# end
|
189
178
|
def child!
|
190
179
|
@attributes = [] unless ::Array === @attributes
|
191
|
-
@attributes << _scope
|
180
|
+
@attributes << _scope{ yield self }
|
192
181
|
end
|
193
182
|
|
194
183
|
# Turns the current element into an array and iterates over the passed collection, adding each iteration as
|
@@ -203,7 +192,7 @@ class Jbuilder < JbuilderProxy
|
|
203
192
|
#
|
204
193
|
# [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ]
|
205
194
|
#
|
206
|
-
#
|
195
|
+
# You can use the call syntax instead of an explicit extract! call:
|
207
196
|
#
|
208
197
|
# json.(@people) { |person| ... }
|
209
198
|
#
|
@@ -222,15 +211,17 @@ class Jbuilder < JbuilderProxy
|
|
222
211
|
#
|
223
212
|
# [1,2,3]
|
224
213
|
def array!(collection = [], *attributes, &block)
|
225
|
-
|
226
|
-
|
227
|
-
elsif
|
214
|
+
array = if collection.nil?
|
215
|
+
[]
|
216
|
+
elsif ::Kernel.block_given?
|
228
217
|
_map_collection(collection, &block)
|
229
218
|
elsif attributes.any?
|
230
219
|
_map_collection(collection) { |element| extract! element, *attributes }
|
231
220
|
else
|
232
|
-
collection
|
221
|
+
_format_keys(collection.to_a)
|
233
222
|
end
|
223
|
+
|
224
|
+
@attributes = _merge_values(@attributes, array)
|
234
225
|
end
|
235
226
|
|
236
227
|
# Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON.
|
@@ -252,14 +243,14 @@ class Jbuilder < JbuilderProxy
|
|
252
243
|
# json.(@person, :name, :age)
|
253
244
|
def extract!(object, *attributes)
|
254
245
|
if ::Hash === object
|
255
|
-
_extract_hash_values(object,
|
246
|
+
_extract_hash_values(object, attributes)
|
256
247
|
else
|
257
|
-
_extract_method_values(object,
|
248
|
+
_extract_method_values(object, attributes)
|
258
249
|
end
|
259
250
|
end
|
260
251
|
|
261
252
|
def call(object, *attributes, &block)
|
262
|
-
if
|
253
|
+
if ::Kernel.block_given?
|
263
254
|
array! object, &block
|
264
255
|
else
|
265
256
|
extract! object, *attributes
|
@@ -278,82 +269,98 @@ class Jbuilder < JbuilderProxy
|
|
278
269
|
@attributes
|
279
270
|
end
|
280
271
|
|
272
|
+
# Merges hash, array, or Jbuilder instance into current builder.
|
273
|
+
def merge!(object)
|
274
|
+
hash_or_array = ::Jbuilder === object ? object.attributes! : object
|
275
|
+
@attributes = _merge_values(@attributes, _format_keys(hash_or_array))
|
276
|
+
end
|
277
|
+
|
281
278
|
# Encodes the current builder as JSON.
|
282
279
|
def target!
|
283
|
-
|
280
|
+
@attributes.to_json
|
284
281
|
end
|
285
282
|
|
286
283
|
private
|
287
284
|
|
288
|
-
|
289
|
-
|
290
|
-
|
285
|
+
def _extract_hash_values(object, attributes)
|
286
|
+
attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
|
287
|
+
end
|
291
288
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
message = "Private method #{method_name.inspect} was used to " +
|
296
|
-
'extract value. This will be an error in future versions of Jbuilder'
|
297
|
-
end
|
289
|
+
def _extract_method_values(object, attributes)
|
290
|
+
attributes.each{ |key| _set_value key, _format_keys(object.public_send(key)) }
|
291
|
+
end
|
298
292
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
293
|
+
def _merge_block(key)
|
294
|
+
current_value = _blank? ? BLANK : @attributes.fetch(_key(key), BLANK)
|
295
|
+
raise NullError.build(key) if current_value.nil?
|
296
|
+
new_value = _scope{ yield self }
|
297
|
+
_merge_values(current_value, new_value)
|
298
|
+
end
|
303
299
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
300
|
+
def _merge_values(current_value, updates)
|
301
|
+
if _blank?(updates)
|
302
|
+
current_value
|
303
|
+
elsif _blank?(current_value) || updates.nil? || current_value.empty? && ::Array === updates
|
304
|
+
updates
|
305
|
+
elsif ::Array === current_value && ::Array === updates
|
306
|
+
current_value + updates
|
307
|
+
elsif ::Hash === current_value && ::Hash === updates
|
308
|
+
current_value.deep_merge(updates)
|
309
|
+
else
|
310
|
+
raise MergeError.build(current_value, updates)
|
309
311
|
end
|
312
|
+
end
|
310
313
|
|
311
|
-
|
312
|
-
|
314
|
+
def _key(key)
|
315
|
+
@key_formatter ? @key_formatter.format(key) : key.to_s
|
316
|
+
end
|
313
317
|
|
314
|
-
|
315
|
-
|
316
|
-
end
|
317
|
-
end
|
318
|
+
def _format_keys(hash_or_array)
|
319
|
+
return hash_or_array unless @deep_format_keys
|
318
320
|
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
@attributes, @key_formatter = parent_attributes, parent_formatter
|
321
|
+
if ::Array === hash_or_array
|
322
|
+
hash_or_array.map { |value| _format_keys(value) }
|
323
|
+
elsif ::Hash === hash_or_array
|
324
|
+
::Hash[hash_or_array.collect { |k, v| [_key(k), _format_keys(v)] }]
|
325
|
+
else
|
326
|
+
hash_or_array
|
326
327
|
end
|
328
|
+
end
|
327
329
|
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
end
|
330
|
+
def _set_value(key, value)
|
331
|
+
raise NullError.build(key) if @attributes.nil?
|
332
|
+
raise ArrayError.build(key) if ::Array === @attributes
|
333
|
+
return if @ignore_nil && value.nil? or _blank?(value)
|
334
|
+
@attributes = {} if _blank?
|
335
|
+
@attributes[_key(key)] = value
|
336
|
+
end
|
336
337
|
|
337
|
-
|
338
|
-
|
339
|
-
|
338
|
+
def _map_collection(collection)
|
339
|
+
collection.map do |element|
|
340
|
+
_scope{ yield element }
|
341
|
+
end - [BLANK]
|
342
|
+
end
|
340
343
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
344
|
+
def _scope
|
345
|
+
parent_attributes, parent_formatter, parent_deep_format_keys = @attributes, @key_formatter, @deep_format_keys
|
346
|
+
@attributes = BLANK
|
347
|
+
yield
|
348
|
+
@attributes
|
349
|
+
ensure
|
350
|
+
@attributes, @key_formatter, @deep_format_keys = parent_attributes, parent_formatter, parent_deep_format_keys
|
351
|
+
end
|
347
352
|
|
348
|
-
|
353
|
+
def _is_collection?(object)
|
354
|
+
_object_respond_to?(object, :map, :count) && NON_ENUMERABLES.none?{ |klass| klass === object }
|
355
|
+
end
|
349
356
|
|
350
|
-
|
351
|
-
|
357
|
+
def _blank?(value=@attributes)
|
358
|
+
BLANK == value
|
359
|
+
end
|
352
360
|
|
353
|
-
|
354
|
-
|
355
|
-
|
361
|
+
def _object_respond_to?(object, *methods)
|
362
|
+
methods.all?{ |m| object.respond_to?(m) }
|
363
|
+
end
|
356
364
|
end
|
357
365
|
|
358
|
-
require 'jbuilder/
|
359
|
-
require 'jbuilder/railtie' if defined?(Rails::VERSION::MAJOR) && Rails::VERSION::MAJOR == 4
|
366
|
+
require 'jbuilder/railtie' if defined?(Rails)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'jbuilder/dependency_tracker'
|
3
|
+
|
4
|
+
|
5
|
+
class FakeTemplate
|
6
|
+
attr_reader :source, :handler
|
7
|
+
def initialize(source, handler = :jbuilder)
|
8
|
+
@source, @handler = source, handler
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
class JbuilderDependencyTrackerTest < ActiveSupport::TestCase
|
14
|
+
def make_tracker(name, source)
|
15
|
+
template = FakeTemplate.new(source)
|
16
|
+
Jbuilder::DependencyTracker.new(name, template)
|
17
|
+
end
|
18
|
+
|
19
|
+
def track_dependencies(source)
|
20
|
+
make_tracker('jbuilder_template', source).dependencies
|
21
|
+
end
|
22
|
+
|
23
|
+
test 'detects dependency via direct partial! call' do
|
24
|
+
dependencies = track_dependencies <<-RUBY
|
25
|
+
json.partial! 'path/to/partial', foo: bar
|
26
|
+
json.partial! 'path/to/another/partial', :fizz => buzz
|
27
|
+
RUBY
|
28
|
+
|
29
|
+
assert_equal %w[path/to/partial path/to/another/partial], dependencies
|
30
|
+
end
|
31
|
+
|
32
|
+
test 'detects dependency via direct partial! call with parens' do
|
33
|
+
dependencies = track_dependencies <<-RUBY
|
34
|
+
json.partial!("path/to/partial")
|
35
|
+
RUBY
|
36
|
+
|
37
|
+
assert_equal %w[path/to/partial], dependencies
|
38
|
+
end
|
39
|
+
|
40
|
+
test 'detects partial with options (1.9 style)' do
|
41
|
+
dependencies = track_dependencies <<-RUBY
|
42
|
+
json.partial! hello: 'world', partial: 'path/to/partial', foo: :bar
|
43
|
+
RUBY
|
44
|
+
|
45
|
+
assert_equal %w[path/to/partial], dependencies
|
46
|
+
end
|
47
|
+
|
48
|
+
test 'detects partial with options (1.8 style)' do
|
49
|
+
dependencies = track_dependencies <<-RUBY
|
50
|
+
json.partial! :hello => 'world', :partial => 'path/to/partial', :foo => :bar
|
51
|
+
RUBY
|
52
|
+
|
53
|
+
assert_equal %w[path/to/partial], dependencies
|
54
|
+
end
|
55
|
+
|
56
|
+
test 'detects partial in indirect collection calls' do
|
57
|
+
dependencies = track_dependencies <<-RUBY
|
58
|
+
json.comments @post.comments, partial: 'comments/comment', as: :comment
|
59
|
+
RUBY
|
60
|
+
|
61
|
+
assert_equal %w[comments/comment], dependencies
|
62
|
+
end
|
63
|
+
|
64
|
+
test 'detects explicit dependency' do
|
65
|
+
dependencies = track_dependencies <<-RUBY
|
66
|
+
# Template Dependency: path/to/partial
|
67
|
+
json.foo 'bar'
|
68
|
+
RUBY
|
69
|
+
|
70
|
+
assert_equal %w[path/to/partial], dependencies
|
71
|
+
end
|
72
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'test_helper'
|
1
2
|
require 'rails/generators/test_case'
|
2
3
|
require 'generators/rails/jbuilder_generator'
|
3
4
|
|
@@ -13,19 +14,45 @@ class JbuilderGeneratorTest < Rails::Generators::TestCase
|
|
13
14
|
%w(index show).each do |view|
|
14
15
|
assert_file "app/views/posts/#{view}.json.jbuilder"
|
15
16
|
end
|
17
|
+
assert_file "app/views/posts/_post.json.jbuilder"
|
16
18
|
end
|
17
19
|
|
18
20
|
test 'index content' do
|
19
21
|
run_generator
|
20
22
|
|
21
23
|
assert_file 'app/views/posts/index.json.jbuilder' do |content|
|
22
|
-
assert_match
|
23
|
-
assert_match /json\.extract! post, :id, :title, :body/, content
|
24
|
-
assert_match /json\.url post_url\(post, format: :json\)/, 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
|
28
|
+
assert_match %r{json\.partial! "posts/post", post: @post}, content
|
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{:created_at, :updated_at}, content
|
34
|
+
assert_match %r{json\.url post_url\(post, format: :json\)}, content
|
35
|
+
end
|
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
|
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
|
29
56
|
end
|
30
57
|
end
|
31
58
|
end
|