diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 1bb6adc5..8e4daa70 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -6,7 +6,7 @@ jobs: test: name: Ruby ${{ matrix.ruby }} (${{ matrix.gemfile }}) runs-on: ubuntu-20.04 - continue-on-error: ${{ matrix.experimental }} + continue-on-error: ${{ matrix.gemfile == 'rails_head' }} env: BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile BUNDLE_JOBS: 4 @@ -14,90 +14,31 @@ jobs: strategy: fail-fast: false matrix: - ruby: - - "2.2" - - "2.3" - - "2.4" - - "2.5" - - "2.6" - - "2.7" + ruby: - "3.0" + - "3.1" + - "3.2" + - "3.3" gemfile: - - "rails_5_0" - - "rails_5_1" - - "rails_5_2" - - "rails_6_0" - - "rails_6_1" + - "rails_7_0" + - "rails_7_1" - "rails_head" - - experimental: [false] + exclude: - - ruby: 2.7 - gemfile: rails_5_0 - - ruby: '3.0' - gemfile: rails_5_0 - - ruby: head - gemfile: rails_5_0 - - ruby: 2.7 - gemfile: rails_5_1 - - ruby: '3.0' - gemfile: rails_5_1 - - ruby: head - gemfile: rails_5_1 - - ruby: 2.2 - gemfile: rails_5_2 - - ruby: 2.7 - gemfile: rails_5_2 - ruby: '3.0' - gemfile: rails_5_2 - - ruby: head - gemfile: rails_5_2 - - ruby: 2.2 - gemfile: rails_6_0 - - ruby: 2.3 - gemfile: rails_6_0 - - ruby: 2.4 - gemfile: rails_6_0 - - ruby: '3.0' - gemfile: rails_6_0 - - ruby: head - gemfile: rails_6_0 - - ruby: 2.2 - gemfile: rails_6_1 - - ruby: 2.3 - gemfile: rails_6_1 - - ruby: 2.4 - gemfile: rails_6_1 - - ruby: 2.2 - gemfile: rails_head - - ruby: 2.3 gemfile: rails_head - - ruby: 2.4 - gemfile: rails_head - - ruby: 2.5 - gemfile: rails_head - - ruby: 2.6 - gemfile: rails_head - - ruby: 2.7 - gemfile: rails_head - experimental: false - - ruby: '3.0' - gemfile: rails_head - experimental: false + include: - - ruby: 2.7 + - ruby: '3.1' gemfile: rails_head - experimental: true - - ruby: '3.0' + - ruby: '3.2' gemfile: rails_head - experimental: true - ruby: head gemfile: rails_head - experimental: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: diff --git a/.gitignore b/.gitignore index 1ecd0e80..709e61b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ tmp +/log gemfiles/.bundle gemfiles/*.lock Gemfile.lock diff --git a/Appraisals b/Appraisals index cdbf059d..93167335 100644 --- a/Appraisals +++ b/Appraisals @@ -1,22 +1,10 @@ -appraise "rails-5-0" do - gem "rails", "~> 5.0.0" -end - -appraise "rails-5-1" do - gem "rails", "~> 5.1.0" -end - -appraise "rails-5-2" do - gem "rails", "~> 5.2.0" -end - -if RUBY_VERSION >= "2.5.0" - appraise "rails-6-0" do - gem "rails", "~> 6.0.0" +if RUBY_VERSION >= "2.7.0" + appraise "rails-7-0" do + gem "rails", "~> 7.0.0" end - appraise "rails-6-1" do - gem "rails", "~> 6.1.0" + appraise "rails-7-1" do + gem "rails", "~> 7.1.0" end appraise "rails-head" do diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ecd004d..35e5f960 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,13 +33,7 @@ git checkout -b my-feature-branch #### Bundle Install and Test -Ensure that you can build the project and run tests. - -``` -bundle install -appraisal install -appraisal rake test -``` +Ensure that you can build the project and run tests using `bin/test`. #### Write Tests diff --git a/README.md b/README.md index be2b3c6e..16535ed1 100644 --- a/README.md +++ b/README.md @@ -338,6 +338,10 @@ environment.rb for example): Jbuilder.deep_format_keys true ``` +## Testing JBuilder Response body with RSpec + +To test the response body of your controller spec, enable `render_views` in your RSpec context. This [configuration](https://rspec.info/features/6-0/rspec-rails/controller-specs/render-views) renders the views in a controller test. + ## Contributing to Jbuilder Jbuilder is the work of many contributors. You're encouraged to submit pull requests, propose diff --git a/bin/release b/bin/release new file mode 100755 index 00000000..97dbe40d --- /dev/null +++ b/bin/release @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +VERSION=$1 + +printf "class Jbuilder\n VERSION = \"$VERSION\"\nend\n" > ./lib/jbuilder/version.rb +bundle +git add lib/jbuilder/version.rb +git commit -m "Bump version for $VERSION" +git push +git tag v$VERSION +git push --tags +gem build jbuilder.gemspec +gem push "jbuilder-$VERSION.gem" --host https://rubygems.org +rm "jbuilder-$VERSION.gem" diff --git a/bin/test b/bin/test new file mode 100755 index 00000000..45d4fe95 --- /dev/null +++ b/bin/test @@ -0,0 +1,6 @@ +#!/bin/env bash +set -e + +bundle install +appraisal install +appraisal rake test diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile deleted file mode 100644 index 78272105..00000000 --- a/gemfiles/rails_5_2.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org/" - -gem "rake" -gem "mocha", require: false -gem "appraisal" -gem "rails", "~> 5.2.0" - -gemspec path: "../" diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile deleted file mode 100644 index 9bc0085f..00000000 --- a/gemfiles/rails_6_0.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org/" - -gem "rake" -gem "mocha", require: false -gem "appraisal" -gem "rails", "~> 6.0.0" - -gemspec path: "../" diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile deleted file mode 100644 index d2856264..00000000 --- a/gemfiles/rails_6_1.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org/" - -gem "rake" -gem "mocha", require: false -gem "appraisal" -gem "rails", "~> 6.1.0" - -gemspec path: "../" diff --git a/gemfiles/rails_5_0.gemfile b/gemfiles/rails_7_0.gemfile similarity index 85% rename from gemfiles/rails_5_0.gemfile rename to gemfiles/rails_7_0.gemfile index 488a5bc2..7c101935 100644 --- a/gemfiles/rails_5_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -5,6 +5,6 @@ source "https://rubygems.org/" gem "rake" gem "mocha", require: false gem "appraisal" -gem "rails", "~> 5.0.0" +gem "rails", "~> 7.0.0" gemspec path: "../" diff --git a/gemfiles/rails_5_1.gemfile b/gemfiles/rails_7_1.gemfile similarity index 85% rename from gemfiles/rails_5_1.gemfile rename to gemfiles/rails_7_1.gemfile index fff877ae..cb7710cb 100644 --- a/gemfiles/rails_5_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -5,6 +5,6 @@ source "https://rubygems.org/" gem "rake" gem "mocha", require: false gem "appraisal" -gem "rails", "~> 5.1.0" +gem "rails", "~> 7.1.0" gemspec path: "../" diff --git a/jbuilder.gemspec b/jbuilder.gemspec index 1191ad05..0b519531 100644 --- a/jbuilder.gemspec +++ b/jbuilder.gemspec @@ -1,6 +1,8 @@ +require_relative "lib/jbuilder/version" + Gem::Specification.new do |s| s.name = 'jbuilder' - s.version = '2.11.5' + s.version = Jbuilder::VERSION s.authors = 'David Heinemeier Hansson' s.email = 'david@basecamp.com' s.summary = 'Create JSON structures via a Builder-style DSL' diff --git a/lib/generators/rails/jbuilder_generator.rb b/lib/generators/rails/jbuilder_generator.rb index 0691f499..79f742e8 100644 --- a/lib/generators/rails/jbuilder_generator.rb +++ b/lib/generators/rails/jbuilder_generator.rb @@ -54,6 +54,10 @@ def attributes_list(attributes = attributes_names) def virtual_attributes attributes.select {|name| name.respond_to?(:virtual?) && name.virtual? } end + + def partial_path_name + [controller_file_path, singular_table_name].join("/") + end end end end diff --git a/lib/generators/rails/templates/api_controller.rb b/lib/generators/rails/templates/api_controller.rb index c41c744c..5c2222cc 100644 --- a/lib/generators/rails/templates/api_controller.rb +++ b/lib/generators/rails/templates/api_controller.rb @@ -48,13 +48,19 @@ def destroy private # Use callbacks to share common setup or constraints between actions. def set_<%= singular_table_name %> + <%- if Rails::VERSION::MAJOR >= 8 -%> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params.expect(:id)") %> + <%- else -%> @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> + <%- end -%> end # Only allow a list of trusted parameters through. def <%= "#{singular_table_name}_params" %> <%- if attributes_names.empty? -%> params.fetch(<%= ":#{singular_table_name}" %>, {}) + <%- elsif Rails::VERSION::MAJOR >= 8 -%> + params.expect(<%= singular_table_name %>: [ <%= permitted_params %> ]) <%- else -%> params.require(<%= ":#{singular_table_name}" %>).permit(<%= permitted_params %>) <%- end -%> diff --git a/lib/generators/rails/templates/controller.rb b/lib/generators/rails/templates/controller.rb index 92f5da78..4771067a 100644 --- a/lib/generators/rails/templates/controller.rb +++ b/lib/generators/rails/templates/controller.rb @@ -30,7 +30,7 @@ def create respond_to do |format| if @<%= orm_instance.save %> - format.html { redirect_to <%= show_helper %>, notice: <%= %("#{human_name} was successfully created.") %> } + format.html { redirect_to <%= redirect_resource_name %>, notice: <%= %("#{human_name} was successfully created.") %> } format.json { render :show, status: :created, location: <%= "@#{singular_table_name}" %> } else format.html { render :new, status: :unprocessable_entity } @@ -43,7 +43,7 @@ def create def update respond_to do |format| if @<%= orm_instance.update("#{singular_table_name}_params") %> - format.html { redirect_to <%= show_helper %>, notice: <%= %("#{human_name} was successfully updated.") %> } + format.html { redirect_to <%= redirect_resource_name %>, notice: <%= %("#{human_name} was successfully updated.") %> } format.json { render :show, status: :ok, location: <%= "@#{singular_table_name}" %> } else format.html { render :edit, status: :unprocessable_entity } @@ -57,7 +57,7 @@ def destroy @<%= orm_instance.destroy %> respond_to do |format| - format.html { redirect_to <%= index_helper %>_url, notice: <%= %("#{human_name} was successfully destroyed.") %> } + format.html { redirect_to <%= index_helper %>_path, status: :see_other, notice: <%= %("#{human_name} was successfully destroyed.") %> } format.json { head :no_content } end end @@ -65,13 +65,19 @@ def destroy private # Use callbacks to share common setup or constraints between actions. def set_<%= singular_table_name %> + <%- if Rails::VERSION::MAJOR >= 8 -%> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params.expect(:id)") %> + <%- else -%> @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> + <%- end -%> end # Only allow a list of trusted parameters through. def <%= "#{singular_table_name}_params" %> <%- if attributes_names.empty? -%> params.fetch(<%= ":#{singular_table_name}" %>, {}) + <%- elsif Rails::VERSION::MAJOR >= 8 -%> + params.expect(<%= singular_table_name %>: [ <%= permitted_params %> ]) <%- else -%> params.require(<%= ":#{singular_table_name}" %>).permit(<%= permitted_params %>) <%- end -%> diff --git a/lib/generators/rails/templates/index.json.jbuilder b/lib/generators/rails/templates/index.json.jbuilder index 955b0954..4e0a14e0 100644 --- a/lib/generators/rails/templates/index.json.jbuilder +++ b/lib/generators/rails/templates/index.json.jbuilder @@ -1 +1 @@ -json.array! @<%= plural_table_name %>, partial: "<%= plural_table_name %>/<%= singular_table_name %>", as: :<%= singular_table_name %> +json.array! @<%= plural_table_name %>, partial: "<%= partial_path_name %>", as: :<%= singular_table_name %> diff --git a/lib/generators/rails/templates/show.json.jbuilder b/lib/generators/rails/templates/show.json.jbuilder index 4723e6bd..4f3bf738 100644 --- a/lib/generators/rails/templates/show.json.jbuilder +++ b/lib/generators/rails/templates/show.json.jbuilder @@ -1 +1 @@ -json.partial! "<%= plural_table_name %>/<%= singular_table_name %>", <%= singular_table_name %>: @<%= singular_table_name %> +json.partial! "<%= partial_path_name %>", <%= singular_table_name %>: @<%= singular_table_name %> diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index 3cf41d8a..b12624be 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -3,8 +3,8 @@ require 'jbuilder/blank' require 'jbuilder/key_formatter' require 'jbuilder/errors' +require 'jbuilder/version' require 'json' -require 'ostruct' require 'active_support/core_ext/hash/deep_merge' class Jbuilder @@ -28,7 +28,6 @@ def self.encode(*args, &block) end BLANK = Blank.new - NON_ENUMERABLES = [ ::Struct, ::OpenStruct ].to_set def set!(key, value = BLANK, *args, &block) result = if ::Kernel.block_given? @@ -292,7 +291,7 @@ def _extract_method_values(object, attributes) def _merge_block(key) current_value = _blank? ? BLANK : @attributes.fetch(_key(key), BLANK) - raise NullError.build(key) if current_value.nil? + ::Kernel.raise NullError.build(key) if current_value.nil? new_value = _scope{ yield self } _merge_values(current_value, new_value) end @@ -307,7 +306,7 @@ def _merge_values(current_value, updates) elsif ::Hash === current_value && ::Hash === updates current_value.deep_merge(updates) else - raise MergeError.build(current_value, updates) + ::Kernel.raise MergeError.build(current_value, updates) end end @@ -328,8 +327,8 @@ def _format_keys(hash_or_array) end def _set_value(key, value) - raise NullError.build(key) if @attributes.nil? - raise ArrayError.build(key) if ::Array === @attributes + ::Kernel.raise NullError.build(key) if @attributes.nil? + ::Kernel.raise ArrayError.build(key) if ::Array === @attributes return if @ignore_nil && value.nil? or _blank?(value) @attributes = {} if _blank? @attributes[_key(key)] = value @@ -351,7 +350,7 @@ def _scope end def _is_collection?(object) - _object_respond_to?(object, :map, :count) && NON_ENUMERABLES.none?{ |klass| klass === object } + _object_respond_to?(object, :map, :count) && !(::Struct === object) end def _blank?(value=@attributes) diff --git a/lib/jbuilder/collection_renderer.rb b/lib/jbuilder/collection_renderer.rb index 5d1620da..48707bfe 100644 --- a/lib/jbuilder/collection_renderer.rb +++ b/lib/jbuilder/collection_renderer.rb @@ -106,4 +106,11 @@ def collection_with_template(view, template) end end end + + class EnumerableCompat < ::SimpleDelegator + # Rails 6.1 requires this. + def size(*args, &block) + __getobj__.count(*args, &block) + end + end end diff --git a/lib/jbuilder/dependency_tracker.rb b/lib/jbuilder/dependency_tracker.rb deleted file mode 100644 index 83c8dbbb..00000000 --- a/lib/jbuilder/dependency_tracker.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'jbuilder/jbuilder' - -dependency_tracker = false - -begin - require 'action_view' - require 'action_view/dependency_tracker' - dependency_tracker = ::ActionView::DependencyTracker -rescue LoadError - begin - require 'cache_digests' - dependency_tracker = ::CacheDigests::DependencyTracker - rescue LoadError - end -end - -if dependency_tracker - class Jbuilder - module DependencyTrackerMethods - # Matches: - # json.partial! "messages/message" - # json.partial!('messages/message') - # - DIRECT_RENDERS = / - \w+\.partial! # json.partial! - \(?\s* # optional parenthesis - (['"])([^'"]+)\1 # quoted value - /x - - # Matches: - # json.partial! partial: "comments/comment" - # json.comments @post.comments, partial: "comments/comment", as: :comment - # json.array! @posts, partial: "posts/post", as: :post - # = render partial: "account" - # - INDIRECT_RENDERS = / - (?::partial\s*=>|partial:) # partial: or :partial => - \s* # optional whitespace - (['"])([^'"]+)\1 # quoted value - /x - - def dependencies - direct_dependencies + indirect_dependencies + explicit_dependencies - end - - private - - def direct_dependencies - source.scan(DIRECT_RENDERS).map(&:second) - end - - def indirect_dependencies - source.scan(INDIRECT_RENDERS).map(&:second) - end - end - end - - ::Jbuilder::DependencyTracker = Class.new(dependency_tracker::ERBTracker) - ::Jbuilder::DependencyTracker.send :include, ::Jbuilder::DependencyTrackerMethods - dependency_tracker.register_tracker :jbuilder, ::Jbuilder::DependencyTracker -end diff --git a/lib/jbuilder/jbuilder.rb b/lib/jbuilder/jbuilder.rb index 57d23e81..22b0ac4e 100644 --- a/lib/jbuilder/jbuilder.rb +++ b/lib/jbuilder/jbuilder.rb @@ -1,7 +1 @@ -Jbuilder = Class.new(begin - require 'active_support/proxy_object' - ActiveSupport::ProxyObject -rescue LoadError - require 'active_support/basic_object' - ActiveSupport::BasicObject -end) +Jbuilder = Class.new(BasicObject) diff --git a/lib/jbuilder/jbuilder_dependency_tracker.rb b/lib/jbuilder/jbuilder_dependency_tracker.rb new file mode 100644 index 00000000..62b6dbf2 --- /dev/null +++ b/lib/jbuilder/jbuilder_dependency_tracker.rb @@ -0,0 +1,73 @@ +class Jbuilder::DependencyTracker + EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/ + + # Matches: + # json.partial! "messages/message" + # json.partial!('messages/message') + # + DIRECT_RENDERS = / + \w+\.partial! # json.partial! + \(?\s* # optional parenthesis + (['"])([^'"]+)\1 # quoted value + /x + + # Matches: + # json.partial! partial: "comments/comment" + # json.comments @post.comments, partial: "comments/comment", as: :comment + # json.array! @posts, partial: "posts/post", as: :post + # = render partial: "account" + # + INDIRECT_RENDERS = / + (?::partial\s*=>|partial:) # partial: or :partial => + \s* # optional whitespace + (['"])([^'"]+)\1 # quoted value + /x + + def self.call(name, template, view_paths = nil) + new(name, template, view_paths).dependencies + end + + def initialize(name, template, view_paths = nil) + @name, @template, @view_paths = name, template, view_paths + end + + def dependencies + direct_dependencies + indirect_dependencies + explicit_dependencies + end + + private + + attr_reader :name, :template + + def direct_dependencies + source.scan(DIRECT_RENDERS).map(&:second) + end + + def indirect_dependencies + source.scan(INDIRECT_RENDERS).map(&:second) + end + + def explicit_dependencies + dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq + + wildcards, explicits = dependencies.partition { |dependency| dependency.end_with?("/*") } + + (explicits + resolve_directories(wildcards)).uniq + end + + def resolve_directories(wildcard_dependencies) + return [] unless @view_paths + return [] if wildcard_dependencies.empty? + + # Remove trailing "/*" + prefixes = wildcard_dependencies.map { |query| query[0..-3] } + + @view_paths.flat_map(&:all_template_paths).uniq.filter_map { |path| + path.to_s if prefixes.include?(path.prefix) + }.sort + end + + def source + template.source + end +end diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 2b90d745..c88a6791 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -89,7 +89,7 @@ def cache!(key=nil, options={}) # # json.extra 'This will not work either, the root must be exclusive' def cache_root!(key=nil, options={}) if @context.controller.perform_caching - raise "cache_root! can't be used after JSON structures have been defined" if @attributes.present? + ::Kernel.raise "cache_root! can't be used after JSON structures have been defined" if @attributes.present? @cached_root = _cache_fragment_for([ :root, key ], options) { yield; target! } else @@ -145,30 +145,40 @@ def _render_partial_with_options(options) collection = options.delete(:collection) || [] partial = options.delete(:partial) options[:locals].merge!(json: self) + collection = EnumerableCompat.new(collection) if collection.respond_to?(:count) && !collection.respond_to?(:size) if options.has_key?(:layout) - raise ::NotImplementedError, "The `:layout' option is not supported in collection rendering." + ::Kernel.raise ::NotImplementedError, "The `:layout' option is not supported in collection rendering." end if options.has_key?(:spacer_template) - raise ::NotImplementedError, "The `:spacer_template' option is not supported in collection rendering." + ::Kernel.raise ::NotImplementedError, "The `:spacer_template' option is not supported in collection rendering." end - results = CollectionRenderer - .new(@context.lookup_context, options) { |&block| _scope(&block) } - .render_collection_with_partial(collection, partial, @context, nil) - - array! if results.respond_to?(:body) && results.body.nil? + if collection.present? + results = CollectionRenderer + .new(@context.lookup_context, options) { |&block| _scope(&block) } + .render_collection_with_partial(collection, partial, @context, nil) + + array! if results.respond_to?(:body) && results.body.nil? + else + array! + end elsif as && options.key?(:collection) && !CollectionRenderer.supported? # For Rails <= 5.2: as = as.to_sym collection = options.delete(:collection) - locals = options.delete(:locals) - array! collection do |member| - member_locals = locals.clone - member_locals.merge! collection: collection - member_locals.merge! as => member - _render_partial options.merge(locals: member_locals) + + if collection.present? + locals = options.delete(:locals) + array! collection do |member| + member_locals = locals.clone + member_locals.merge! collection: collection + member_locals.merge! as => member + _render_partial options.merge(locals: member_locals) + end + else + array! end else _render_partial options diff --git a/lib/jbuilder/railtie.rb b/lib/jbuilder/railtie.rb index 7ae3ad7c..2aeefbb6 100644 --- a/lib/jbuilder/railtie.rb +++ b/lib/jbuilder/railtie.rb @@ -6,7 +6,7 @@ class Railtie < ::Rails::Railtie initializer :jbuilder do ActiveSupport.on_load :action_view do ActionView::Template.register_template_handler :jbuilder, JbuilderHandler - require 'jbuilder/dependency_tracker' + require 'jbuilder/jbuilder_dependency_tracker' end if Rails::VERSION::MAJOR >= 5 @@ -17,7 +17,7 @@ module ApiRendering end ActiveSupport.on_load :action_controller do - if self == ActionController::API + if name == 'ActionController::API' include ActionController::Helpers include ActionController::ImplicitRender end diff --git a/lib/jbuilder/version.rb b/lib/jbuilder/version.rb new file mode 100644 index 00000000..2a62782f --- /dev/null +++ b/lib/jbuilder/version.rb @@ -0,0 +1,3 @@ +class Jbuilder + VERSION = "2.13.0" +end diff --git a/test/jbuilder_dependency_tracker_test.rb b/test/jbuilder_dependency_tracker_test.rb index d89be37a..df30ea29 100644 --- a/test/jbuilder_dependency_tracker_test.rb +++ b/test/jbuilder_dependency_tracker_test.rb @@ -1,6 +1,5 @@ require 'test_helper' -require 'jbuilder/dependency_tracker' - +require 'jbuilder/jbuilder_dependency_tracker' class FakeTemplate attr_reader :source, :handler diff --git a/test/jbuilder_generator_test.rb b/test/jbuilder_generator_test.rb index 4884b6c7..e4a2f165 100644 --- a/test/jbuilder_generator_test.rb +++ b/test/jbuilder_generator_test.rb @@ -44,6 +44,18 @@ class JbuilderGeneratorTest < Rails::Generators::TestCase end end + test 'namespaced views are generated correctly for index' do + run_generator %w(Admin::Post --model-name=Post) + + assert_file 'app/views/admin/posts/index.json.jbuilder' do |content| + assert_match %r{json\.array! @posts, partial: "admin/posts/post", as: :post}, content + end + + assert_file 'app/views/admin/posts/show.json.jbuilder' do |content| + assert_match %r{json\.partial! "admin/posts/post", post: @post}, content + end + end + if Rails::VERSION::MAJOR >= 6 test 'handles virtual attributes' do run_generator %w(Message content:rich_text video:attachment photos:attachments) diff --git a/test/jbuilder_template_test.rb b/test/jbuilder_template_test.rb index eaac5051..6b8ac82f 100644 --- a/test/jbuilder_template_test.rb +++ b/test/jbuilder_template_test.rb @@ -73,6 +73,14 @@ class JbuilderTemplateTest < ActiveSupport::TestCase assert_equal "Pavel", result[5]["author"]["first_name"] end + test "partial collection by name with caching" do + result = render('json.partial! "post", collection: @posts, cached: true, as: :post', posts: POSTS) + assert_equal 10, result.count + assert_equal "Post #5", result[4]["body"] + assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] + assert_equal "Pavel", result[5]["author"]["first_name"] + end + test "partial collection by name with string local" do result = render('json.partial! "post", collection: @posts, as: "post"', posts: POSTS) assert_equal 10, result.count @@ -90,10 +98,12 @@ class JbuilderTemplateTest < ActiveSupport::TestCase end test "nil partial collection by name" do + Jbuilder::CollectionRenderer.expects(:new).never assert_equal [], render('json.partial! "post", collection: @posts, as: :post', posts: nil) end test "nil partial collection by options" do + Jbuilder::CollectionRenderer.expects(:new).never assert_equal [], render('json.partial! partial: "post", collection: @posts, as: :post', posts: nil) end @@ -105,7 +115,13 @@ class JbuilderTemplateTest < ActiveSupport::TestCase assert_equal "Pavel", result[5]["author"]["first_name"] end + test "empty array of partials from empty collection" do + Jbuilder::CollectionRenderer.expects(:new).never + assert_equal [], render('json.array! @posts, partial: "post", as: :post', posts: []) + end + test "empty array of partials from nil collection" do + Jbuilder::CollectionRenderer.expects(:new).never assert_equal [], render('json.array! @posts, partial: "post", as: :post', posts: nil) end @@ -118,10 +134,17 @@ class JbuilderTemplateTest < ActiveSupport::TestCase end test "empty array of partials under key from nil collection" do + Jbuilder::CollectionRenderer.expects(:new).never result = render('json.posts @posts, partial: "post", as: :post', posts: nil) assert_equal [], result["posts"] end + test "empty array of partials under key from an empy collection" do + Jbuilder::CollectionRenderer.expects(:new).never + result = render('json.posts @posts, partial: "post", as: :post', posts: []) + assert_equal [], result["posts"] + end + test "object fragment caching" do render(<<-JBUILDER) json.cache! "cache-key" do @@ -285,6 +308,7 @@ class JbuilderTemplateTest < ActiveSupport::TestCase if JbuilderTemplate::CollectionRenderer.supported? test "returns an empty array for an empty collection" do + Jbuilder::CollectionRenderer.expects(:new).never result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: []) # Do not use #assert_empty as it is important to ensure that the type of the JSON result is an array. @@ -294,7 +318,6 @@ class JbuilderTemplateTest < ActiveSupport::TestCase test "works with an enumerable object" do enumerable_class = Class.new do include Enumerable - alias length count # Rails 6.1 requires this. def each(&block) [].each(&block) diff --git a/test/scaffold_api_controller_generator_test.rb b/test/scaffold_api_controller_generator_test.rb index 2c8f8ef8..e7e2b355 100644 --- a/test/scaffold_api_controller_generator_test.rb +++ b/test/scaffold_api_controller_generator_test.rb @@ -38,8 +38,17 @@ class ScaffoldApiControllerGeneratorTest < Rails::Generators::TestCase assert_match %r{@post\.destroy}, m end + assert_match %r{def set_post}, content + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(:id\)}, content + else + assert_match %r{params\[:id\]}, content + end + assert_match %r{def post_params}, content - if Rails::VERSION::MAJOR >= 6 + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(post: \[ :title, :body, images: \[\] \]\)}, content + elsif Rails::VERSION::MAJOR >= 6 assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content else assert_match %r{params\.require\(:post\)\.permit\(:title, :body, :images\)}, content @@ -62,7 +71,11 @@ class ScaffoldApiControllerGeneratorTest < Rails::Generators::TestCase run_generator ["Message", "content:rich_text", "video:attachment", "photos:attachments"] assert_file 'app/controllers/messages_controller.rb' do |content| - assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content + else + assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content + end end end end diff --git a/test/scaffold_controller_generator_test.rb b/test/scaffold_controller_generator_test.rb index 10bf26bf..f2a06f9a 100644 --- a/test/scaffold_controller_generator_test.rb +++ b/test/scaffold_controller_generator_test.rb @@ -31,14 +31,14 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_instance_method :create, content do |m| assert_match %r{@post = Post\.new\(post_params\)}, m assert_match %r{@post\.save}, m - assert_match %r{format\.html \{ redirect_to post_url\(@post\), notice: "Post was successfully created\." \}}, m + assert_match %r{format\.html \{ redirect_to @post, notice: "Post was successfully created\." \}}, m assert_match %r{format\.json \{ render :show, status: :created, location: @post \}}, m assert_match %r{format\.html \{ render :new, status: :unprocessable_entity \}}, m assert_match %r{format\.json \{ render json: @post\.errors, status: :unprocessable_entity \}}, m end assert_instance_method :update, content do |m| - assert_match %r{format\.html \{ redirect_to post_url\(@post\), notice: "Post was successfully updated\." \}}, m + assert_match %r{format\.html \{ redirect_to @post, notice: "Post was successfully updated\." \}}, m assert_match %r{format\.json \{ render :show, status: :ok, location: @post \}}, m assert_match %r{format\.html \{ render :edit, status: :unprocessable_entity \}}, m assert_match %r{format\.json \{ render json: @post.errors, status: :unprocessable_entity \}}, m @@ -46,12 +46,21 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_instance_method :destroy, content do |m| assert_match %r{@post\.destroy}, m - assert_match %r{format\.html \{ redirect_to posts_url, notice: "Post was successfully destroyed\." \}}, m + assert_match %r{format\.html \{ redirect_to posts_path, status: :see_other, notice: "Post was successfully destroyed\." \}}, m assert_match %r{format\.json \{ head :no_content \}}, m end + assert_match %r{def set_post}, content + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(:id\)}, content + else + assert_match %r{params\[:id\]}, content + end + assert_match %r{def post_params}, content - if Rails::VERSION::MAJOR >= 6 + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(post: \[ :title, :body, images: \[\] \]\)}, content + elsif Rails::VERSION::MAJOR >= 6 assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content else assert_match %r{params\.require\(:post\)\.permit\(:title, :body, :images\)}, content @@ -64,15 +73,15 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase run_generator %w(Admin::Post --model-name=Post) assert_file 'app/controllers/admin/posts_controller.rb' do |content| assert_instance_method :create, content do |m| - assert_match %r{format\.html \{ redirect_to admin_post_url\(@post\), notice: "Post was successfully created\." \}}, m + assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully created\." \}}, m end assert_instance_method :update, content do |m| - assert_match %r{format\.html \{ redirect_to admin_post_url\(@post\), notice: "Post was successfully updated\." \}}, m + assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully updated\." \}}, m end assert_instance_method :destroy, content do |m| - assert_match %r{format\.html \{ redirect_to admin_posts_url, notice: "Post was successfully destroyed\." \}}, m + assert_match %r{format\.html \{ redirect_to admin_posts_path, status: :see_other, notice: "Post was successfully destroyed\." \}}, m end end end @@ -92,7 +101,11 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase run_generator %w(Message content:rich_text video:attachment photos:attachments) assert_file 'app/controllers/messages_controller.rb' do |content| - assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content + else + assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content + end end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 5fb9e7c5..d985da9c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -8,12 +8,16 @@ require "active_support/cache/memory_store" require "active_support/json" require "active_model" +require 'action_controller/railtie' +require 'action_view/railtie' require "active_support/testing/autorun" require "mocha/minitest" ActiveSupport.test_order = :random +ENV["RAILS_ENV"] ||= "test" + class << Rails def cache @cache ||= ActiveSupport::Cache::MemoryStore.new @@ -33,4 +37,11 @@ class Racer < Struct.new(:id, :name) include ActiveModel::Conversion end -ActionView::Template.register_template_handler :jbuilder, JbuilderHandler +# Instantiate an Application in order to trigger the initializers +Class.new(Rails::Application) do + config.secret_key_base = 'secret' + config.eager_load = false +end.initialize! + +# Touch AV::Base in order to trigger :action_view on_load hook before running the tests +ActionView::Base.inspect