jbuilder 2.7.0 → 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 +2 -0
- data/Appraisals +20 -12
- data/CONTRIBUTING.md +4 -6
- data/Gemfile +0 -1
- data/MIT-LICENSE +1 -1
- data/README.md +99 -19
- data/Rakefile +1 -1
- data/gemfiles/rails_5_0.gemfile +1 -4
- data/gemfiles/rails_5_1.gemfile +1 -4
- data/gemfiles/{rails_4_2.gemfile → rails_5_2.gemfile} +1 -4
- 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 +18 -4
- data/lib/generators/rails/jbuilder_generator.rb +12 -2
- data/lib/generators/rails/scaffold_controller_generator.rb +6 -0
- data/lib/generators/rails/templates/api_controller.rb +4 -4
- data/lib/generators/rails/templates/controller.rb +15 -19
- data/lib/generators/rails/templates/index.json.jbuilder +1 -1
- data/lib/generators/rails/templates/partial.json.jbuilder +15 -1
- data/lib/jbuilder/collection_renderer.rb +109 -0
- data/lib/jbuilder/jbuilder_template.rb +63 -15
- data/lib/jbuilder/railtie.rb +1 -1
- data/lib/jbuilder.rb +65 -23
- data/test/jbuilder_dependency_tracker_test.rb +2 -2
- data/test/jbuilder_generator_test.rb +25 -5
- data/test/jbuilder_template_test.rb +280 -327
- data/test/jbuilder_test.rb +229 -4
- data/test/scaffold_api_controller_generator_test.rb +18 -3
- data/test/scaffold_controller_generator_test.rb +41 -8
- data/test/test_helper.rb +30 -8
- metadata +21 -14
- data/.travis.yml +0 -66
- data/CHANGELOG.md +0 -249
@@ -1,19 +1,17 @@
|
|
1
1
|
<% if namespaced? -%>
|
2
|
-
require_dependency "<%=
|
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: [
|
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,40 @@ 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
|
33
|
+
format.html { redirect_to <%= show_helper %>, 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
|
46
|
+
format.html { redirect_to <%= show_helper %>, 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 %>
|
58
|
+
|
63
59
|
respond_to do |format|
|
64
|
-
format.html { redirect_to <%= index_helper %>_url, notice: <%= "
|
60
|
+
format.html { redirect_to <%= index_helper %>_url, notice: <%= %("#{human_name} was successfully destroyed.") %> }
|
65
61
|
format.json { head :no_content }
|
66
62
|
end
|
67
63
|
end
|
@@ -72,12 +68,12 @@ class <%= controller_class_name %>Controller < ApplicationController
|
|
72
68
|
@<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
|
73
69
|
end
|
74
70
|
|
75
|
-
#
|
71
|
+
# Only allow a list of trusted parameters through.
|
76
72
|
def <%= "#{singular_table_name}_params" %>
|
77
73
|
<%- if attributes_names.empty? -%>
|
78
74
|
params.fetch(<%= ":#{singular_table_name}" %>, {})
|
79
75
|
<%- else -%>
|
80
|
-
params.require(<%= ":#{singular_table_name}" %>).permit(<%=
|
76
|
+
params.require(<%= ":#{singular_table_name}" %>).permit(<%= permitted_params %>)
|
81
77
|
<%- end -%>
|
82
78
|
end
|
83
79
|
end
|
@@ -1 +1 @@
|
|
1
|
-
json.array! @<%= plural_table_name %>, partial:
|
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 %>, <%=
|
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 -%>
|
@@ -0,0 +1,109 @@
|
|
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
|
+
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
|
@@ -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,
|
109
|
+
def cache_if!(condition, *args, &block)
|
110
|
+
condition ? cache!(*args, &block) : yield
|
78
111
|
end
|
79
112
|
|
80
113
|
def target!
|
@@ -104,11 +137,30 @@ 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
|
+
|
149
|
+
if options.has_key?(:layout)
|
150
|
+
raise ::NotImplementedError, "The `:layout' option is not supported in collection rendering."
|
151
|
+
end
|
152
|
+
|
153
|
+
if options.has_key?(:spacer_template)
|
154
|
+
raise ::NotImplementedError, "The `:spacer_template' option is not supported in collection rendering."
|
155
|
+
end
|
156
|
+
|
157
|
+
results = CollectionRenderer
|
158
|
+
.new(@context.lookup_context, options) { |&block| _scope(&block) }
|
159
|
+
.render_collection_with_partial(collection, partial, @context, nil)
|
160
|
+
|
161
|
+
array! if results.respond_to?(:body) && results.body.nil?
|
162
|
+
elsif as && options.key?(:collection) && !CollectionRenderer.supported?
|
163
|
+
# For Rails <= 5.2:
|
112
164
|
as = as.to_sym
|
113
165
|
collection = options.delete(:collection)
|
114
166
|
locals = options.delete(:locals)
|
@@ -151,8 +203,8 @@ class JbuilderTemplate < Jbuilder
|
|
151
203
|
name_options = options.slice(:skip_digest, :virtual_path)
|
152
204
|
key = _fragment_name_with_digest(key, name_options)
|
153
205
|
|
154
|
-
if @context.respond_to?(:
|
155
|
-
key = @context.
|
206
|
+
if @context.respond_to?(:combined_fragment_cache_key)
|
207
|
+
key = @context.combined_fragment_cache_key(key)
|
156
208
|
else
|
157
209
|
key = url_for(key).split('://', 2).last if ::Hash === key
|
158
210
|
end
|
@@ -162,12 +214,7 @@ class JbuilderTemplate < Jbuilder
|
|
162
214
|
|
163
215
|
def _fragment_name_with_digest(key, options)
|
164
216
|
if @context.respond_to?(:cache_fragment_name)
|
165
|
-
|
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)
|
217
|
+
@context.cache_fragment_name(key, **options)
|
171
218
|
else
|
172
219
|
key
|
173
220
|
end
|
@@ -222,11 +269,12 @@ end
|
|
222
269
|
|
223
270
|
class JbuilderHandler
|
224
271
|
cattr_accessor :default_format
|
225
|
-
self.default_format =
|
272
|
+
self.default_format = :json
|
226
273
|
|
227
|
-
def self.call(template)
|
274
|
+
def self.call(template, source = nil)
|
275
|
+
source ||= template.source
|
228
276
|
# this juggling is required to keep line numbers right in the error
|
229
|
-
%{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{
|
277
|
+
%{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{source}
|
230
278
|
json.target! unless (__already_defined && __already_defined != "method")}
|
231
279
|
end
|
232
280
|
end
|
data/lib/jbuilder/railtie.rb
CHANGED
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 '
|
6
|
+
require 'json'
|
6
7
|
require 'ostruct'
|
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
|
@@ -26,12 +30,12 @@ class Jbuilder
|
|
26
30
|
BLANK = Blank.new
|
27
31
|
NON_ENUMERABLES = [ ::Struct, ::OpenStruct ].to_set
|
28
32
|
|
29
|
-
def set!(key, value = BLANK, *args)
|
33
|
+
def set!(key, value = BLANK, *args, &block)
|
30
34
|
result = if ::Kernel.block_given?
|
31
35
|
if !_blank?(value)
|
32
36
|
# json.comments @post.comments { |comment| ... }
|
33
37
|
# { "comments": [ { ... }, { ... } ] }
|
34
|
-
_scope{ array! value,
|
38
|
+
_scope{ array! value, &block }
|
35
39
|
else
|
36
40
|
# json.comments { ... }
|
37
41
|
# { "comments": ... }
|
@@ -42,11 +46,11 @@ class Jbuilder
|
|
42
46
|
# json.age 32
|
43
47
|
# json.person another_jbuilder
|
44
48
|
# { "age": 32, "person": { ... }
|
45
|
-
value.attributes!
|
49
|
+
_format_keys(value.attributes!)
|
46
50
|
else
|
47
51
|
# json.age 32
|
48
52
|
# { "age": 32 }
|
49
|
-
value
|
53
|
+
_format_keys(value)
|
50
54
|
end
|
51
55
|
elsif _is_collection?(value)
|
52
56
|
# json.comments @post.comments, :content, :created_at
|
@@ -61,9 +65,9 @@ class Jbuilder
|
|
61
65
|
_set_value key, result
|
62
66
|
end
|
63
67
|
|
64
|
-
def method_missing(*args)
|
68
|
+
def method_missing(*args, &block)
|
65
69
|
if ::Kernel.block_given?
|
66
|
-
set!(*args,
|
70
|
+
set!(*args, &block)
|
67
71
|
else
|
68
72
|
set!(*args)
|
69
73
|
end
|
@@ -130,6 +134,31 @@ class Jbuilder
|
|
130
134
|
@@ignore_nil = value
|
131
135
|
end
|
132
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
|
+
|
133
162
|
# Turns the current element into an array and yields a builder to add a hash.
|
134
163
|
#
|
135
164
|
# Example:
|
@@ -163,7 +192,7 @@ class Jbuilder
|
|
163
192
|
#
|
164
193
|
# [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ]
|
165
194
|
#
|
166
|
-
#
|
195
|
+
# You can use the call syntax instead of an explicit extract! call:
|
167
196
|
#
|
168
197
|
# json.(@people) { |person| ... }
|
169
198
|
#
|
@@ -181,18 +210,18 @@ class Jbuilder
|
|
181
210
|
# json.array! [1, 2, 3]
|
182
211
|
#
|
183
212
|
# [1,2,3]
|
184
|
-
def array!(collection = [], *attributes)
|
213
|
+
def array!(collection = [], *attributes, &block)
|
185
214
|
array = if collection.nil?
|
186
215
|
[]
|
187
216
|
elsif ::Kernel.block_given?
|
188
|
-
_map_collection(collection,
|
217
|
+
_map_collection(collection, &block)
|
189
218
|
elsif attributes.any?
|
190
219
|
_map_collection(collection) { |element| extract! element, *attributes }
|
191
220
|
else
|
192
|
-
collection.to_a
|
221
|
+
_format_keys(collection.to_a)
|
193
222
|
end
|
194
223
|
|
195
|
-
|
224
|
+
@attributes = _merge_values(@attributes, array)
|
196
225
|
end
|
197
226
|
|
198
227
|
# Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON.
|
@@ -220,9 +249,9 @@ class Jbuilder
|
|
220
249
|
end
|
221
250
|
end
|
222
251
|
|
223
|
-
def call(object, *attributes)
|
252
|
+
def call(object, *attributes, &block)
|
224
253
|
if ::Kernel.block_given?
|
225
|
-
array! object,
|
254
|
+
array! object, &block
|
226
255
|
else
|
227
256
|
extract! object, *attributes
|
228
257
|
end
|
@@ -240,24 +269,25 @@ class Jbuilder
|
|
240
269
|
@attributes
|
241
270
|
end
|
242
271
|
|
243
|
-
# Merges hash or
|
244
|
-
def merge!(
|
245
|
-
|
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))
|
246
276
|
end
|
247
277
|
|
248
278
|
# Encodes the current builder as JSON.
|
249
279
|
def target!
|
250
|
-
|
280
|
+
@attributes.to_json
|
251
281
|
end
|
252
282
|
|
253
283
|
private
|
254
284
|
|
255
285
|
def _extract_hash_values(object, attributes)
|
256
|
-
attributes.each{ |key| _set_value key, object.fetch(key) }
|
286
|
+
attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
|
257
287
|
end
|
258
288
|
|
259
289
|
def _extract_method_values(object, attributes)
|
260
|
-
attributes.each{ |key| _set_value key, object.public_send(key) }
|
290
|
+
attributes.each{ |key| _set_value key, _format_keys(object.public_send(key)) }
|
261
291
|
end
|
262
292
|
|
263
293
|
def _merge_block(key)
|
@@ -275,7 +305,7 @@ class Jbuilder
|
|
275
305
|
elsif ::Array === current_value && ::Array === updates
|
276
306
|
current_value + updates
|
277
307
|
elsif ::Hash === current_value && ::Hash === updates
|
278
|
-
current_value.
|
308
|
+
current_value.deep_merge(updates)
|
279
309
|
else
|
280
310
|
raise MergeError.build(current_value, updates)
|
281
311
|
end
|
@@ -285,6 +315,18 @@ class Jbuilder
|
|
285
315
|
@key_formatter ? @key_formatter.format(key) : key.to_s
|
286
316
|
end
|
287
317
|
|
318
|
+
def _format_keys(hash_or_array)
|
319
|
+
return hash_or_array unless @deep_format_keys
|
320
|
+
|
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
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
288
330
|
def _set_value(key, value)
|
289
331
|
raise NullError.build(key) if @attributes.nil?
|
290
332
|
raise ArrayError.build(key) if ::Array === @attributes
|
@@ -300,12 +342,12 @@ class Jbuilder
|
|
300
342
|
end
|
301
343
|
|
302
344
|
def _scope
|
303
|
-
parent_attributes, parent_formatter = @attributes, @key_formatter
|
345
|
+
parent_attributes, parent_formatter, parent_deep_format_keys = @attributes, @key_formatter, @deep_format_keys
|
304
346
|
@attributes = BLANK
|
305
347
|
yield
|
306
348
|
@attributes
|
307
349
|
ensure
|
308
|
-
@attributes, @key_formatter = parent_attributes, parent_formatter
|
350
|
+
@attributes, @key_formatter, @deep_format_keys = parent_attributes, parent_formatter, parent_deep_format_keys
|
309
351
|
end
|
310
352
|
|
311
353
|
def _is_collection?(object)
|
@@ -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
|
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
|
@@ -61,7 +61,7 @@ class JbuilderDependencyTrackerTest < ActiveSupport::TestCase
|
|
61
61
|
assert_equal %w[comments/comment], dependencies
|
62
62
|
end
|
63
63
|
|
64
|
-
test 'detects explicit
|
64
|
+
test 'detects explicit dependency' do
|
65
65
|
dependencies = track_dependencies <<-RUBY
|
66
66
|
# Template Dependency: path/to/partial
|
67
67
|
json.foo 'bar'
|
@@ -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
|
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
|
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
|