jbuilder 2.3.2 → 2.9.1

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.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +49 -25
  4. data/Appraisals +19 -36
  5. data/CHANGELOG.md +72 -3
  6. data/CONTRIBUTING.md +14 -8
  7. data/Gemfile +0 -1
  8. data/MIT-LICENSE +1 -1
  9. data/README.md +47 -22
  10. data/Rakefile +1 -5
  11. data/gemfiles/rails_4_2.gemfile +3 -6
  12. data/gemfiles/rails_5_0.gemfile +10 -0
  13. data/gemfiles/rails_5_1.gemfile +10 -0
  14. data/gemfiles/rails_5_2.gemfile +10 -0
  15. data/gemfiles/rails_6_0.gemfile +10 -0
  16. data/gemfiles/rails_head.gemfile +10 -0
  17. data/jbuilder.gemspec +4 -5
  18. data/lib/generators/rails/jbuilder_generator.rb +9 -2
  19. data/lib/generators/rails/scaffold_controller_generator.rb +7 -1
  20. data/lib/generators/rails/templates/api_controller.rb +2 -2
  21. data/lib/generators/rails/templates/controller.rb +2 -2
  22. data/lib/generators/rails/templates/index.json.jbuilder +1 -4
  23. data/lib/generators/rails/templates/partial.json.jbuilder +2 -0
  24. data/lib/generators/rails/templates/show.json.jbuilder +1 -1
  25. data/lib/jbuilder.rb +7 -7
  26. data/lib/jbuilder/errors.rb +7 -0
  27. data/lib/jbuilder/jbuilder_template.rb +61 -8
  28. data/lib/jbuilder/key_formatter.rb +2 -2
  29. data/lib/jbuilder/railtie.rb +12 -5
  30. data/test/jbuilder_dependency_tracker_test.rb +1 -1
  31. data/test/jbuilder_generator_test.rb +18 -4
  32. data/test/jbuilder_template_test.rb +204 -275
  33. data/test/jbuilder_test.rb +38 -2
  34. data/test/scaffold_api_controller_generator_test.rb +24 -11
  35. data/test/scaffold_controller_generator_test.rb +31 -18
  36. data/test/test_helper.rb +26 -6
  37. metadata +12 -36
  38. data/gemfiles/rails_3_0.gemfile +0 -14
  39. data/gemfiles/rails_3_1.gemfile +0 -14
  40. data/gemfiles/rails_3_2.gemfile +0 -14
  41. data/gemfiles/rails_4_0.gemfile +0 -13
  42. data/gemfiles/rails_4_1.gemfile +0 -13
  43. data/gemfiles/rails_edge.gemfile +0 -13
data/Rakefile CHANGED
@@ -12,11 +12,7 @@ else
12
12
 
13
13
  test.libs << "test"
14
14
 
15
- if Rails::VERSION::MAJOR == 3
16
- test.test_files = %w[test/jbuilder_template_test.rb test/jbuilder_test.rb]
17
- else
18
- test.test_files = FileList["test/*_test.rb"]
19
- end
15
+ test.test_files = FileList["test/*_test.rb"]
20
16
  end
21
17
 
22
18
  task default: :test
@@ -3,11 +3,8 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rake"
6
- gem "mocha", :require => false
6
+ gem "mocha", require: false
7
7
  gem "appraisal"
8
- gem "pry"
9
- gem "railties", "~> 4.2.0"
10
- gem "actionpack", "~> 4.2.0"
11
- gem "activemodel", "~> 4.2.0"
8
+ gem "rails", "~> 4.2.0"
12
9
 
13
- gemspec :path => "../"
10
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake"
6
+ gem "mocha", require: false
7
+ gem "appraisal"
8
+ gem "rails", "~> 5.0.0"
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake"
6
+ gem "mocha", require: false
7
+ gem "appraisal"
8
+ gem "rails", "~> 5.1.0"
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake"
6
+ gem "mocha", require: false
7
+ gem "appraisal"
8
+ gem "rails", "~> 5.2.0"
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake"
6
+ gem "mocha", require: false
7
+ gem "appraisal"
8
+ gem "rails", "~> 6.0.0.rc1"
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake"
6
+ gem "mocha", require: false
7
+ gem "appraisal"
8
+ gem "rails", github: "rails/rails"
9
+
10
+ gemspec path: "../"
data/jbuilder.gemspec CHANGED
@@ -1,16 +1,15 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'jbuilder'
3
- s.version = '2.3.2'
4
- s.authors = ['David Heinemeier Hansson', 'Pavel Pravosud']
5
- s.email = ['david@37signals.com', 'pavel@pravosud.com']
3
+ s.version = '2.9.1'
4
+ s.authors = 'David Heinemeier Hansson'
5
+ s.email = 'david@basecamp.com'
6
6
  s.summary = 'Create JSON structures via a Builder-style DSL'
