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.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +49 -0
- data/.gitignore +3 -0
- data/Appraisals +10 -14
- data/CONTRIBUTING.md +5 -13
- data/Gemfile +0 -1
- data/MIT-LICENSE +1 -1
- data/README.md +103 -19
- data/Rakefile +1 -1
- data/bin/release +14 -0
- data/bin/test +6 -0
- data/gemfiles/{rails_4_2.gemfile → rails_7_0.gemfile} +1 -4
- data/gemfiles/{rails_5_0.gemfile → rails_7_1.gemfile} +1 -4
- data/gemfiles/{rails_5_1.gemfile → rails_head.gemfile} +1 -4
- data/jbuilder.gemspec +20 -4
- data/lib/generators/rails/jbuilder_generator.rb +16 -2
- data/lib/generators/rails/scaffold_controller_generator.rb +6 -0
- data/lib/generators/rails/templates/api_controller.rb +10 -4
- data/lib/generators/rails/templates/controller.rb +21 -19
- data/lib/generators/rails/templates/index.json.jbuilder +1 -1
- data/lib/generators/rails/templates/partial.json.jbuilder +15 -1
- data/lib/generators/rails/templates/show.json.jbuilder +1 -1
- data/lib/jbuilder/collection_renderer.rb +116 -0
- data/lib/jbuilder/jbuilder.rb +1 -7
- data/lib/jbuilder/jbuilder_dependency_tracker.rb +73 -0
- data/lib/jbuilder/jbuilder_template.rb +80 -22
- data/lib/jbuilder/railtie.rb +3 -3
- data/lib/jbuilder/version.rb +3 -0
- data/lib/jbuilder.rb +71 -30
- data/test/jbuilder_dependency_tracker_test.rb +3 -4
- data/test/jbuilder_generator_test.rb +37 -5
- data/test/jbuilder_template_test.rb +300 -324
- data/test/jbuilder_test.rb +229 -4
- data/test/scaffold_api_controller_generator_test.rb +31 -3
- data/test/scaffold_controller_generator_test.rb +54 -8
- data/test/test_helper.rb +41 -8
- metadata +28 -21
- data/.travis.yml +0 -66
- data/CHANGELOG.md +0 -249
- data/lib/jbuilder/dependency_tracker.rb +0 -61
@@ -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 -%>
|
@@ -1 +1 @@
|
|
1
|
-
json.partial! "<%=
|
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
|
data/lib/jbuilder/jbuilder.rb
CHANGED
@@ -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,
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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?(:
|
155
|
-
key = @context.
|
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
|
-
|
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 =
|
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); #{
|
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
|
data/lib/jbuilder/railtie.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'rails
|
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/
|
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
|
20
|
+
if name == 'ActionController::API'
|
21
21
|
include ActionController::Helpers
|
22
22
|
include ActionController::ImplicitRender
|
23
23
|
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 '
|
6
|
-
require '
|
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,
|
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,
|
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
|
-
#
|
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,
|
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
|
-
|
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,
|
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
|
244
|
-
def merge!(
|
245
|
-
|
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
|
-
|
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.
|
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) &&
|
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/
|
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
|
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
|
63
|
+
test 'detects explicit dependency' do
|
65
64
|
dependencies = track_dependencies <<-RUBY
|
66
65
|
# Template Dependency: path/to/partial
|
67
66
|
json.foo 'bar'
|