7
7
  s.homepage = 'https://github.com/rails/jbuilder'
8
8
  s.license = 'MIT'
9
9
 
10
10
  s.required_ruby_version = '>= 1.9.3'
11
11
 
12
- s.add_dependency 'activesupport', '>= 3.0.0', '< 5'
13
- s.add_dependency 'multi_json', '~> 1.2'
12
+ s.add_dependency 'activesupport', '>= 4.2.0'
14
13
 
15
14
  s.files = `git ls-files`.split("\n")
16
15
  s.test_files = `git ls-files -- test/*`.split("\n")
@@ -10,6 +10,8 @@ module Rails
10
10
 
11
11
  argument :attributes, type: :array, default: [], banner: 'field:type field:type'
12
12
 
13
+ class_option :timestamps, type: :boolean, default: true
14
+
13
15
  def create_root_folder
14
16
  path = File.join('app/views', controller_file_path)
15
17
  empty_directory path unless File.directory?(path)
@@ -20,6 +22,7 @@ module Rails
20
22
  filename = filename_with_extensions(view)
21
23
  template filename, File.join('app/views', controller_file_path, filename)
22
24
  end
25
+ template filename_with_extensions('partial'), File.join('app/views', controller_file_path, filename_with_extensions("_#{singular_table_name}"))
23
26
  end
24
27
 
25
28
 
@@ -32,8 +35,12 @@ module Rails
32
35
  [name, :json, :jbuilder] * '.'
33
36
  end
34
37
 
35
- def attributes_list_with_timestamps
36
- attributes_list(attributes_names + %w(created_at updated_at))
38
+ def full_attributes_list
39
+ if options[:timestamps]
40
+ attributes_list(attributes_names + %w(created_at updated_at))
41
+ else
42
+ attributes_list(attributes_names)
43
+ end
37
44
  end
38
45
 
39
46
  def attributes_list(attributes = attributes_names)
@@ -6,7 +6,13 @@ module Rails
6
6
  class ScaffoldControllerGenerator
7
7
  source_paths << File.expand_path('../templates', __FILE__)
8
8
 
9
- hook_for :jbuilder, default: true
9
+ hook_for :jbuilder, type: :boolean, default: true
10
+
11
+ private
12
+
13
+ def permitted_params
14
+ attributes_names.map { |name| ":#{name}" }.join(', ')
15
+ end unless private_method_defined? :permitted_params
10
16
  end
11
17
  end
12
18
  end
@@ -54,9 +54,9 @@ class <%= controller_class_name %>Controller < ApplicationController
54
54
  # Never trust parameters from the scary internet, only allow the white list through.
55
55
  def <%= "#{singular_table_name}_params" %>
56
56
  <%- if attributes_names.empty? -%>
57
- params[<%= ":#{singular_table_name}" %>]
57
+ params.fetch(<%= ":#{singular_table_name}" %>, {})
58
58
  <%- else -%>
59
- params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
59
+ params.require(<%= ":#{singular_table_name}" %>).permit(<%= permitted_params %>)
60
60
  <%- end -%>
61
61
  end
62
62
  end
@@ -75,9 +75,9 @@ class <%= controller_class_name %>Controller < ApplicationController
75
75
  # Never trust parameters from the scary internet, only allow the white list through.
76
76
  def <%= "#{singular_table_name}_params" %>
77
77
  <%- if attributes_names.empty? -%>
78
- params[<%= ":#{singular_table_name}" %>]
78
+ params.fetch(<%= ":#{singular_table_name}" %>, {})
79
79
  <%- else -%>
80
- params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
80
+ params.require(<%= ":#{singular_table_name}" %>).permit(<%= permitted_params %>)
81
81
  <%- end -%>
82
82
  end
83
83
  end
@@ -1,4 +1 @@
1
- json.array!(@<%= plural_table_name %>) do |<%= singular_table_name %>|
2
- json.extract! <%= singular_table_name %>, <%= attributes_list %>
3
- json.url <%= singular_table_name %>_url(<%= singular_table_name %>, format: :json)
4
- end
1
+ json.array! @<%= plural_table_name %>, partial: "<%= plural_table_name %>/<%= singular_table_name %>", as: :<%= singular_table_name %>
@@ -0,0 +1,2 @@
1
+ json.extract! <%= singular_table_name %>, <%= full_attributes_list %>
2
+ json.url <%= singular_table_name %>_url(<%= singular_table_name %>, format: :json)
@@ -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 %>
data/lib/jbuilder.rb CHANGED
@@ -2,7 +2,7 @@ require 'jbuilder/jbuilder'
2
2
  require 'jbuilder/blank'
3
3
  require 'jbuilder/key_formatter'
4
4
  require 'jbuilder/errors'
5
- require 'multi_json'
5
+ require 'json'
6
6
  require 'ostruct'
7
7
 
8
8
  class Jbuilder
@@ -247,7 +247,7 @@ class Jbuilder
247
247
 
248
248
  # Encodes the current builder as JSON.
249
249
  def target!
250
- ::MultiJson.dump(@attributes)
250
+ @attributes.to_json
251
251
  end
252
252
 
253
253
  private
@@ -270,14 +270,14 @@ class Jbuilder
270
270
  def _merge_values(current_value, updates)
271
271
  if _blank?(updates)
272
272
  current_value
273
- elsif _blank?(current_value) || updates.nil?
273
+ elsif _blank?(current_value) || updates.nil? || current_value.empty? && ::Array === updates
274
274
  updates
275
- elsif ::Array === updates
276
- ::Array === current_value ? current_value + updates : updates
277
- elsif ::Hash === current_value
275
+ elsif ::Array === current_value && ::Array === updates
276
+ current_value + updates
277
+ elsif ::Hash === current_value && ::Hash === updates
278
278
  current_value.merge(updates)
279
279
  else
280
- raise "Can't merge #{updates.inspect} with #{current_value.inspect}"
280
+ raise MergeError.build(current_value, updates)
281
281
  end
282
282
  end
283
283
 
@@ -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,6 +11,7 @@ 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
 
@@ -32,7 +33,7 @@ class JbuilderTemplate < Jbuilder
32
33
  # end
33
34
  def cache!(key=nil, options={})
34
35
  if @context.controller.perform_caching
35
- value = ::Rails.cache.fetch(_cache_key(key, options), options) do
36
+ value = _cache_fragment_for(key, options) do
36
37
  _scope { yield self }
37
38
  end
38
39
 
@@ -42,6 +43,27 @@ class JbuilderTemplate < Jbuilder
42
43
  end
43
44
  end
44
45
 
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
+
45
67
  # Conditionally caches the json depending in the condition given as first parameter. Has the same
46
68
  # signature as the `cache` helper method in `ActionView::Helpers::CacheHelper` and so can be used in
47
69
  # the same way.
@@ -55,6 +77,10 @@ class JbuilderTemplate < Jbuilder
55
77
  condition ? cache!(*args, &::Proc.new) : yield
56
78
  end
57
79
 
80
+ def target!
81
+ @cached_root || super
82
+ end
83
+
58
84
  def array!(collection = [], *args)
59
85
  options = args.first
60
86
 
@@ -78,7 +104,7 @@ class JbuilderTemplate < Jbuilder
78
104
  private
79
105
 
80
106
  def _render_partial_with_options(options)
81
- options.reverse_merge! locals: {}
107
+ options.reverse_merge! locals: options.except(:partial, :as, :collection)
82
108
  options.reverse_merge! ::JbuilderTemplate.template_lookup_options
83
109
  as = options[:as]
84
110
 
@@ -102,9 +128,35 @@ class JbuilderTemplate < Jbuilder
102
128
  @context.render options
103
129
  end
104
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
+
105
150
  def _cache_key(key, options)
106
- key = _fragment_name_with_digest(key, options)
107
- 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?(:combined_fragment_cache_key)
155
+ key = @context.combined_fragment_cache_key(key)
156
+ else
157
+ key = url_for(key).split('://', 2).last if ::Hash === key
158
+ end
159
+
108
160
  ::ActiveSupport::Cache.expand_cache_key(key, :jbuilder)
109
161
  end
110
162
 
@@ -136,7 +188,7 @@ class JbuilderTemplate < Jbuilder
136
188
  _scope{ _render_partial_with_options options.merge(collection: object) }
137
189
  else
138
190
  locals = ::Hash[options[:as], object]
139
- _scope{ _render_partial options.merge(locals: locals) }
191
+ _scope{ _render_partial_with_options options.merge(locals: locals) }
140
192
  end
141
193
 
142
194
  set! name, value
@@ -170,11 +222,12 @@ end
170
222
 
171
223
  class JbuilderHandler
172
224
  cattr_accessor :default_format
173
- self.default_format = Mime[:json]
225
+ self.default_format = :json
174
226
 
175
- def self.call(template)
227
+ def self.call(template, source = nil)
228
+ source ||= template.source
176
229
  # this juggling is required to keep line numbers right in the error
177
- %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{template.source}
230
+ %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{source}
178
231
  json.target! unless (__already_defined && __already_defined != "method")}
179
232
  end
180
233
  end
@@ -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,17 +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
15
- include ActionController::Helpers
16
- include ActionController::ImplicitRender
20
+ if self == ActionController::API
21
+ include ActionController::Helpers
22
+ include ActionController::ImplicitRender
23
+ end
17
24
  end
18
25
  end
19
26
  end
@@ -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 collecton calls' do
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
@@ -14,19 +14,33 @@ 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
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
30
44
  end
31
45
  end
32
46
  end